Skip to content

ci: add build deb#4

Merged
datagutt merged 49 commits into
mainfrom
feat/uvc-hardening
Jun 15, 2026
Merged

ci: add build deb#4
datagutt merged 49 commits into
mainfrom
feat/uvc-hardening

Conversation

@datagutt

@datagutt datagutt commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added PTZ (pan/tilt/zoom) control via native GObject properties and a set-ptz action signal
    • Added optional Unix-domain control socket for remote PTZ command control
    • Added bounded auto-reconnect capability when device becomes unresponsive
    • Expanded device selection: supports VID:PID, serial number, and bus:address selectors in addition to ordinal index
    • Added SPS/PPS parameter set caching to optimize reconnection scenarios
    • Added live-source latency reporting based on negotiated frame interval
    • Added V4L2 H.264 capability probe support
  • Bug Fixes

    • Hardened NAL unit parsing and bounds checking
    • Fixed USB teardown to prevent double-close and resource leaks
    • Improved caps negotiation with stricter validation
    • Added PTS monotonicity guards to prevent backward timestamp jumps
    • Enhanced restart-state reset behavior
    • Fixed mutex/unlock deadlock prevention in teardown
  • Documentation

    • Rewrote README with usage examples, property reference, and control-socket protocol documentation
    • Added comprehensive CHANGELOG for 2.0 hardening release
    • Added architecture documentation and testing guidance
  • Tests

    • Integrated mock-based test infrastructure with sanitizer support (ASan/TSan/LSan)
    • Added comprehensive test coverage for all new features
  • Build & Infrastructure

    • Added multi-architecture Docker support for ARM64 and AMD64
    • Established CMake build system with automated testing
    • Added GitHub Actions CI workflow for build verification and functional tests

datagutt and others added 30 commits January 5, 2025 16:49
Use libuvc timestamps for setting more accurate PTS/DTS
de-jitter our PTSes to avoid skipped and duplicated frames
- Add build-check.yml: runs on PR/push to main, builds for arm64/amd64
- Replace publish-deb.yml with publish-release.yml
  - Manual workflow_dispatch trigger (was push/release)
  - CalVer versioning with stable/beta channels
  - Builds .deb packages and .tar.gz archives for both architectures
  - GPG signs APT repository metadata
  - Uploads to Cloudflare R2
  - Creates GitHub release with all artifacts and checksums
- Remove hardcoded ARM64 platform constraints from Dockerfile
- Add TARGETARCH build arg for architecture-agnostic builds
- Map TARGETARCH to GNU triplets (aarch64-linux-gnu, x86_64-linux-gnu)
- Update workflows to use native runners (ubuntu-24.04-arm64 for ARM64)
- Remove QEMU/emulation complexity from CI workflows
- Use matrix.config.runner and matrix.config.arch pattern

This enables faster builds using native ARM64 GitHub runners
instead of slow QEMU emulation on x86_64.
GitHub Actions ARM64 runners use 'ubuntu-24.04-arm' not 'ubuntu-24.04-arm64'.
This fixes the stuck/queued builds.
Catch-up merge of irlserver/gstlibuvch264src:main (9 commits, base 45df046)
into CERALIVE/main. Brings in the pipeline-clock timestamping rework,
libuvc_control socket + video control + zoom (canonical b11a18b), H265 UVC
support, the unistd.h fix, and the switch from bundled libuvc to upstream
v0.0.7 built with the bundled patches/.

Conflict resolution:
- libuvch264src/src/*.c,*.h: took irlserver (canonical functional changes;
  supersedes CERALIVE's rough 45ef3b4/ecee7c5 video-control commits).
- Dockerfile (add/add): irlserver's upstream-libuvc build stage + CERALIVE's
  multiarch model (TARGETARCH->triplet). Plugin and libuvc relocated under
  /usr/lib/<triplet>/ (preserving file names) so build-check finds
  build/usr/lib/*/gstreamer-1.0/ and publish-release packages build/usr/=/usr/.
- .github/workflows: kept CERALIVE's build-check.yml + publish-release.yml;
  dropped irlserver's publish-deb.yml (parallel IRL-branded CI infra).
- libuvc/ bundled tree deleted (replaced by patches/ + upstream clone).

Also pass -DCMAKE_POLICY_VERSION_MINIMUM=3.5 to the libuvc cmake invocation:
ubuntu:latest is now 26.04 with CMake 4.x, which rejects libuvc v0.0.7's
cmake_minimum_required(VERSION 3.1). Verified the amd64 image builds end to
end and installs build/usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstlibuvch264src.so.
* chore: add .clang-format

* test(gstlibuvch264src): add plugin-load smoke test

What
  Add a hardware-independent gst-check smoke test that verifies the
  libuvch264src plugin loads and its element registers correctly, wired as a
  ctest target.

Why
  The plugin had no automated test coverage. A load/registration smoke test
  catches symbol/registration regressions (renamed factories, dropped
  properties, broken pad templates) without needing a UVC capture device.

How
  - tests/test_plugin_load.c: gst-check suite asserting the plugin registers,
    both factories (libuvch264src + libuvch26xsrc alias) exist, the element is
    a GstPushSrc, the "index" string property defaults to "0", and the ALWAYS
    "src" pad template advertises video/x-h264.
  - Top-level CMakeLists.txt (TEST-ONLY): compiles the plugin from its
    unmodified source and vendors libuvc v0.0.7 + patches via FetchContent
    (mirroring the Dockerfile), then exposes the test via ctest. The canonical
    production build stays Meson; runtime behavior is unchanged.
  - tests/CMakeLists.txt: registers the ctest target, isolating GST_PLUGIN_PATH
    /registry and resolving the vendored libuvc at dlopen time.
  - CI: new smoke-test job in build-check.yml runs
    `cmake -B build && cmake --build build && ctest --test-dir build`.
  - Doc: AGENTS.md TEST section + structure refresh.

How to verify
  cmake -B build && cmake --build build && ctest --test-dir build --output-on-failure
  -> 1/1 passed (5 gst-check assertions). Negative control with an empty
  GST_PLUGIN_PATH fails all 5, proving the test is not vacuous.

Risks
  Low. Additive test-only build; no plugin source or Meson/Docker build change.
  The CMake path clones libuvc at build time (network), matching the Dockerfile.
Document the authoritative CalVer version scheme for gstlibuvch264src.

The .deb version is derived purely from git tags at publish time via the
publish-release.yml workflow. There is no separate VERSION file by design.

Scheme: YYYY.MINOR.PATCH
- YYYY = current year (UTC)
- MINOR = current month (UTC, no zero-pad)
- PATCH = monotonic counter per month

Example: 2026.6.1 (June 2026, patch 1)

Tag format:
- Stable: v<VERSION> (e.g., v2026.6.1)
- Beta: v<VERSION>-beta.<N> (e.g., v2026.6.2-beta.1)

