Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 79 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ on:
pull_request:
branches: ["main"]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build (${{ matrix.os }})
Expand All @@ -20,14 +27,20 @@ jobs:
os: [ubuntu-22.04, ubuntu-24.04]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Initialize regression test results
run: |
result_dir="test-results/libuvc-regression-${{ matrix.os }}"
mkdir -p "$result_dir"
printf 'os=%s\nstatus=started\n' "${{ matrix.os }}" > "$result_dir/status.txt"

- name: Install dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y \
build-essential cmake pkg-config \
libusb-1.0-0-dev libjpeg-dev ccache
libusb-1.0-0-dev libjpeg-dev ccache jq

- name: Record compiler identity
id: compiler
Expand All @@ -39,7 +52,7 @@ jobs:
uses: actions/cache@v6
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.os }}-${{ steps.compiler.outputs.fingerprint }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'include/**', 'src/**', '.github/workflows/build.yml') }}
key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.os }}-${{ steps.compiler.outputs.fingerprint }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'include/**', 'src/**', 'tests/**', '.github/workflows/build.yml') }}
restore-keys: |
ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.os }}-${{ steps.compiler.outputs.fingerprint }}-

Expand All @@ -55,23 +68,78 @@ jobs:
-DBUILD_SHARED_LIBS=ON \
-DBUILD_EXAMPLE=OFF \
-DBUILD_TEST=OFF \
-DBUILD_TESTING=OFF \
-DLIBUVC_AUTO_DETACH_KERNEL_DRIVER=ON \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache

- name: Build
run: cmake --build build --parallel

- name: Verify capabilities present
- name: Configure regression tests
run: |
echo "=== H.265 support ==="
grep -r "UVC_FRAME_FORMAT_H265" include/ && echo "PASS: H265 enum present"
set -o pipefail
result_dir="test-results/libuvc-regression-${{ matrix.os }}"
cmake -S . -B "$result_dir/build" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_BUILD_TARGET=Static \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_EXAMPLE=OFF \
-DBUILD_TEST=OFF \
-DBUILD_TESTING=ON \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
2>&1 | tee "$result_dir/configure.log"

echo "=== UVC 1.5 header ==="
grep -r "0x0150" src/ && echo "PASS: UVC 1.5 case present"
- name: Build regression tests
run: |
set -o pipefail
result_dir="test-results/libuvc-regression-${{ matrix.os }}"
cmake --build "$result_dir/build" --parallel \
2>&1 | tee "$result_dir/build.log"

echo "=== Configurable auto-detach ==="
grep "LIBUVC_AUTO_DETACH_KERNEL_DRIVER" CMakeLists.txt && echo "PASS: auto-detach option present"
- name: Verify exact regression test inventory
run: |
set -o pipefail
result_dir="test-results/libuvc-regression-${{ matrix.os }}"
ctest --test-dir "$result_dir/build" --show-only=json-v1 \
> "$result_dir/inventory.json"
jq -e '
([.tests[].name] | sort) == ([
"libuvc.descriptor.degenerate_h26x",
"libuvc.descriptor.h264",
"libuvc.descriptor.h265",
"libuvc.descriptor.truncated_format",
"libuvc.descriptor.truncated_frame",
"libuvc.negotiation.h264",
"libuvc.negotiation.h265",
"libuvc.negotiation.near_match",
"libuvc.negotiation.probe_get_error",
"libuvc.negotiation.probe_set_error",
"libuvc.transfer.retry_failure",
"libuvc.transfer.retry_success",
"libuvc.transfer.terminal_statuses"
] | sort) and (.tests | length == 13)
' "$result_dir/inventory.json" \
2>&1 | tee "$result_dir/inventory-check.log"

- name: Run regression tests
run: |
set -o pipefail
result_dir="$GITHUB_WORKSPACE/test-results/libuvc-regression-${{ matrix.os }}"
ctest --test-dir "$result_dir/build" \
--output-on-failure \
--output-junit "$result_dir/junit.xml" \
2>&1 | tee "$result_dir/ctest.log"

- name: Upload regression test results
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: libuvc-regression-${{ matrix.os }}
path: test-results/libuvc-regression-${{ matrix.os }}/
if-no-files-found: error

