From 0a320db4a7677151e76091396bc91fdfecffe562 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 29 Oct 2026 08:03:46 -0700 Subject: [PATCH 1/3] camera-check runs against a laptop's own USB camera with --hardware The probe read a frame with read(), which uvcvideo does not implement, and the check always loaded vivid first, so nothing could check a real camera. The probe now lists the node's formats and takes frames by mmap streaming, the path a browser takes, in both modes. A held probe reports DQBUF-FAILED. camera-check.sh USER --hardware runs at a console of the installed laptop. It never loads vivid, and it requires a video-class USB interface with uvcvideo not loaded and no grant since boot. It also checks what only uvcvideo shows: - a program keeping its descriptor past the revoke holds the driver, which stays loaded; - a grant meanwhile is refused; - the next grant captures again and unloads the driver at its end; - deauthorizing the camera's USB device ends the grant, and the camera coming back loads nothing. The QEMU run refuses to start when a USB camera is present, and keeps its check names. Its capture check now also asks for MJPG or YUYV among the formats, and that the granted node's own driver is the expected one. --- build/guest-tests/camera-check.sh | 165 ++++++++++++++++++++++++------ docs/design/camera.md | 20 +++- docs/hardware.md | 1 + 3 files changed, 153 insertions(+), 33 deletions(-) diff --git a/build/guest-tests/camera-check.sh b/build/guest-tests/camera-check.sh index 3b1969bd..ac5220a1 100755 --- a/build/guest-tests/camera-check.sh +++ b/build/guest-tests/camera-check.sh @@ -3,7 +3,13 @@ # guest by tools/image/zones-test.sh. QEMU has no camera, so vivid, which only this loads, stands # in for one. # -# camera-check.sh USER USER is the session's user, who grants as the chrome does +# camera-check.sh USER USER is the session's user, who grants as the chrome does +# camera-check.sh USER --hardware the machine's own USB camera instead, vivid never loaded +# +# --hardware runs as root at a console (Ctrl+Alt+F2) of the installed laptop, with USER logged in, +# the screen unlocked and nothing using the camera since boot, and within the ten minutes before +# the idle lock, which takes the camera back. It checks what QEMU cannot: uvcvideo's capture, a +# program that keeps the camera past its grant holding the driver, and the camera taken away. # # Output: "CT PASS|FAIL name - detail"; a check that cannot run fails. set -u @@ -13,6 +19,7 @@ KD=/usr/bin/kryptikd LOG=/var/log/kryptik/camera-check mkdir -p "$LOG" USER_NAME="${1:-tester}" +HW=0; [[ "${2:-}" == --hardware ]] && HW=1 pass() { echo "CT PASS $1${2:+ - $2}"; } fail() { echo "CT FAIL $1${2:+ - $2}"; } oneline() { tr '\n' ' ' | cut -c1-300; } @@ -34,10 +41,11 @@ wait_for() { # wait_for FILE REGEX SECONDS done return 1 } -# A program in a zone: opens /dev/video0, asks the driver's name, reads a frame. With an -# argument it keeps reading every half second until a read fails; with WAIT it first waits for -# the camera to be given. -PROBE='import fcntl, os, sys, time +# A program in a zone: opens /dev/video0, asks the driver's name and the formats it offers, and +# takes a frame by mmap streaming, as a browser does (uvcvideo has no read()). With HOLD it takes +# one every half second until that fails, and with KEEP then holds the descriptor two minutes +# more; with WAIT it first waits for the camera to be given. +PROBE='import fcntl, mmap, os, select, struct, sys, time wait = "wait" in sys.argv for _ in range(60 if wait else 1): try: @@ -52,17 +60,59 @@ else: cap = bytearray(104) fcntl.ioctl(fd, 0x80685600, cap) print("DRIVER", bytes(cap[:16]).rstrip(b"\0").decode(), flush=True) -print("FRAME", len(os.read(fd, 8 << 20)), flush=True) +fmts = [] +for i in range(64): + d = bytearray(64) + struct.pack_into("=II", d, 0, i, 1) + try: + fcntl.ioctl(fd, 0xc0405602, d) + except OSError: + break + fmts.append(bytes(d[44:48]).decode("ascii", "replace")) +print("FORMATS", " ".join(fmts), flush=True) +def buf(i=0): + b = bytearray(88) + struct.pack_into("=II", b, 0, i, 1) + struct.pack_into("=I", b, 60, 1) + return b +req = bytearray(20) +struct.pack_into("=III", req, 0, 4, 1, 1) +fcntl.ioctl(fd, 0xc0145608, req) +maps = [] +for i in range(struct.unpack_from("=I", req, 0)[0]): + b = buf(i) + fcntl.ioctl(fd, 0xc0585609, b) + off, size = struct.unpack_from("=I", b, 64)[0], struct.unpack_from("=I", b, 72)[0] + maps.append(mmap.mmap(fd, size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offset=off)) + fcntl.ioctl(fd, 0xc058560f, b) +fcntl.ioctl(fd, 0x40045612, struct.pack("=i", 1)) +def frame(secs): + end = time.monotonic() + secs + while time.monotonic() < end: + if not select.select([fd], [], [], max(0.0, end - time.monotonic()))[0]: + break + b = buf() + fcntl.ioctl(fd, 0xc0585611, b) + used, flags = struct.unpack_from("=II", b, 8) + fcntl.ioctl(fd, 0xc058560f, b) + if used and not flags & 0x40: + return used + return 0 +print("FRAME", frame(5), flush=True) if "hold" in sys.argv: print("HELD", flush=True) for _ in range(240): time.sleep(0.5) try: - os.read(fd, 8 << 20) + frame(0.5) except OSError as e: - print("READ-FAILED", e.errno, flush=True) + print("DQBUF-FAILED", e.errno, flush=True) + if "keep" in sys.argv: + time.sleep(120) sys.exit(0) - print("STILL-READS", flush=True)' + print("STILL-STREAMS", flush=True)' +# A video-class USB interface: a camera of the machine's own. +usb_camera() { grep -lsx 0e /sys/bus/usb/devices/*/bInterfaceClass | head -n 1; } [[ "$(id -u)" = 0 ]] || { fail "root" "this must run as root"; exit 1; } untrusted_uid="$(sed -n 's/^uid_base *= *\([0-9]*\).*/\1/p' "$Z/untrusted.toml")" node=/run/kryptik-camera/untrusted/video0 @@ -78,7 +128,21 @@ else fail "camera-not-by-alias" "uvcvideo's video-class aliases: ${claims}; loading by alias: $(oneline <<<"$kept"); loaded: $(loaded uvcvideo && echo yes || echo no)" fi -modprobe vivid n_devs=1 node_types=0x1 2> "$LOG/vivid.err" || { fail "camera-stand-in" "$(oneline < "$LOG/vivid.err")"; exit 0; } +# The camera: the machine's own, met before any grant this boot, or vivid where there is none. +usb="$(usb_camera)" +if [[ "$HW" -eq 1 ]]; then + if [[ -n "$usb" ]] && ! loaded uvcvideo && [[ "$(holder)" == none ]]; then + pass "camera-hardware" "the USB camera at ${usb%/*}, untouched since boot" + else + fail "camera-hardware" "USB camera: ${usb:-none}; uvcvideo $(loaded uvcvideo && echo loaded || echo absent); $(holder)" + exit 0 + fi + DRV=uvcvideo +else + [[ -z "$usb" ]] || { fail "camera-stand-in" "a USB camera is here (${usb%/*}): run with --hardware"; exit 0; } + modprobe vivid n_devs=1 node_types=0x1 2> "$LOG/vivid.err" || { fail "camera-stand-in" "$(oneline < "$LOG/vivid.err")"; exit 0; } + DRV=vivid +fi zrun untrusted -- python3 -c "$PROBE" if grep -q '^OPEN-FAILED 2$' <<<"$ZOUT"; then pass "camera-not-granted" "untrusted finds no camera before a grant"; else fail "camera-not-granted" "rc ${ZRC}: $(oneline <<<"$ZOUT")"; fi @@ -91,40 +155,60 @@ for z in vault net; do fi done -# Granted: the driver loads, and the node, the zone's root's alone, is in the zone's directory. +# Granted: the driver loads, and the node, the zone's root's alone, is in the zone's directory, +# for the camera's own device. said="$(as_user "kryptik-launch --camera-grant untrusted" 2>&1)"; rc=$? st="$(stat -c '%F %u %a' "$node" 2>&1)" -if [[ "$rc" -eq 0 ]] && loaded uvcvideo && [[ "$st" == "character special file ${untrusted_uid} 600" && "$(holder)" == "zone untrusted holds "* ]]; then - pass "camera-granted" "${said}; ${st}" +hn="$(holder | sed -n 's/.*(\(video[0-9]*\))$/\1/p')" +hd="$(basename "$(readlink -f "/sys/class/video4linux/${hn:-none}/device/driver")")" +if [[ "$rc" -eq 0 ]] && loaded uvcvideo && [[ "$st" == "character special file ${untrusted_uid} 600" && "$(holder)" == "zone untrusted holds "* && "$hd" == "$DRV" ]]; then + pass "camera-granted" "${said}; ${st}; ${hn}, driven by ${hd}" else - fail "camera-granted" "rc ${rc}: ${said}; node: ${st}; uvcvideo $(loaded uvcvideo && echo loaded || echo absent); $(holder)" + fail "camera-granted" "rc ${rc}: ${said}; node: ${st}; uvcvideo $(loaded uvcvideo && echo loaded || echo absent); $(holder); ${hn:-no node}'s driver ${hd}" fi +# A picture a browser shows: MJPG or YUYV among the formats, not only an infrared camera's grey. zrun untrusted -- python3 -c "$PROBE" -if grep -q '^DRIVER vivid$' <<<"$ZOUT" && grep -qE '^FRAME [1-9][0-9]*$' <<<"$ZOUT"; then pass "camera-zone-captures" "$(oneline <<<"$ZOUT")"; else fail "camera-zone-captures" "rc ${ZRC}: $(oneline <<<"$ZOUT")"; fi +if grep -q "^DRIVER ${DRV}$" <<<"$ZOUT" && grep -qE '^FORMATS (.* )?(MJPG|YUYV)( |$)' <<<"$ZOUT" && grep -qE '^FRAME [1-9][0-9]*$' <<<"$ZOUT"; then pass "camera-zone-captures" "$(oneline <<<"$ZOUT")"; else fail "camera-zone-captures" "rc ${ZRC}: $(oneline <<<"$ZOUT")"; fi if [[ "$(holder)" == none* && ! -e "$node" ]]; then pass "camera-ends-with-zone" "untrusted stopped, so its grant ended"; else fail "camera-ends-with-zone" "$(holder); node $(ls -l "$node" 2>&1)"; fi -# Another zone finds none while untrusted holds it. +# Another zone finds none while untrusted holds it. personal's passphrase is a file only the +# QEMU run's earlier checks make. as_user "kryptik-launch --camera-grant untrusted" > /dev/null 2>&1 -zrun personal --passphrase-file /root/zt/personal.pass -- python3 -c "$PROBE" -if grep -q '^OPEN-FAILED 2$' <<<"$ZOUT"; then pass "camera-other-zone-none" "personal finds no camera"; else fail "camera-other-zone-none" "rc ${ZRC}: $(oneline <<<"$ZOUT")"; fi +if [[ "$HW" -eq 0 ]]; then + zrun personal --passphrase-file /root/zt/personal.pass -- python3 -c "$PROBE" + if grep -q '^OPEN-FAILED 2$' <<<"$ZOUT"; then pass "camera-other-zone-none" "personal finds no camera"; else fail "camera-other-zone-none" "rc ${ZRC}: $(oneline <<<"$ZOUT")"; fi +fi # k alone: the descriptor a program kept fails, the node goes, and the driver lets go of the -# stand-in and unloads. -"$KD" run untrusted --zones "$Z" --rootfs "$R" -- python3 -c "$PROBE" hold > "$LOG/holder.out" 2>&1 & +# camera. vivid's node pins vivid, so uvcvideo unloads; a USB camera's pins uvcvideo, which stays +# loaded, and no grant is given until that program lets go, as the camera would come back with +# no capture node. +hold=(hold); [[ "$HW" -eq 0 ]] || hold+=(keep) +"$KD" run untrusted --zones "$Z" --rootfs "$R" -- python3 -c "$PROBE" "${hold[@]}" > "$LOG/holder.out" 2>&1 & hp=$! if wait_for "$LOG/holder.out" '^HELD' 60; then said="$(as_user "kryptik-launch --camera-revoke" 2>&1)"; rc=$? - if [[ "$rc" -eq 0 && "$said" == untrusted* && ! -e "$node" ]] && ! loaded uvcvideo && [[ ! -e /sys/bus/platform/drivers/vivid/vivid.0 ]]; then + if [[ "$HW" -eq 0 && "$rc" -eq 0 && "$said" == untrusted* && ! -e "$node" ]] && ! loaded uvcvideo && [[ ! -e /sys/bus/platform/drivers/vivid/vivid.0 ]]; then pass "camera-revoked" "${said}: the node is gone, vivid.0 let go of and uvcvideo unloaded" + elif [[ "$HW" -eq 1 && "$rc" -eq 0 && "$said" == "untrusted stayed" && ! -e "$node" ]] && loaded uvcvideo && [[ "$(holder)" == "none; its driver stayed loaded" ]]; then + pass "camera-revoked" "${said}: the node is gone, and uvcvideo stays, held by the program" else - fail "camera-revoked" "rc ${rc}: ${said}; node $(ls -l "$node" 2>&1); uvcvideo $(loaded uvcvideo && echo loaded || echo absent)" + fail "camera-revoked" "rc ${rc}: ${said}; node $(ls -l "$node" 2>&1); uvcvideo $(loaded uvcvideo && echo loaded || echo absent); $(holder)" + fi + if wait_for "$LOG/holder.out" '^DQBUF-FAILED [0-9]+$' 30; then pass "camera-revoked-kept-descriptor" "$(grep '^DQBUF-FAILED' "$LOG/holder.out")"; else fail "camera-revoked-kept-descriptor" "$(oneline < "$LOG/holder.out")"; fi + if [[ "$HW" -eq 1 ]]; then + said="$(as_user "kryptik-launch --camera-grant untrusted" 2>&1)"; rc=$? + if [[ "$rc" -ne 0 && "$said" == *"still holds the camera"* && ! -e "$node" ]]; then + pass "camera-kept-refuses-grant" "$said" + else + fail "camera-kept-refuses-grant" "rc ${rc}: ${said}; $(holder)" + fi fi - if wait_for "$LOG/holder.out" '^READ-FAILED [0-9]+$' 30; then pass "camera-revoked-kept-descriptor" "$(grep '^READ-FAILED' "$LOG/holder.out")"; else fail "camera-revoked-kept-descriptor" "$(oneline < "$LOG/holder.out")"; fi else fail "camera-revoked" "the program never held the camera: $(oneline < "$LOG/holder.out")" fail "camera-revoked-kept-descriptor" "not run" fi -for _ in $(seq 1 60); do kill -0 "$hp" 2>/dev/null || break; sleep 1; done +[[ "$HW" -eq 1 ]] || for _ in $(seq 1 60); do kill -0 "$hp" 2>/dev/null || break; sleep 1; done kill "$hp" 2>/dev/null; wait "$hp" 2>/dev/null # Granted again, to a zone already running, as to a browser waiting for it: the stand-in is @@ -140,24 +224,47 @@ else fi for _ in $(seq 1 60); do kill -0 "$wp" 2>/dev/null || break; sleep 1; done kill "$wp" 2>/dev/null; wait "$wp" 2>/dev/null -if [[ "$(holder)" == none* ]]; then pass "camera-ends-with-zone-again" "$(holder)"; else fail "camera-ends-with-zone-again" "$(holder)"; fi +# With nothing holding it this time, the driver unloads, and the mark that it stayed is gone. +h="$(holder)" +if { [[ "$HW" -eq 0 && "$h" == none* ]] || [[ "$HW" -eq 1 && "$h" == none ]]; } && ! loaded uvcvideo; then + pass "camera-ends-with-zone-again" "$(holder)" +else + fail "camera-ends-with-zone-again" "$(holder); uvcvideo $(loaded uvcvideo && echo loaded || echo absent)" +fi # Taken away while granted, as a USB camera pulled out: udev's removal of its node ends the grant -# at once, before another camera plugged in could take the node's number. +# at once, before another camera plugged in could take the node's number. A built-in camera is +# taken away by deauthorizing its USB device; authorized again, it comes back loading nothing. said="$(as_user "kryptik-launch --camera-grant untrusted" 2>&1)"; rc=$? ended() { [[ "$(holder)" == none* && ! -e "$node" ]]; } +# The granted node's USB device: its interface's parent. +hn="$(holder | sed -n 's/.*(\(video[0-9]*\))$/\1/p')" +dev="$(readlink -f "/sys/class/video4linux/${hn:-none}/device/..")" if [[ "$rc" -eq 0 && -e "$node" ]]; then - modprobe -r vivid > "$LOG/vivid-gone.out" 2>&1 + if [[ "$HW" -eq 1 ]]; then echo 0 > "$dev/authorized"; else modprobe -r vivid; fi > "$LOG/gone.out" 2>&1 ok=no for _ in $(seq 1 15); do ended && { ok=yes; break; }; sleep 1; done + [[ "$HW" -eq 0 ]] || echo 1 > "$dev/authorized" if [[ "$ok" == yes ]]; then - pass "camera-gone-ends-grant" "vivid taken away while untrusted held it: the grant ended and the node went" + pass "camera-gone-ends-grant" "the camera taken away while untrusted held it: the grant ended and the node went" else - fail "camera-gone-ends-grant" "status: $(holder); node $(ls -l "$node" 2>&1); $(oneline < "$LOG/vivid-gone.out")" + fail "camera-gone-ends-grant" "status: $(holder); node $(ls -l "$node" 2>&1); $(oneline < "$LOG/gone.out")" fi else fail "camera-gone-ends-grant" "the grant: rc ${rc}: ${said}" fi +if [[ "$HW" -eq 1 ]]; then + for _ in $(seq 1 10); do [[ -n "$(usb_camera)" ]] && break; sleep 1; done + sleep 3 + if [[ -n "$(usb_camera)" ]] && ! loaded uvcvideo && [[ "$(holder)" == none* ]]; then + pass "camera-back-loads-nothing" "the camera is back at ${dev}, and uvcvideo is not loaded" + else + fail "camera-back-loads-nothing" "USB camera: $(usb_camera); uvcvideo $(loaded uvcvideo && echo loaded || echo absent); $(holder)" + fi + modprobe -r uvcvideo 2>/dev/null + echo "CT END" + exit 0 +fi # Of two cameras, the first that offers a picture a browser reads: vivid.0's node offers only # multi-planar formats, which Firefox does not read, so the grant passes it for vivid.1's, as it diff --git a/docs/design/camera.md b/docs/design/camera.md index efea27ae..9ed1616f 100644 --- a/docs/design/camera.md +++ b/docs/design/camera.md @@ -101,14 +101,16 @@ loaded only for a grant. QEMU has no USB camera. The kernel's `vivid`, a test driver that makes V4L2 devices with no hardware (as `test_power` makes a battery), stands in, built -as a module that the zones and desktop suites alone load; `uvcvideo` -itself waits for the tests on physical hardware. +as a module that the zones and desktop suites alone load. The probe takes +its frames by mmap streaming, as a browser does, since `uvcvideo` has no +`read()`. - with no grant, no zone's `/dev/video0` opens, and loading by a camera's alias, as eudev does when one is plugged in, loads nothing (`modprobe -b` with a video-class alias); -- granted to `personal`, a probe there captures a frame from the stand-in; - `work` finds no camera; the chrome's window names `personal`; +- granted to `personal`, a probe there captures a frame from the stand-in, + which offers MJPEG or YUYV; `work` finds no camera; the chrome's window + names `personal`; - taken away while granted (`vivid` unloaded), the grant ends and the node goes; - of two stand-ins, the grant passes over the lower numbered, whose node @@ -118,6 +120,16 @@ itself waits for the tests on physical hardware. descriptor fails at its next read, and the node is gone; - a zone whose file refuses the camera is never offered it. +On a laptop, `camera-check.sh USER --hardware` +(`/usr/lib/kryptik/guest-tests`), run as root at a console with the user +logged in and nothing using the camera since boot, checks the same against +the laptop's own USB camera, with `vivid` never loaded, and what only +`uvcvideo` shows: a program that keeps its descriptor past `k` holds the +driver, which stays loaded, and no grant is given until it lets go; the +next grant after that captures again and unloads the driver at its end; and +the camera taken away (its USB device deauthorized) ends the grant, and +loads nothing when it comes back. + ## Not covered - MIPI cameras behind Intel's IPU6 and AMD's image processor (below), and diff --git a/docs/hardware.md b/docs/hardware.md index dbba3d8f..36db6540 100644 --- a/docs/hardware.md +++ b/docs/hardware.md @@ -106,6 +106,7 @@ let the row's note name what did not work: | closed on its dock, then the dock unplugged | it sleeps within seconds, as a lid closed undocked does | | the state passphrase at boot, typed on the built-in keyboard | it unlocks, whether the keyboard is on PS/2 or on I2C behind Intel's LPSS or AMD's DesignWare controller; the row's note names which (`/proc/bus/input/devices`). One that types nothing there is among those status.md lists, and a plain USB keyboard is the way through | | the listing's `camera usb` line for the built-in camera, taken before any `k` | it reads `port fixed control auto runtime suspended`; a port that reads `unknown` keeps the camera awake, and the row's note gives the line | +| at a console (Ctrl+Alt+F2) as root, before any `k`: `bash /usr/lib/kryptik/guest-tests/camera-check.sh USER --hardware`, USER the one logged in | every line reads `CT PASS`; a `CT FAIL` line goes in the row's note whole | | `k` and a zone's number in the menu, then a program there that records video | a USB camera records; one the listing's `camera` line puts behind an IPU does not, as 1.0 drives none | | a USB camera given to a zone with `k` and its number, then pulled out | the grant ends at once, and the menu's Camera line says no zone has it; plugged back in, it waits for `k` again | | Firefox in `untrusted`: Alt+p, its number and `b` | a window opens; where the listing's `shstk` line says yes, this is the only test of Firefox starting with shadow stacks on, and a failure names SHSTK in what it prints | From dc05c6f15729efa7cb43cbe74f5e97c6ba57e232 Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 29 Oct 2026 08:08:39 -0700 Subject: [PATCH 2/3] Firefox in a zone finds a camera without inotify, and stops on one taken away Zones refuse inotify, since a watch on the /usr they share with zone 0 would see every program started. WebRTC's V4L2 device code learned of cameras only by inotify, so a camera granted after a page loaded was never listed, and pages got no devicechange. A fifth Firefox patch makes its inotify thread fall back, when inotify_init fails, to looking for /dev/video once a second. It reports a change when the set differs, and the zone's dangling /dev/video0 link reads as absent. The thread still stops within 100 ms of shutdown. Its capture thread polled a camera that was taken away in a busy loop: the node answers POLLERR or POLLHUP and never POLLIN. The thread now ends there. STREAMON precedes the thread and StopCapture joins it first, so a POLLERR while it runs is the device or its queue failing. The user guide's camera notes follow. --- ...-v4l2-camera-changes-without-inotify.patch | 48 +++++++++++++++++++ build/patches/firefox-153.6.0/README.md | 14 +++++- build/patches/firefox-153.6.0/SHA256SUMS | 1 + docs/user-guide.md | 16 ++++--- 4 files changed, 71 insertions(+), 8 deletions(-) create mode 100644 build/patches/firefox-153.6.0/0005-v4l2-camera-changes-without-inotify.patch diff --git a/build/patches/firefox-153.6.0/0005-v4l2-camera-changes-without-inotify.patch b/build/patches/firefox-153.6.0/0005-v4l2-camera-changes-without-inotify.patch new file mode 100644 index 00000000..f2fcce67 --- /dev/null +++ b/build/patches/firefox-153.6.0/0005-v4l2-camera-changes-without-inotify.patch @@ -0,0 +1,48 @@ +--- a/third_party/libwebrtc/modules/video_capture/linux/device_info_v4l2.cc ++++ b/third_party/libwebrtc/modules/video_capture/linux/device_info_v4l2.cc +@@ -170,6 +170,33 @@ + + close(_fd_v4l); + close(_fd_dev); ++ } else { ++ // No inotify, as in a sandbox that refuses it: look for /dev/video once a second ++ // instead, and say the devices changed whenever the set of them does. ++ if (_fd_v4l >= 0) { ++ close(_fd_v4l); ++ } ++ if (_fd_dev >= 0) { ++ close(_fd_dev); ++ } ++ uint64_t seen = 0; ++ for (int tick = 0; !_isShutdown; tick++) { ++ if (tick % 10 == 0) { ++ uint64_t now = 0; ++ for (int n = 0; n < 64; n++) { ++ char path[32]; ++ snprintf(path, sizeof(path), "/dev/video%d", n); ++ if (access(path, F_OK) == 0) { ++ now |= uint64_t(1) << n; ++ } ++ } ++ if (tick > 0 && now != seen) { ++ DeviceChange(); ++ } ++ seen = now; ++ } ++ usleep(100 * 1000); ++ } + } + } + #endif +--- a/third_party/libwebrtc/modules/video_capture/linux/video_capture_v4l2.cc ++++ b/third_party/libwebrtc/modules/video_capture/linux/video_capture_v4l2.cc +@@ -451,6 +451,9 @@ + } else if (retVal == 0) { + // poll timed out + return true; ++ } else if (rSet.revents & (POLLERR | POLLHUP | POLLNVAL)) { ++ // the device is gone, or its queue failed: polling again would spin ++ return false; + } else if (!(rSet.revents & POLLIN)) { + // not event on camera handle + return true; diff --git a/build/patches/firefox-153.6.0/README.md b/build/patches/firefox-153.6.0/README.md index 900d4361..d068a6d0 100644 --- a/build/patches/firefox-153.6.0/README.md +++ b/build/patches/firefox-153.6.0/README.md @@ -5,7 +5,8 @@ Applied by `s_browser_firefox` in the browser image through `apply_repo_patches` Mozilla builds Firefox with clang; Kryptik builds it with stage 04's GCC 14, as every other program is built. The first three are the changes Gentoo's Firefox -153 patch set carries for a GCC build, each a few lines; the fourth is Kryptik's: +153 patch set carries for a GCC build, each a few lines; the fourth and fifth +are Kryptik's: - `0001`: `features.h` leaves the list of system headers Firefox wraps. Wrapped, glibc's `_FORTIFY_SOURCE` checks do not take effect, which stage 06's audit @@ -21,6 +22,17 @@ every other program is built. The first three are the changes Gentoo's Firefox build with neither gets no remote at all, as on a platform without one: a second `firefox` with the same profile is not handed to the first, and says that Firefox is already running. +- `0005`: the camera in a zone, which refuses inotify (a watch on the `/usr` + it shares with zone 0 would see every program started). WebRTC's V4L2 code + learned of cameras only by inotify, so a camera granted after a page loaded + was never listed and pages got no `devicechange`. Without inotify it now + looks for `/dev/video` once a second and reports a change, and the + zone's `/dev/video0` link, dangling with no grant, reads as absent. Its + capture thread also polled a camera that was taken away in a busy loop, + since the node answers `POLLERR` and never `POLLIN`; it now stops there. + Told the camera is gone, Firefox can end the track and close the node, + which the next grant waits for (`camera.rs`); whether it does is for the + tests on physical hardware. The flag stage 04 adds that this build cannot take, `-fno-delete-null-pointer-checks`, is dropped for Firefox alone in `build/config/hardening-exceptions.txt`. diff --git a/build/patches/firefox-153.6.0/SHA256SUMS b/build/patches/firefox-153.6.0/SHA256SUMS index 915dce98..e863c245 100644 --- a/build/patches/firefox-153.6.0/SHA256SUMS +++ b/build/patches/firefox-153.6.0/SHA256SUMS @@ -2,3 +2,4 @@ b9bc94714ef801b3450dddfba1a35a7139ed48c9a958378dbda9016a4403ee14 0002-ffvpx-gcc-incompatible-pointer-types.patch 692345659a387cda546bf9aa2a8b19cefce60f3545634607e6a35d526145fd90 0003-cstdint-includes.patch d68bb973e17677582d8988053518549d8261fe20934b732c12a4138bf9ec8dfb 0004-remote-needs-dbus-or-x11.patch +a844c745485cbe462bfeab8dbce837594a8f883ee4de5164ed1e5a872aca52db 0005-v4l2-camera-changes-without-inotify.patch diff --git a/docs/user-guide.md b/docs/user-guide.md index 3aad200b..63122da5 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -471,14 +471,16 @@ processing in userspace (MIPI cameras behind the IPU6), those behind the image processor of AMD's Ryzen AI Max (HP's ZBook Ultra G1a), and infrared face-login cameras do not. `vault` and the net zone never get the camera. -Firefox in a zone is not told when the camera is given or taken back. -- Give the camera before opening the call. A page that looked for a camera - before goes on hearing there is none, even after a reload. Turning the - camera on in the call finds it, and so does starting Firefox again. +Firefox in a zone is not told when the camera is given or taken back, so it +looks once a second. +- Give the camera before opening the call where you can. A page that looked + for a camera before hears of it within a second if it listens for that; + otherwise turning the camera on in the call finds it, and so does a + reload. - Turn the camera off in the call, or leave the call, before `k` takes the - camera back or the screen locks, or as soon as you can after. A call still - showing a camera that was taken back keeps a processor core busy and - keeps hold of the camera, and until then `k` cannot give it to any zone. + camera back or the screen locks, or as soon as you can after. Until the + call lets go of a camera that was taken back, `k` cannot give it to any + zone. For administration, Ctrl+Alt+F2 gives a text login: log in as the user, and `su` becomes root, where passwords change: `passwd NAME` for the user, From 64793ba3a354cb8c4e3dd92043d911b1d7472d9b Mon Sep 17 00:00:00 2001 From: DevomB Date: Thu, 29 Oct 2026 08:09:54 -0700 Subject: [PATCH 3/3] camera-check --hardware fails plainly when the granted node has no USB device, and camera.md says an infrared camera goes with it --- build/guest-tests/camera-check.sh | 5 ++++- docs/design/camera.md | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build/guest-tests/camera-check.sh b/build/guest-tests/camera-check.sh index ac5220a1..b001ec36 100755 --- a/build/guest-tests/camera-check.sh +++ b/build/guest-tests/camera-check.sh @@ -240,7 +240,10 @@ ended() { [[ "$(holder)" == none* && ! -e "$node" ]]; } # The granted node's USB device: its interface's parent. hn="$(holder | sed -n 's/.*(\(video[0-9]*\))$/\1/p')" dev="$(readlink -f "/sys/class/video4linux/${hn:-none}/device/..")" -if [[ "$rc" -eq 0 && -e "$node" ]]; then +if [[ "$HW" -eq 1 && ! ( -n "$hn" && -w "$dev/authorized" ) ]]; then + fail "camera-gone-ends-grant" "no USB device for ${hn:-the granted node}: $(holder)" + as_user "kryptik-launch --camera-revoke" > /dev/null 2>&1 +elif [[ "$rc" -eq 0 && -e "$node" ]]; then if [[ "$HW" -eq 1 ]]; then echo 0 > "$dev/authorized"; else modprobe -r vivid; fi > "$LOG/gone.out" 2>&1 ok=no for _ in $(seq 1 15); do ended && { ok=yes; break; }; sleep 1; done diff --git a/docs/design/camera.md b/docs/design/camera.md index 9ed1616f..31a4c86a 100644 --- a/docs/design/camera.md +++ b/docs/design/camera.md @@ -127,7 +127,8 @@ the laptop's own USB camera, with `vivid` never loaded, and what only `uvcvideo` shows: a program that keeps its descriptor past `k` holds the driver, which stays loaded, and no grant is given until it lets go; the next grant after that captures again and unloads the driver at its end; and -the camera taken away (its USB device deauthorized) ends the grant, and +the camera taken away (its USB device deauthorized, which takes an infrared +camera in the same module with it for those seconds) ends the grant, and loads nothing when it comes back. ## Not covered