The VERSION env var from calculate-version is passed directly to FPM's -v flag,
producing .deb packages with CalVer versions like:
  gstreamer1.0-libuvch264src_2026.6.1_arm64.deb

Single source of truth: git tag namespace (v*).

Verified: build-check gate green (plugin-load smoke test passed).
andrescera and others added 19 commits June 14, 2026 22:39
Untrusted NAL lengths from parse_nal_units could exceed the fixed 1024 B
vps/sps/pps instance buffers. Both the live frame_callback path and the
disk-cache load path memcpy'd straight into them, overflowing the GObject
allocation and corrupting adjacent fds/mutex/pointers.

Clamp every copy: drop any NAL with len <= 0 or len > SPSPPSBUFSZ with a
GST_WARNING instead of copying. Add an ASan regression test driven by the
mock feeder's oversized-SPS mode (sps_bounds + sps_bounds_asan); the mock
payload is enlarged so the unclamped overflow crosses the allocation
boundary where ASan's redzone lives.
…ward-compatible)

Parse the index property once at start() into a tagged selector and match one
enumerated device:
  - "N"            ordinal into the device list (unchanged; cerastream passes i.to_string())
  - "vid:pid"      hex vendor:product id
  - "serial:<sn>"  exact USB serial-number string
  - "bus:<b>:<a>"  decimal USB bus number and device address

Malformed selectors still fail loudly with RESOURCE/SETTINGS; a selector that
matches no enumerated device is RESOURCE/NOT_FOUND (never a silent fallback to
device 0). The mock gains per-device descriptors plus an opened-index probe, and
test_device_select covers all four forms, the ordinal backward-compat path, and
the no-match cases.
feat(uvc): robustness, reliability & multi-system hardening (v2026.6.2)
merge the hardening work from the downstream ceralive fork: module split
(frame_pipeline, spspps_cache, ptz_control, uvc_device, error), a mock-backed
ctest suite (tsan/asan variants), 19 memory-safety and correctness fixes
(sps/pps heap-overflow clamp, usb teardown double-close, nal 3-byte start
codes, device-list leak, strict index parsing, framerate guard, pts
monotonicity), native pan/tilt/zoom properties, richer device selection
(vid:pid / serial / bus), an opt-in hardened control socket under
$XDG_RUNTIME_DIR (mode 0600), latency query, and a v4l2 capability probe.

adaptations on merge:
- keep our publish-deb release ci; drop the fork's publish-release workflow
- pin plugin version to 2.0 (our scheme, not calver)
- control socket PAN_TILT/ZOOM now accept normalized floats in [-1,1] mapped
  onto the device range, matching the irl control client
- de-brand docs/comments to product-neutral wording; credit ceralive

verified: meson build loads (version 2.0), full ctest suite 36/36 pass.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR delivers the gstlibuvch264src 2.0 hardening release. The monolithic source is split into modules: an Annex-B NAL parser, SPS/PPS disk cache, PTZ Unix-socket control, USB teardown helper, and error mapper. The main element gains multi-selector device index, caps/latency hardening, flush-safe create() with bounded reconnect, and new GObject properties. A hardware-independent mock-backed ctest suite with ASan/TSan variants is wired via a new CMake build alongside a multi-arch Dockerfile.

Changes

gstlibuvch264src 2.0 Hardening Release

Layer / File(s) Summary
Element struct refactor and error module
libuvch264src/src/gstlibuvch264src.h, libuvch264src/src/gstlibuvch264src_internal.h, libuvch264src/src/gstlibuvch264src_error.h, libuvch264src/src/gstlibuvch264src_error.c
Instance struct moved from public header into gstlibuvch264src_internal.h, which adds control-socket and PTZ state fields; a new error module maps uvc_error_t to typed GST_ELEMENT_ERROR calls.
Annex-B NAL parser and frame_callback
libuvch264src/src/frame_pipeline.h, libuvch264src/src/frame_pipeline.c
Introduces nal_unit_t type, start-code scanner (find_nal_unit), count/parse helpers, and frame_callback that builds GstBuffer objects with PTS/DTS/DURATION clamping, prepends cached VPS/SPS/PPS to IDR frames, and pushes onto the async queue.
SPS/PPS cache path builder and I/O
libuvch264src/src/spspps_path.h, libuvch264src/src/spspps_cache.h, libuvch264src/src/spspps_cache.c
spspps_build_path constructs resolution-keyed filenames with safe strtol index clamping; load_spspps/store_spspps read/write VPS/SPS/PPS with per-unit size bounds checks.
V4L2 probe and USB teardown helper
libuvch264src/src/uvc_device.h, libuvch264src/src/uvc_device.c
Non-fatal VIDIOC_TRY_FMT H.264 probe on /dev/videoN and force_usb_release that derives interface count from the active config descriptor and releases interfaces without closing the shared libusb handle.
PTZ control socket, probing, and axis setters
libuvch264src/src/ptz_control.h, libuvch264src/src/ptz_control.c
Binds an owner-only AF_UNIX socket under XDG_RUNTIME_DIR, runs a select()-based command-dispatch thread, probes UVC absolute pan/tilt/zoom ranges on start, maps normalized [-1,1] inputs to device ranges, and provides clamping setters guarded by control_mutex.
Main element rewrite
libuvch264src/src/gstlibuvch264src.c, libuvch264src/src/meson.build
Installs pan/tilt/zoom/control-socket/reconnect GObject properties and set-ptz action signal; hardens caps negotiation; adds GST_QUERY_LATENCY reporting; expands index to vid:pid, serial:, bus: selectors; rewrites create() with bounded timeout, silence-based disconnect detection, exponential-backoff reconnect, and flush-sentinel for unlock(); defers control_mutex clear to finalize().
Build system and tooling
CMakeLists.txt, Dockerfile, libuvch264src/meson.build, .clang-format, .gitignore, .github/workflows/build-check.yml
New root CMakeLists.txt with conditional FetchContent libuvc and RPATH; Dockerfile updated for multi-arch buildx with pinned ubuntu:24.04 digest and TARGETARCH→triplet mapping; meson.build version bumped to 2.0; LLVM/Allman .clang-format and .gitignore added; CI workflow added for Docker build verification and sanitized ctest run.
Mock test doubles
tests/mock_libuvc.h, tests/mock_libuvc.c, tests/mock_libusb.h, tests/mock_libusb.c
Full threaded libuvc API replacement with configurable device descriptors, frame modes (disconnect, nonidr-lead), PTZ stubs, and observable counters; libusb-1.0 subset mock for teardown regression testing with interface-count control and open/close/config-query counters.
ctest suite with sanitizer variants
tests/CMakeLists.txt, tests/test_*.c, tests/tsan*.suppressions, tests/lsan_negotiate.suppressions
1247-line CMakeLists wires 20+ test executables with isolated GST registry/HOME, RESOURCE_LOCK on the control socket, and ASan/TSan variants; test files cover plugin introspection, cache path safety, NAL parsing, device selection, PTZ property/signal/gating, socket bind/unbind, error mapping, SPS bounds clamping, PTS monotonicity/thread-safety, USB teardown double-close regression, negotiate edge cases, live-source latency/offsets, functional H.264/H.265 caps matrix, disconnect detection, and reconnect resume.
README, CHANGELOG, and reconnect spike
README.md, CHANGELOG.md, libuvch264src/docs/notes/reconnect-spike.md
README rewritten with pipeline examples, full Properties/socket-protocol docs, multiarch build and test steps; CHANGELOG documents all 2.0 fixes, features, and risks; reconnect-spike.md records teardown safety verdict and recommended reopen sequence.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 A bunny once wrestled a camera device,
With NAL units parsed and PTS kept precise.
PTZ sockets bound, reconnect on unplug,
Mocks and sanitizers, all snug as a bug.
The H.265 frames now arrive without doubt —
Version two is hardened, hooray! Hip-hip-shout! 📷

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/uvc-hardening

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Dockerfile (1)