- name: Configure + build (auto-detach OFF — rollback path)
run: |
Expand All @@ -80,6 +148,7 @@ jobs:
-DBUILD_SHARED_LIBS=ON \
-DBUILD_EXAMPLE=OFF \
-DBUILD_TEST=OFF \
-DBUILD_TESTING=OFF \
-DLIBUVC_AUTO_DETACH_KERNEL_DRIVER=OFF \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Build directory (recommended location)
build/
test-results/
6 changes: 4 additions & 2 deletions CHANGELOG.ceralive.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,10 @@ fork-style-adapted commit on `hardening/v0.0.7.3` (branched from `eae7f49`, tag
`ceralive-v0.0.7.2`). Every change is either off-by-default or a pure robustness
guard — no negotiated default changes and byte-identical streaming behavior for
currently-working devices. Each entry cites its **fork-commit SHA** and upstream
provenance. Backlog IDs (A1–A14) refer to the equivalence audit in the plugin
repo's `.omo/evidence/task-1-uvc-camera-compat-stability.md`.
provenance. Backlog IDs (A1–A14) and their durable evidence dispositions are
recorded in `docs/evidence/uvc-camera-compat-stability.md`. The focused
hardware-independent assertions live in `tests/` and are enforced by the
blocking regression gate in `.github/workflows/build.yml`.

### Added

Expand Down
46 changes: 46 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ endif()