64-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run the runtime image as non-root by default.

Line 64 starts the runtime stage, but no USER is set, so the final container runs as root. This increases impact if the process or plugin is compromised.

🔧 Suggested hardening patch
 FROM ubuntu:24.04@sha256:786a8b558f7be160c6c8c4a54f9a57274f3b4fb1491cf65146521ae77ff1dc54 AS runtime
@@
 RUN apt-get update && apt-get install -y \
 	libgstreamer1.0-0 \
 	libgstreamer-plugins-base1.0-0 \
 	libusb-1.0-0 \
 	&& rm -rf /var/lib/apt/lists/*
@@
 RUN GNUARCH=$(cat /tmp/gnuarch) && \
 	mv /tmp/stage/libgstlibuvch264src.so /usr/lib/${GNUARCH}/gstreamer-1.0/ && \
 	mv /tmp/stage/libuvc.* /usr/lib/${GNUARCH}/ && \
 	rm -rf /tmp/stage /tmp/gnuarch
+
+# Drop root privileges for runtime execution.
+RUN groupadd -r gst && useradd -r -g gst -d /nonexistent -s /usr/sbin/nologin gst
+USER gst
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 64 - 97, The runtime stage in the Dockerfile does
not specify a non-root user, causing the final container to run as root which
increases security risk. Add a USER directive at the end of the runtime stage
(after the final RUN command that moves the GStreamer plugin and libuvc
libraries) to specify a non-root user account. This ensures the container runs
with reduced privileges by default.

Source: Linters/SAST tools

🧹 Nitpick comments (3)
libuvch264src/src/frame_pipeline.c (1)

248-258: 💤 Low value

Minor: offset variable is int while used with gsize buffer operations.

The local offset variable at line 249 is declared as int, but it's used alongside gsize values for buffer fill positions. While SPSPPSBUFSZ bounds keep the combined offset well under INT_MAX, using gsize would be more consistent with the rest of the function.

Suggested fix
-                    int offset = 0;
+                    gsize offset = 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libuvch264src/src/frame_pipeline.c` around lines 248 - 258, The offset
variable is declared as int but is used with GStreamer buffer operations that
expect gsize types. Change the declaration of the offset variable from int to
gsize to ensure consistency with the gst_buffer_fill function calls that follow.
This aligns with GStreamer API conventions where buffer positions and sizes use
gsize rather than int.
tests/test_lifecycle.c (1)

122-129: ⚡ Quick win

Prefer explicit state convergence checks over fixed sleep in restart cycles.

Using a fixed 100ms delay makes this regression test timing-sensitive. Waiting for state completion each cycle (gst_element_get_state with bounded timeout) gives deterministic coverage of the mutex lifecycle path.

Refactor sketch
   for (int i = 0; i < cycles; i++) {
     fail_unless (gst_element_set_state (pipeline, GST_STATE_PLAYING)
         != GST_STATE_CHANGE_FAILURE, "cycle %d: could not set PLAYING", i);
-    /* Let start() bring the control thread fully up before tearing it down. */
-    g_usleep (100 * G_TIME_SPAN_MILLISECOND);
+    fail_unless (gst_element_get_state (pipeline, NULL, NULL, 5 * GST_SECOND)
+        != GST_STATE_CHANGE_FAILURE, "cycle %d: PLAYING did not converge", i);
     fail_unless (gst_element_set_state (pipeline, GST_STATE_NULL)
         != GST_STATE_CHANGE_FAILURE, "cycle %d: could not set NULL", i);
+    fail_unless (gst_element_get_state (pipeline, NULL, NULL, 5 * GST_SECOND)
+        != GST_STATE_CHANGE_FAILURE, "cycle %d: NULL did not converge", i);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_lifecycle.c` around lines 122 - 129, The restart cycle loop uses a
fixed 100ms sleep delay with g_usleep to wait for the control thread to complete
state setup before transitioning to NULL, which makes the test timing-sensitive
and non-deterministic. Replace the g_usleep call with an explicit state
convergence check using gst_element_get_state with a bounded timeout after
setting the pipeline to PLAYING state. This ensures the test waits for the
actual state change to complete before proceeding to the NULL state transition,
providing deterministic coverage of the mutex lifecycle path without relying on
arbitrary sleep durations.
.github/workflows/build-check.yml (1)

34-37: ⚡ Quick win

Verify the plugin artifact directly, not just the directory.

The current check can pass when the directory exists but the plugin .so is absent or renamed. Assert the expected artifact path explicitly.

✅ Stronger artifact assertion
       - name: Verify plugin was built
         run: |
-          ls -la build/usr/lib/*/gstreamer-1.0/
+          ls -la build/usr/lib/*/gstreamer-1.0/
+          test -f build/usr/lib/*/gstreamer-1.0/libgstlibuvch264src.so
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-check.yml around lines 34 - 37, The "Verify plugin
was built" step currently only lists directory contents with ls, which allows
the check to pass even if the actual plugin .so file is missing or has been
renamed. Replace the ls command with an explicit test assertion that verifies
the expected plugin artifact file exists at its full path (e.g., using a test
command like [ -f ] or similar to assert the presence of the .so file in the
build/usr/lib/*/gstreamer-1.0/ directory). This ensures the verification
actually confirms the plugin binary was successfully created.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/build-check.yml:
- Around line 21-22: The actions/checkout@v4 steps are missing the
persist-credentials configuration, which allows token-backed git credentials to
remain in the workspace and creates a security risk. At
.github/workflows/build-check.yml lines 21-22 (anchor) and lines 49-50
(sibling), add persist-credentials: false to the with section of each
actions/checkout@v4 step to ensure credentials are not persisted after the
checkout completes.
- Around line 1-9: The GitHub Actions workflow in build-check.yml is using
default token permissions instead of explicitly declaring minimal,
least-privilege permissions. Add a permissions block at the workflow level
(after the on section and before the jobs section) to explicitly set minimal
read-only permissions. For a build check workflow, this typically means setting
permissions to read-only access for repository contents or using an empty
permissions object if no special token permissions are required.
- Line 22: Replace all mutable GitHub Actions version tags with exact
40-character commit SHAs in the build-check.yml workflow file. For each uses
statement at lines 22, 25, 28, and 50, change the reference from the format
actions/[owner]/[action]`@v`[version] to
actions/[owner]/[action]@[full-40-char-commit-sha]. This pins the actions to
immutable commit references, protecting against supply-chain attacks and
upstream action substitution.

In `@libuvch264src/src/frame_pipeline.c`:
- Around line 19-25: The H.265 frame type mapping in the switch statement within
the UVC_FRAME_FORMAT_H265 conditional block is incomplete. Currently only case 1
maps to UNIT_FRAME_NON_IDR and case 20 maps to UNIT_FRAME_IDR, while all other
cases implicitly return UNIT_INVALID, causing frame loss. Extend the switch
cases to handle all H.265 non-IDR slice types 0–9 by adding multiple case labels
(case 0 through case 9) that all return UNIT_FRAME_NON_IDR. Additionally,
evaluate whether H.265 recovery point types 16–18 (BLA_W_LP, BLA_W_RADL,
BLA_N_LP) should be handled as IDR frames or a separate recovery point type
depending on your application requirements. Verify that UVC cameras in your
target deployment emit slice types beyond 1 and 20 to ensure the complete
mapping prevents frame loss.

In `@libuvch264src/src/spspps_path.h`:
- Around line 43-45: When the HOME environment variable is not set and home_dir
becomes NULL, the current code at lines 43-45 unsafely falls back to an empty
string, which causes subsequent path construction at lines 49 and 61-62 to build
paths under root-level directories like /.spspps. Instead of allowing this
fallback, implement fail-closed behavior by returning early from the function or
setting an error state at lines 43-45 when home_dir is NULL, preventing the
function from continuing and avoiding any root-level cache writes at lines 49
and 61-62.

In `@libuvch264src/src/uvc_device.c`:
- Around line 88-97: The comment in the UVC device code states the intent is to
"Reattach detached interfaces to the kernel" but the implementation calls
libusb_detach_kernel_driver() which does the opposite. To align the code with
the stated goal of restoring kernel control over the device, replace the
libusb_detach_kernel_driver() call with libusb_attach_kernel_driver() to
actually reattach the kernel driver to each interface. The surrounding loop
logic with libusb_kernel_driver_active() will then correctly identify which
interfaces need reattachment and restore them to a usable state for other
consumers.

In `@tests/mock_libuvc.c`:
- Around line 408-415: Add a NULL check immediately after the calloc call in the
mock_new_device function to guard against allocation failures. Before
dereferencing dev with dev->ctx, dev->index, or dev->refcount, verify that
calloc did not return NULL and return NULL early if allocation failed.
Additionally, the caller loop at line 462 must be updated to handle the case
where mock_new_device returns NULL, ensuring the test does not attempt to use a
NULL device pointer.

In `@tests/test_error_map.c`:
- Around line 32-35: Replace the call to gst_bus_pop_filtered() with
gst_bus_timed_pop_filtered() and provide a reasonable timeout value (in
milliseconds) as the second parameter. This change prevents the test from
hanging indefinitely if the GST_MESSAGE_ERROR fails to be posted to the bus for
any reason, allowing the test to fail promptly and enabling proper teardown
instead of waiting for an external timeout.

In `@tests/test_functional.c`:
- Around line 122-138: Move the pipeline teardown calls
(gst_element_set_state(pipeline, GST_STATE_NULL) and gst_object_unref(pipeline))
to execute before the fail_unless() assertion checks in both affected files. In
tests/test_functional.c (lines 122-138), relocate the state-change and unref
calls from after the assertions to immediately after the gst_message_unref(msg)
and gst_object_unref(bus) calls and before the fail_unless() checks. Apply the
same reordering in tests/test_mock_smoke.c (lines 75-92) to ensure cleanup
completes before any longjmp from a failed assertion can occur, preventing the
streaming pipeline from remaining active and causing resource leaks.

In `@tests/test_socket.c`:
- Around line 169-170: The g_str_has_prefix checks in the underA and underB
variable assignments are vulnerable to false positives with sibling directory
names (e.g., /tmp/run matching /tmp/run2). For each g_str_has_prefix call, add
an additional boundary-safety check to ensure the matched prefix is followed by
a path separator (/) or is at the end of the string. This prevents the function
from incorrectly matching partial directory names that happen to share a common
prefix.

In `@tests/test_sps_bounds.c`:
- Around line 101-109: The error branch calls fail() before cleaning up
resources like the pipeline and bus, which skips teardown and can cause hanging
or flaky behavior. Restructure the error handling in the GST_MESSAGE_ERROR
branch (where gst_message_parse_error is called) to ensure that any pipeline and
bus unreffing/cleanup operations occur before the fail() call, so that shutdown
happens even when the test fails on error.

---

Outside diff comments:
In `@Dockerfile`:
- Around line 64-97: The runtime stage in the Dockerfile does not specify a
non-root user, causing the final container to run as root which increases
security risk. Add a USER directive at the end of the runtime stage (after the
final RUN command that moves the GStreamer plugin and libuvc libraries) to
specify a non-root user account. This ensures the container runs with reduced
privileges by default.

---

Nitpick comments:
In @.github/workflows/build-check.yml:
- Around line 34-37: The "Verify plugin was built" step currently only lists
directory contents with ls, which allows the check to pass even if the actual
plugin .so file is missing or has been renamed. Replace the ls command with an
explicit test assertion that verifies the expected plugin artifact file exists
at its full path (e.g., using a test command like [ -f ] or similar to assert
the presence of the .so file in the build/usr/lib/*/gstreamer-1.0/ directory).
This ensures the verification actually confirms the plugin binary was
successfully created.

In `@libuvch264src/src/frame_pipeline.c`:
- Around line 248-258: The offset variable is declared as int but is used with
GStreamer buffer operations that expect gsize types. Change the declaration of
the offset variable from int to gsize to ensure consistency with the
gst_buffer_fill function calls that follow. This aligns with GStreamer API
conventions where buffer positions and sizes use gsize rather than int.

In `@tests/test_lifecycle.c`:
- Around line 122-129: The restart cycle loop uses a fixed 100ms sleep delay
with g_usleep to wait for the control thread to complete state setup before
transitioning to NULL, which makes the test timing-sensitive and
non-deterministic. Replace the g_usleep call with an explicit state convergence
check using gst_element_get_state with a bounded timeout after setting the
pipeline to PLAYING state. This ensures the test waits for the actual state
change to complete before proceeding to the NULL state transition, providing
deterministic coverage of the mutex lifecycle path without relying on arbitrary
sleep durations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e710c8e-3faa-41b1-95f0-0375de39a59f

📥 Commits

Reviewing files that changed from the base of the PR and between 8f913f6 and ba16ac7.

📒 Files selected for processing (50)
  • .clang-format
  • .github/workflows/build-check.yml
  • .gitignore
  • CHANGELOG.md
  • CMakeLists.txt
  • Dockerfile
  • README.md
  • libuvch264src/docs/notes/reconnect-spike.md
  • libuvch264src/meson.build
  • libuvch264src/src/frame_pipeline.c
  • libuvch264src/src/frame_pipeline.h
  • libuvch264src/src/gstlibuvch264src.c
  • libuvch264src/src/gstlibuvch264src.h
  • libuvch264src/src/gstlibuvch264src_error.c
  • libuvch264src/src/gstlibuvch264src_error.h
  • libuvch264src/src/gstlibuvch264src_internal.h
  • libuvch264src/src/meson.build
  • libuvch264src/src/ptz_control.c
  • libuvch264src/src/ptz_control.h
  • libuvch264src/src/spspps_cache.c
  • libuvch264src/src/spspps_cache.h
  • libuvch264src/src/spspps_path.h
  • libuvch264src/src/uvc_device.c
  • libuvch264src/src/uvc_device.h
  • tests/CMakeLists.txt
  • tests/lsan_negotiate.suppressions
  • tests/mock_libusb.c
  • tests/mock_libusb.h
  • tests/mock_libuvc.c
  • tests/mock_libuvc.h
  • tests/test_cache.c
  • tests/test_device_select.c
  • tests/test_error_map.c
  • tests/test_functional.c
  • tests/test_lifecycle.c
  • tests/test_live_source.c
  • tests/test_mock_smoke.c
  • tests/test_nal_parse.c
  • tests/test_negotiate.c
  • tests/test_plugin_load.c
  • tests/test_pts_monotonic.c
  • tests/test_pts_thread_safety.c
  • tests/test_ptz.c
  • tests/test_reconnect.c
  • tests/test_socket.c
  • tests/test_sps_bounds.c
  • tests/test_usb_teardown.c
  • tests/test_v4l2_probe.c
  • tests/tsan.suppressions
  • tests/tsan_pts.suppressions

Comment on lines +1 to +9
name: Build Check

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Set least-privilege token permissions explicitly.

The workflow currently inherits default token permissions; declare minimal read-only permissions at workflow (or per-job) scope.

🔒 Suggested hardening
 name: Build Check
 
 on:
   push:
     branches: [main]
   pull_request:
     branches: [main]
+
+permissions:
+  contents: read
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name: Build Check
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
name: Build Check
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 1-83: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-check.yml around lines 1 - 9, The GitHub Actions
workflow in build-check.yml is using default token permissions instead of
explicitly declaring minimal, least-privilege permissions. Add a permissions
block at the workflow level (after the on section and before the jobs section)
to explicitly set minimal read-only permissions. For a build check workflow,
this typically means setting permissions to read-only access for repository
contents or using an empty permissions object if no special token permissions
are required.

Source: Linters/SAST tools

Comment on lines +21 to +22
- name: Checkout code
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disable credential persistence in checkout steps.

actions/checkout should set persist-credentials: false to avoid leaving token-backed git credentials in the workspace config.

🔐 Suggested change
       - name: Checkout code
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
@@
       - name: Checkout code
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false

Also applies to: 49-50

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 21-22: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 22-22: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-check.yml around lines 21 - 22, The
actions/checkout@v4 steps are missing the persist-credentials configuration,
which allows token-backed git credentials to remain in the workspace and creates
a security risk. At .github/workflows/build-check.yml lines 21-22 (anchor) and
lines 49-50 (sibling), add persist-credentials: false to the with section of
each actions/checkout@v4 step to ensure credentials are not persisted after the
checkout completes.

Source: Linters/SAST tools

runner: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expect no output after pinning to 40-char SHAs.
rg -nP 'uses:\s*[^@]+@(?![0-9a-fA-F]{40}\b)' .github/workflows/build-check.yml

Repository: irlserver/gstlibuvch264src

Length of output: 236


Pin GitHub Actions to immutable commit SHAs instead of version tags.

Using mutable tags (@v*) exposes the workflow to upstream action substitution and supply-chain attacks. Pin all uses: statements to exact 40-character commit SHAs.

Applies to lines: 22, 25, 28, 50

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 21-22: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 22-22: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-check.yml at line 22, Replace all mutable GitHub
Actions version tags with exact 40-character commit SHAs in the build-check.yml
workflow file. For each uses statement at lines 22, 25, 28, and 50, change the
reference from the format actions/[owner]/[action]`@v`[version] to
actions/[owner]/[action]@[full-40-char-commit-sha]. This pins the actions to
immutable commit references, protecting against supply-chain attacks and
upstream action substitution.

Source: Linters/SAST tools

Comment on lines +19 to +25
} else if (format == UVC_FRAME_FORMAT_H265) {
switch (type) {
case 1:
return UNIT_FRAME_NON_IDR;
case 20:
return UNIT_FRAME_IDR;
case 32:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are tests or documentation mentioning H.265 NAL type handling
rg -n -C2 'HEVC|H\.265|h265' --type=c

Repository: irlserver/gstlibuvch264src

Length of output: 8885


🏁 Script executed:

# Check the convert_unit_type function and understand how unmapped types are handled
sed -n '1,40p' libuvch264src/src/frame_pipeline.c | cat -n

Repository: irlserver/gstlibuvch264src

Length of output: 1468


🏁 Script executed:

# Check what happens when an unmapped NAL type is encountered
rg -n 'UNIT_INVALID|UNIT_FRAME' libuvch264src/src/frame_pipeline.c | head -20

Repository: irlserver/gstlibuvch264src

Length of output: 482


🏁 Script executed:

# Look at frame_callback to see how it handles UNIT_INVALID
sed -n '128,170p' libuvch264src/src/frame_pipeline.c | cat -n

Repository: irlserver/gstlibuvch264src

Length of output: 2180


🌐 Web query:

H.265 HEVC NAL unit types slice types 0-21 specification

💡 Result:

In the H.265/HEVC (High Efficiency Video Coding) standard, Network Abstraction Layer (NAL) unit types are defined in Table 7-1 of the ITU-T H.265 specification [1][2][3]. The values 0 through 21 correspond specifically to Video Coding Layer (VCL) NAL units, which contain the actual slice segment data [4][5]. The NAL unit type codes 0-21 are specified as follows [2][3][4]: 0: TRAIL_N (Trailing, non-TSA, non-STSA) 1: TRAIL_R (Trailing, REAP) 2: TSA_N (Temporal sub-layer access, non-TSA) 3: TSA_R (Temporal sub-layer access) 4: STSA_N (Step-wise temporal sub-layer access, non-STSA) 5: STSA_R (Step-wise temporal sub-layer access) 6: RADL_N (Random access decodable leading, non-RADL) 7: RADL_R (Random access decodable leading) 8: RASL_N (Random access skipped leading, non-RASL) 9: RASL_R (Random access skipped leading) 10: RSV_VCL_N10 (Reserved VCL NAL unit type) 11: RSV_VCL_R11 (Reserved VCL NAL unit type) 12: RSV_VCL_N12 (Reserved VCL NAL unit type) 13: RSV_VCL_R13 (Reserved VCL NAL unit type) 14: RSV_VCL_N14 (Reserved VCL NAL unit type) 15: RSV_VCL_R15 (Reserved VCL NAL unit type) 16: BLA_W_LP (Broken link access with leading pictures) 17: BLA_W_RADL (Broken link access with RADL pictures) 18: BLA_N_LP (Broken link access with no leading pictures) 19: IDR_W_RADL (Instantaneous decoding refresh with RADL pictures) 20: IDR_N_LP (Instantaneous decoding refresh with no leading pictures) 21: CRA_NUT (Clean random access) These types categorize the NAL units based on their role in the decoding process, such as whether they are leading pictures, random access points (like IDR or CRA), or standard trailing pictures [5]. Note that "slice type" is a distinct concept within the slice segment header (e.g., I, P, or B slices) and should not be confused with the NAL unit type [5][6].

Citations:


H.265 non-IDR slice type mapping is incomplete and should handle all slice types 0–9 and potentially types 16–18.

H.265 defines non-IDR slice NAL types 0–9 (TRAIL_N, TRAIL_R, TSA_N, TSA_R, STSA_N, STSA_R, RADL_N, RADL_R, RASL_N, RASL_R) and IDR-like types 16–18 (BLA_W_LP, BLA_W_RADL, BLA_N_LP) per ITU-T H.265. Currently only type 1 maps to UNIT_FRAME_NON_IDR; all others return UNIT_INVALID and are silently dropped in frame_callback().

The mapping should be extended to at minimum:

case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9:
    return UNIT_FRAME_NON_IDR;

Types 16–18 (BLA_LP, BLA_RADL, BLA_N_LP) may also warrant handling as recovery point slices depending on the application requirements (they function similarly to IDR frames in random access scenarios). Consider whether these should map to UNIT_FRAME_IDR or a separate recovery point type.

Verify whether UVC cameras in the target deployment emit H.265 slice types beyond 1 and 20; if they do, the incomplete mapping will cause frame loss.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libuvch264src/src/frame_pipeline.c` around lines 19 - 25, The H.265 frame
type mapping in the switch statement within the UVC_FRAME_FORMAT_H265
conditional block is incomplete. Currently only case 1 maps to
UNIT_FRAME_NON_IDR and case 20 maps to UNIT_FRAME_IDR, while all other cases
implicitly return UNIT_INVALID, causing frame loss. Extend the switch cases to
handle all H.265 non-IDR slice types 0–9 by adding multiple case labels (case 0
through case 9) that all return UNIT_FRAME_NON_IDR. Additionally, evaluate
whether H.265 recovery point types 16–18 (BLA_W_LP, BLA_W_RADL, BLA_N_LP) should
be handled as IDR frames or a separate recovery point type depending on your
application requirements. Verify that UVC cameras in your target deployment emit
slice types beyond 1 and 20 to ensure the complete mapping prevents frame loss.