option(BUILD_EXAMPLE "Build example program" ON)
option(BUILD_TEST "Build test program" OFF)
option(BUILD_TESTING "Build hardware-independent regression tests" OFF)
include(CTest)
option(ENABLE_UVC_DEBUGGING "Enable UVC debugging" OFF)
option(LIBUVC_AUTO_DETACH_KERNEL_DRIVER
"Auto-detach the kernel driver (e.g. uvcvideo) when claiming UVC interfaces (CeraLive)"
Expand Down Expand Up @@ -146,6 +148,50 @@ foreach(target_name IN LISTS UVC_TARGETS)
endif()
endforeach()

if(BUILD_TESTING)
if(NOT TARGET uvc_static)
message(FATAL_ERROR
"BUILD_TESTING requires CMAKE_BUILD_TARGET=Static or Both")
endif()
if(NOT UNIX OR APPLE OR NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
message(FATAL_ERROR
"The libuvc regression tests require Linux and GNU-compatible --wrap")
endif()

add_executable(uvc_descriptor_assertions tests/descriptor_assertions.c)
add_executable(uvc_negotiation_assertions tests/negotiation_assertions.c)
add_executable(uvc_transfer_assertions tests/transfer_assertions.c)

foreach(test_target
uvc_descriptor_assertions
uvc_negotiation_assertions
uvc_transfer_assertions)
target_link_libraries(${test_target}
PRIVATE uvc_static LibUSB::LibUSB ${threads})
if(JPEG_FOUND)
target_link_libraries(${test_target} PRIVATE JPEG::JPEG)
endif()
endforeach()

set_property(TARGET uvc_negotiation_assertions APPEND_STRING
PROPERTY LINK_FLAGS " -Wl,--wrap=libusb_control_transfer -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/02-link.map")
set_property(TARGET uvc_transfer_assertions APPEND_STRING
PROPERTY LINK_FLAGS " -Wl,--wrap=libusb_submit_transfer -Wl,--wrap=libusb_free_transfer -Wl,--wrap=free")

foreach(case_name h264 h265 truncated_format truncated_frame degenerate_h26x)
add_test(NAME libuvc.descriptor.${case_name}
COMMAND uvc_descriptor_assertions --case ${case_name})
endforeach()
foreach(case_name h264 h265 near_match probe_set_error probe_get_error)
add_test(NAME libuvc.negotiation.${case_name}
COMMAND uvc_negotiation_assertions --case ${case_name})
endforeach()
foreach(case_name terminal_statuses retry_success retry_failure)
add_test(NAME libuvc.transfer.${case_name}
COMMAND uvc_transfer_assertions --case ${case_name})
endforeach()
endif()

if(BUILD_EXAMPLE)
add_executable(example src/example.c)
find_package(Threads)
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Prerequisites: You will need `libusb` and [CMake](http://www.cmake.org/) install

To build, you can just run these shell commands:

git clone https://github.com/libuvc/libuvc
git clone https://github.com/CERALIVE/libuvc
cd libuvc
mkdir build
cd build
Expand All @@ -49,8 +49,30 @@ To build, you can just run these shell commands:
and you're set! If you want to change the build configuration, you can edit `CMakeCache.txt`
in the build directory, or use a CMake GUI to make the desired changes.

There is also `BUILD_EXAMPLE` and `BUILD_TEST` options to enable the compilation of `example` and `uvc_test` programs. To use them, replace the `cmake ..` command above with `cmake .. -DBUILD_TEST=ON -DBUILD_EXAMPLE=ON`.
Then you can start them with `./example` and `./uvc_test` respectively. Note that you need OpenCV to build the later (for displaying image).
`BUILD_EXAMPLE` enables the example program. `BUILD_TEST` enables `uvc_test`, an
interactive OpenCV demo that needs a real UVC camera and a display; it is not the
automated test suite. `BUILD_TESTING` enables the default-off, hardware-independent
Linux CTest suite. Configure, build, inspect, and run its static build with:

cmake -S . -B build/regression \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_BUILD_TARGET=Static \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_EXAMPLE=OFF \
-DBUILD_TEST=OFF \
-DBUILD_TESTING=ON
cmake --build build/regression --parallel
ctest --test-dir build/regression --show-only=json-v1 \
| jq -e '.tests | length == 13'
ctest --test-dir build/regression --output-on-failure

The 13 cases are grouped as descriptor (5: `h264`, `h265`,
`truncated_format`, `truncated_frame`, `degenerate_h26x`), negotiation (5:
`h264`, `h265`, `near_match`, `probe_set_error`, `probe_get_error`), and
transfer (3: `terminal_statuses`, `retry_success`, `retry_failure`). CI runs
this suite without camera hardware on Ubuntu 22.04 and Ubuntu 24.04. See
`docs/evidence/uvc-camera-compat-stability.md` for its exact scope.

## Developing with libuvc

Expand Down
84 changes: 84 additions & 0 deletions docs/evidence/uvc-camera-compat-stability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# UVC camera compatibility and stability evidence

This document keeps the A1–A14 audit disposition reproducible in a standalone
clone. It distinguishes the focused CTest gate from historical sanitizer work
and manual camera/plugin validation. No camera or other USB hardware was used
to create or run the 13-case suite; it does not reproduce physical-camera
behavior, USB timing, or quarantine/thread races.

## Focused regression gate

The production code is in `src/device.c` and `src/stream.c`. CMake registers
three fixtures from `tests/`, and `.github/workflows/build.yml` blocks Ubuntu
22.04 and Ubuntu 24.04 jobs on the exact 13-test inventory and its result:

- `tests/descriptor_assertions.c`: `libuvc.descriptor.h264`,
`libuvc.descriptor.h265`, `libuvc.descriptor.truncated_format`,
`libuvc.descriptor.truncated_frame`, and
`libuvc.descriptor.degenerate_h26x`.
- `tests/negotiation_assertions.c`: `libuvc.negotiation.h264`,
`libuvc.negotiation.h265`, `libuvc.negotiation.near_match`,
`libuvc.negotiation.probe_set_error`, and
`libuvc.negotiation.probe_get_error`.
- `tests/transfer_assertions.c`: `libuvc.transfer.terminal_statuses`,
`libuvc.transfer.retry_success`, and `libuvc.transfer.retry_failure`.

Only A4 directly overlaps this focused suite: `degenerate_h26x` asserts the
zero buffer-size and invalid default-interval repair introduced by `5df5401`.
The codec, malformed-descriptor, negotiation-error, and transfer-lifecycle
cases protect other production behavior; they must not be read as automated
coverage of the other A-items.

Before the corresponding production guards were applied, retained red runs
showed `truncated_format` and `truncated_frame` failing because malformed
descriptor lengths were accepted, and `probe_set_error` and `probe_get_error`
failing because control-transfer errors were discarded. The fixes are the
length/order checks in `src/device.c` and error propagation in `src/stream.c`.
With those fixes, all 13 cases pass. This records a red/green disposition, not
a claim that those four defects belong to A1–A14.

On Linux with CMake, a GNU-compatible C compiler, libusb development headers,
and pthreads, reproduce the green result exactly with:

```sh
cmake -S . -B build/regression \
-DCMAKE_POLICY_VERSION_MINIMUM=3.5 \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_BUILD_TARGET=Static \
-DBUILD_SHARED_LIBS=OFF \
-DBUILD_EXAMPLE=OFF \
-DBUILD_TEST=OFF \
-DBUILD_TESTING=ON
cmake --build build/regression --parallel
ctest --test-dir build/regression --show-only=json-v1 \
| jq -e '.tests | length == 13'
ctest --test-dir build/regression --output-on-failure
```

Expected green outcome: exactly 13 discovered tests and 13 passes. A red run
returns non-zero and names the failed assertion. Capture local tool versions
with `cmake --version; ctest --version; cc --version; pkg-config --modversion
libusb-1.0`.

## A1–A14 dispositions

| Item | Disposition | Durable repository evidence and limits |
|---|---|---|
| A1 | not covered by this focused gate | Alt-setting retry is production code from `3195bbc` in `src/stream.c`; transient real-USB timing remains hardware scope. |
| A2 | not covered by this focused gate | Transfer-count API and zero-submit handling are in `001e8d3`, `include/libuvc/libuvc.h`, and `src/stream.c`; the 13 cases do not exercise stream startup allocation. |
| A3 | historical sanitizer evidence only | Metadata cleanup is in `3195bbc`; teardown leak validation was retained historical sanitizer scope, not this CTest suite. |
| A4 | repository CTest coverage | Descriptor repair is in `5df5401` and `src/device.c`; `libuvc.descriptor.degenerate_h26x` directly asserts both repairs without hardware. Actual DJI descriptors/capture remain hardware scope. |
| A5 | historical sanitizer evidence only | The bounded stop and later quarantine hardening are recorded by `ab49e21`, `dfba86f`, `a1949ae`, `3ede00e`, `2dcca59`, and `71588db`; the thread/timing harness is not part of this focused gate. |
| A6 | not covered by this focused gate | Existing control-interface routing uses parsed interface state in `src/ctrl.c`; no new backport or focused assertion was required. Composite hardware remains manual scope. |
| A7 | not covered by this focused gate | The zero `GET_MAX` payload fallback is in `69c7da8` and `src/stream.c`; the negotiation cases here do not assert it. |
| A8 | not covered by this focused gate | The corrupt-payload superset in A9 subsumed this item in `69c7da8`; packet payload processing is not exercised here. |
| A9 | not covered by this focused gate | Payload bounds/error handling is in `69c7da8` and `src/stream.c`; callback delivery with real packets remains outside this gate. |
| A10 | not covered by this focused gate | Descriptor robustness was already backported in `eae7f49` in `src/device.c`; these descriptor fixtures do not exercise device enumeration. |
| A11 | hardware/plugin validation only | Auto-detach configuration is in `2f32812`, `CMakeLists.txt`, and `src/device.c`; CI builds both option values, but kernel-driver detach behavior requires hardware/system validation. |
| A12 | not covered by this focused gate | Clock preservation is in `9874f4c` in `src/device.c` and `src/stream.c`; the value is not surfaced onto frames and is not asserted here. |
| A13 | historical sanitizer evidence only | Audit disposition in `9874f4c` found the libuvc refcount path already correct and the Android-only path absent; historical enumeration leak checking was not added to this suite. |
| A14 | hardware/plugin validation only | The opt-in double-probe quirk belongs to the consuming plugin, not this repository; its device table and camera validation remain plugin/manual scope. |

CI and local CTest use synthetic descriptors and wrapped libusb calls. Passing
them establishes deterministic parser, negotiation, and transfer-callback
behavior only. It does not establish end-to-end camera compatibility.
16 changes: 16 additions & 0 deletions src/device.c
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,11 @@ uvc_error_t uvc_parse_vs_frame_format(uvc_streaming_interface_t *stream_if,
size_t block_size) {
UVC_ENTER();

if (block_size < 28) {
UVC_EXIT(UVC_ERROR_INVALID_DEVICE);
return UVC_ERROR_INVALID_DEVICE;
}

uvc_format_desc_t *format = calloc(1, sizeof(*format));

format->parent = stream_if;
Expand Down Expand Up @@ -1568,6 +1573,17 @@ uvc_error_t uvc_parse_vs_frame_frame(uvc_streaming_interface_t *stream_if,

UVC_ENTER();

if (block_size < 26 || !stream_if->format_descs) {
UVC_EXIT(UVC_ERROR_INVALID_DEVICE);
return UVC_ERROR_INVALID_DEVICE;
}

if ((block[21] == 0 && block_size < 38) ||
(block[21] != 0 && block_size < 26 + 4 * (size_t) block[21])) {
UVC_EXIT(UVC_ERROR_INVALID_DEVICE);
return UVC_ERROR_INVALID_DEVICE;
}

format = stream_if->format_descs->prev;
frame = calloc(1, sizeof(*frame));

Expand Down
10 changes: 8 additions & 2 deletions src/stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,15 @@ uvc_error_t uvc_probe_stream_ctrl(
uvc_device_handle_t *devh,
uvc_stream_ctrl_t *ctrl) {
uvc_stream_ctrl_t required_ctrl = *ctrl;
uvc_error_t result;

uvc_query_stream_ctrl( devh, ctrl, 1, UVC_SET_CUR );
uvc_query_stream_ctrl( devh, ctrl, 1, UVC_GET_CUR );
result = uvc_query_stream_ctrl(devh, ctrl, 1, UVC_SET_CUR);
if (result != UVC_SUCCESS)
return result;

result = uvc_query_stream_ctrl(devh, ctrl, 1, UVC_GET_CUR);
if (result != UVC_SUCCESS)
return result;

if(!_uvc_stream_params_negotiated(&required_ctrl, ctrl)) {
UVC_DEBUG("Unable to negotiate streaming format");
Expand Down
Loading