Comment on lines +43 to +45
if (home_dir == NULL) {
home_dir = "";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when HOME is missing to avoid root-level cache writes.

Line 43 currently falls back to home_dir = "", and Lines 49/61 then build paths under /.spspps. If HOME is unset, cache I/O can target a global root path instead of user scope.

Suggested fix
-    if (home_dir == NULL) {
-        home_dir = "";
-    }
+    if (home_dir == NULL || home_dir[0] == '\0') {
+        return -1;
+    }

Also applies to: 49-49, 61-62

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libuvch264src/src/spspps_path.h` around lines 43 - 45, When the HOME
environment variable is not set and home_dir becomes NULL, the current code at
lines 43-45 unsafely falls back to an empty string, which causes subsequent path
construction at lines 49 and 61-62 to build paths under root-level directories
like /.spspps. Instead of allowing this fallback, implement fail-closed behavior
by returning early from the function or setting an error state at lines 43-45
when home_dir is NULL, preventing the function from continuing and avoiding any
root-level cache writes at lines 49 and 61-62.

Comment thread tests/mock_libuvc.c
Comment on lines +408 to +415
static uvc_device_t *mock_new_device(uvc_context_t *ctx, int index) {
uvc_device_t *dev = calloc(1, sizeof(*dev));
dev->ctx = ctx;
dev->index = index;
dev->refcount = 1;
if (ctx->device_count < MOCK_MAX_DEVICES)
ctx->devices[ctx->device_count++] = dev;
return dev;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard allocation failure before dereferencing mock device pointers.

Line 410 dereferences dev immediately after calloc without a NULL check. On OOM, this can crash the test process, and the caller loop at Line 462 currently assumes allocation always succeeded.

🛠️ Proposed fix
 static uvc_device_t *mock_new_device(uvc_context_t *ctx, int index) {
   uvc_device_t *dev = calloc(1, sizeof(*dev));
+  if (!dev)
+    return NULL;
   dev->ctx = ctx;
   dev->index = index;
   dev->refcount = 1;
   if (ctx->device_count < MOCK_MAX_DEVICES)
     ctx->devices[ctx->device_count++] = dev;
   return dev;
 }
@@
   for (int i = 0; i < n; i++)
-    list[i] = mock_new_device(ctx, i);
+  {
+    list[i] = mock_new_device(ctx, i);
+    if (!list[i]) {
+      free(list);
+      *devs = NULL;
+      return UVC_ERROR_NO_MEM;
+    }
+  }

Also applies to: 461-463

🧰 Tools
🪛 Cppcheck (2.20.0)

[warning] 410-410: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 411-411: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)


[warning] 412-412: If memory allocation fails, then there is a possible null pointer dereference

(nullPointerOutOfMemory)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/mock_libuvc.c` around lines 408 - 415, Add a NULL check immediately
after the calloc call in the mock_new_device function to guard against
allocation failures. Before dereferencing dev with dev->ctx, dev->index, or
dev->refcount, verify that calloc did not return NULL and return NULL early if
allocation failed. Additionally, the caller loop at line 462 must be updated to
handle the case where mock_new_device returns NULL, ensuring the test does not
attempt to use a NULL device pointer.

Source: Linters/SAST tools

Comment thread tests/test_error_map.c
Comment on lines +32 to +35
GstMessage *msg = gst_bus_pop_filtered (bus, GST_MESSAGE_ERROR);
fail_unless (msg != NULL,
"expected a GST_MESSAGE_ERROR on the bus (uvc_error_t=%d, disconnect=%d)",
err, disconnect);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "test_error_map.c" -type f

Repository: irlserver/gstlibuvch264src

Length of output: 95


🏁 Script executed:

head -50 tests/test_error_map.c

Repository: irlserver/gstlibuvch264src

Length of output: 2211


🌐 Web query:

GStreamer gst_bus_pop_filtered gst_bus_timed_pop_filtered API documentation

💡 Result:

In GStreamer, gst_bus_pop_filtered and gst_bus_timed_pop_filtered are functions used to retrieve specific types of messages from a GstBus while discarding non-matching messages [1][2][3]. gst_bus_pop_filtered This function retrieves the next message from the bus that matches the provided types mask [1][4]. Any messages posted before the first matching message that do not match the mask are discarded [1][5]. If no message matches the type, all existing messages are discarded, and it returns NULL [1][4]. The returned message must be unreferenced using gst_message_unref after use [4][5]. gst_bus_timed_pop_filtered This function acts similarly to gst_bus_pop_filtered but includes a timeout mechanism [2][3]. It waits up to the specified timeout (in nanoseconds) for a matching message, while discarding non-matching messages encountered during the wait [2][6]. Key behaviors: - If timeout is 0, the function behaves identically to gst_bus_pop_filtered [2][3]. - If timeout is GST_CLOCK_TIME_NONE, the function will block indefinitely until a matching message is posted [2][6]. - If no matching message is found before the timeout expires, it returns NULL [2][6]. - Similar to the non-timed version, the returned message must be unreferenced using gst_message_unref [2][6]. For most applications, GStreamer developers recommend using an asynchronous bus watch rather than these synchronous polling functions, except for specific cases involving short timeouts [1][4][2].

Citations:


🏁 Script executed:

cat -n tests/test_error_map.c | sed -n '20,60p'

Repository: irlserver/gstlibuvch264src

Length of output: 1911


🏁 Script executed:

rg -A 10 "gst_libuvc_h264_src_post_error\|gst_libuvc_h264_src_post_disconnect_error" --type c

Repository: irlserver/gstlibuvch264src

Length of output: 52


🏁 Script executed:

fd -type f -name "*.c" -o -name "*.h" | xargs rg -l "gst_libuvc_h264_src_post_error|gst_libuvc_h264_src_post_disconnect_error" | head -5

Repository: irlserver/gstlibuvch264src

Length of output: 421


🏁 Script executed:

find . -name "*error*" -type f | grep -E '\.(c|h)$'

Repository: irlserver/gstlibuvch264src

Length of output: 185


🏁 Script executed:

cat -n libuvch264src/src/gstlibuvch264src_error.c

Repository: irlserver/gstlibuvch264src

Length of output: 2098


🏁 Script executed:

cat -n libuvch264src/src/gstlibuvch264src_error.h

Repository: irlserver/gstlibuvch264src

Length of output: 1344


Replace gst_bus_pop_filtered() with timed variant to prevent indefinite test hangs.

The test posts an error message and immediately pops it from the bus. However, gst_bus_pop_filtered() blocks indefinitely if the message isn't found—if error posting fails silently for any reason, the test hangs until external timeout rather than failing quickly. Use gst_bus_timed_pop_filtered() with a reasonable timeout to ensure test failures surface promptly and teardown runs.

Proposed fix
-  GstMessage *msg = gst_bus_pop_filtered (bus, GST_MESSAGE_ERROR);
+  GstMessage *msg =
+      gst_bus_timed_pop_filtered (bus, 2 * GST_SECOND, GST_MESSAGE_ERROR);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
GstMessage *msg = gst_bus_pop_filtered (bus, GST_MESSAGE_ERROR);
fail_unless (msg != NULL,
"expected a GST_MESSAGE_ERROR on the bus (uvc_error_t=%d, disconnect=%d)",
err, disconnect);
GstMessage *msg =
gst_bus_timed_pop_filtered (bus, 2 * GST_SECOND, GST_MESSAGE_ERROR);
fail_unless (msg != NULL,
"expected a GST_MESSAGE_ERROR on the bus (uvc_error_t=%d, disconnect=%d)",
err, disconnect);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_error_map.c` around lines 32 - 35, Replace the call to
gst_bus_pop_filtered() with gst_bus_timed_pop_filtered() and provide a
reasonable timeout value (in milliseconds) as the second parameter. This change
prevents the test from hanging indefinitely if the GST_MESSAGE_ERROR fails to be
posted to the bus for any reason, allowing the test to fail promptly and
enabling proper teardown instead of waiting for an external timeout.

Comment thread tests/test_functional.c
Comment on lines +122 to +138
if (msg != NULL && GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
GError *gerr = NULL;
gchar *dbg = NULL;
gst_message_parse_error (msg, &gerr, &dbg);
fail ("pipeline errored instead of reaching EOS: %s (%s)",
gerr ? gerr->message : "(none)", dbg ? dbg : "(no debug)");
g_clear_error (&gerr);
g_free (dbg);
}
fail_unless (msg != NULL, "timed out waiting for EOS");
fail_unless (GST_MESSAGE_TYPE (msg) == GST_MESSAGE_EOS,
"expected EOS, got %s", GST_MESSAGE_TYPE_NAME (msg));
gst_message_unref (msg);
gst_object_unref (bus);

gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (pipeline);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Read the actual code from test_functional.c at lines 122-138
sed -n '122,138p' tests/test_functional.c

Repository: irlserver/gstlibuvch264src

Length of output: 727


🏁 Script executed:

# Read the actual code from test_mock_smoke.c at lines 75-92
sed -n '75,92p' tests/test_mock_smoke.c

Repository: irlserver/gstlibuvch264src

Length of output: 768


🏁 Script executed:

# Check what fail() and fail_unless() do - likely from a test framework header
find . -name "*.h" -o -name "*.c" | xargs grep -l "define fail\|^fail(" | head -5

Repository: irlserver/gstlibuvch264src

Length of output: 52


🏁 Script executed:

# Look for other similar cleanup patterns in test files to understand common practice
rg "gst_element_set_state.*GST_STATE_NULL" tests/ -A 2 -B 10 | head -100

Repository: irlserver/gstlibuvch264src

Length of output: 5658


Move pipeline teardown before EOS/error assertion checks to prevent resource leaks when assertions fail.

In both tests/test_functional.c (lines 122–138) and tests/test_mock_smoke.c (lines 75–92), fail_unless() calls execute before gst_element_set_state(pipeline, GST_STATE_NULL) and gst_object_unref(pipeline). When assertions fail, fail_unless() longjmps past the cleanup code, leaving the streaming pipeline active and preventing proper resource teardown. Other test files in this repository (test_usb_teardown.c, test_reconnect.c) explicitly document and follow the correct pattern: capture outcome details, execute cleanup, then assert.

Move the state-change and unref calls before the fail_unless() checks in both files.

📍 Affects 2 files
  • tests/test_functional.c#L122-L138 (this comment)
  • tests/test_mock_smoke.c#L75-L92
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_functional.c` around lines 122 - 138, Move the pipeline teardown
calls (gst_element_set_state(pipeline, GST_STATE_NULL) and
gst_object_unref(pipeline)) to execute before the fail_unless() assertion checks
in both affected files. In tests/test_functional.c (lines 122-138), relocate the
state-change and unref calls from after the assertions to immediately after the
gst_message_unref(msg) and gst_object_unref(bus) calls and before the
fail_unless() checks. Apply the same reordering in tests/test_mock_smoke.c
(lines 75-92) to ensure cleanup completes before any longjmp from a failed
assertion can occur, preventing the streaming pipeline from remaining active and
causing resource leaks.

Comment thread tests/test_socket.c
Comment on lines +169 to +170
gboolean underA = (pathA != NULL) && g_str_has_prefix (pathA, runtime);
gboolean underB = (pathB != NULL) && g_str_has_prefix (pathB, runtime);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make runtime-dir containment check boundary-safe.

g_str_has_prefix(path, runtime) alone can false-pass for sibling prefixes (e.g., /tmp/run vs /tmp/run2). Add a separator/end boundary check.

🧪 Tighten assertion
-  gboolean underA = (pathA != NULL) && g_str_has_prefix (pathA, runtime);
-  gboolean underB = (pathB != NULL) && g_str_has_prefix (pathB, runtime);
+  gsize rlen = strlen (runtime);
+  gboolean underA = (pathA != NULL) && g_str_has_prefix (pathA, runtime)
+      && (pathA[rlen] == G_DIR_SEPARATOR || pathA[rlen] == '\0');
+  gboolean underB = (pathB != NULL) && g_str_has_prefix (pathB, runtime)
+      && (pathB[rlen] == G_DIR_SEPARATOR || pathB[rlen] == '\0');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gboolean underA = (pathA != NULL) && g_str_has_prefix (pathA, runtime);
gboolean underB = (pathB != NULL) && g_str_has_prefix (pathB, runtime);
gsize rlen = strlen (runtime);
gboolean underA = (pathA != NULL) && g_str_has_prefix (pathA, runtime)
&& (pathA[rlen] == G_DIR_SEPARATOR || pathA[rlen] == '\0');
gboolean underB = (pathB != NULL) && g_str_has_prefix (pathB, runtime)
&& (pathB[rlen] == G_DIR_SEPARATOR || pathB[rlen] == '\0');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_socket.c` around lines 169 - 170, The g_str_has_prefix checks in
the underA and underB variable assignments are vulnerable to false positives
with sibling directory names (e.g., /tmp/run matching /tmp/run2). For each
g_str_has_prefix call, add an additional boundary-safety check to ensure the
matched prefix is followed by a path separator (/) or is at the end of the
string. This prevents the function from incorrectly matching partial directory
names that happen to share a common prefix.

Comment thread tests/test_sps_bounds.c
Comment on lines +101 to +109
if (msg != NULL && GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
GError *gerr = NULL;
gchar *dbg = NULL;
gst_message_parse_error (msg, &gerr, &dbg);
fail ("pipeline errored instead of reaching EOS: %s (%s)",
gerr ? gerr->message : "(none)", dbg ? dbg : "(no debug)");
g_clear_error (&gerr);
g_free (dbg);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid failing before teardown in the error branch.

This branch calls fail() before shutting down/unreffing the pipeline and bus, which can skip cleanup and cause hanging/flaky ctest behavior under non-forking execution.

🛠️ Teardown-safe pattern
-  if (msg != NULL && GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
-    GError *gerr = NULL;
-    gchar *dbg = NULL;
-    gst_message_parse_error (msg, &gerr, &dbg);
-    fail ("pipeline errored instead of reaching EOS: %s (%s)",
-        gerr ? gerr->message : "(none)", dbg ? dbg : "(no debug)");
-    g_clear_error (&gerr);
-    g_free (dbg);
-  }
+  gboolean got_error = FALSE;
+  gchar *error_text = NULL;
+  if (msg != NULL && GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
+    GError *gerr = NULL;
+    gchar *dbg = NULL;
+    gst_message_parse_error (msg, &gerr, &dbg);
+    got_error = TRUE;
+    error_text = g_strdup_printf ("%s (%s)",
+        gerr ? gerr->message : "(none)", dbg ? dbg : "(no debug)");
+    g_clear_error (&gerr);
+    g_free (dbg);
+  }
@@
   gst_element_set_state (pipeline, GST_STATE_NULL);
   gst_object_unref (pipeline);
+
+  fail_unless (!got_error, "pipeline errored instead of reaching EOS: %s",
+      error_text ? error_text : "(unknown)");
+  g_free (error_text);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (msg != NULL && GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
GError *gerr = NULL;
gchar *dbg = NULL;
gst_message_parse_error (msg, &gerr, &dbg);
fail ("pipeline errored instead of reaching EOS: %s (%s)",
gerr ? gerr->message : "(none)", dbg ? dbg : "(no debug)");
g_clear_error (&gerr);
g_free (dbg);
}
gboolean got_error = FALSE;
gchar *error_text = NULL;
if (msg != NULL && GST_MESSAGE_TYPE (msg) == GST_MESSAGE_ERROR) {
GError *gerr = NULL;
gchar *dbg = NULL;
gst_message_parse_error (msg, &gerr, &dbg);
got_error = TRUE;
error_text = g_strdup_printf ("%s (%s)",
gerr ? gerr->message : "(none)", dbg ? dbg : "(no debug)");
g_clear_error (&gerr);
g_free (dbg);
}
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (pipeline);
fail_unless (!got_error, "pipeline errored instead of reaching EOS: %s",
error_text ? error_text : "(unknown)");
g_free (error_text);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sps_bounds.c` around lines 101 - 109, The error branch calls
fail() before cleaning up resources like the pipeline and bus, which skips
teardown and can cause hanging or flaky behavior. Restructure the error handling
in the GST_MESSAGE_ERROR branch (where gst_message_parse_error is called) to
ensure that any pipeline and bus unreffing/cleanup operations occur before the
fail() call, so that shutdown happens even when the test fails on error.

@datagutt datagutt merged commit 8c95c80 into main Jun 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants