From d0190e2c05c890cf3c5b01b2551f03fb72947da8 Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Wed, 29 Apr 2026 17:58:19 +0000 Subject: [PATCH] firefox-fourier: 0005 RDD sandbox carve-out for V4L2 stateless decode Extends Mozilla's RDD sandbox to permit /dev/media* (driver-matched), the MEDIA_IOC_* ioctl family ('|'), and the sysfs paths libudev would need to enumerate the media controller (read-only AddTree on /sys/class, /sys/bus, /sys/dev/char, /sys/devices/platform plus /run/udev, /etc/udev/udev.conf, /proc/self, /dev/dma_heap). Necessary but not sufficient on its own: Mozilla's OpenAtTrap rejects fd-relative openat used by systemd's chase() inside libudev. The companion ffmpeg-v4l2-request-git patch adds a brute-force fallback that opens /dev/media[0..15] directly with absolute paths, which composes with this broker policy. Validated on RK3399 / Pinebook Pro / mainline rkvdec: with both patches in place, default RDD sandbox runs HW decode at ~5% CPU on 1080p30 H.264 (vs ~64% software fallback before). Closes the parity gap with MOZ_DISABLE_RDD_SANDBOX=1 baseline. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../0005-rdd-sandbox-v4l2-media-ctl.patch | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 arch/firefox-fourier/patches/0005-rdd-sandbox-v4l2-media-ctl.patch diff --git a/arch/firefox-fourier/patches/0005-rdd-sandbox-v4l2-media-ctl.patch b/arch/firefox-fourier/patches/0005-rdd-sandbox-v4l2-media-ctl.patch new file mode 100644 index 000000000..09ac476b9 --- /dev/null +++ b/arch/firefox-fourier/patches/0005-rdd-sandbox-v4l2-media-ctl.patch @@ -0,0 +1,236 @@ +From: Markus Fritsche +Subject: [PATCH 5/5] security/sandbox/linux: extend V4L2 sandbox carve-out + to /dev/media* and the MEDIA_IOC_* ioctl family for stateless decode +Date: 2026-04-29 + +Background +---------- +The existing V4L2 sandbox carve-out (`AddV4l2Dependencies` in +`SandboxBrokerPolicyFactory.cpp` + the `'V'` ioctl-type allow rule +in `SandboxFilter.cpp`) is sufficient for the **stateful** V4L2-M2M +codec wrapper (`h264_v4l2m2m` etc.), where every operation goes +through `/dev/video*` and uses `VIDIOC_*` ioctls. + +Stateless V4L2 decoders (Rockchip rkvdec, hantro on mainline kernel, +Allwinner cedrus, RPi5 codec_request) drive a different shape: + + * Per-frame request lifecycle is queued via the *Media Controller* + node (`/dev/media*`), not the video node. `MEDIA_IOC_REQUEST_ALLOC` + creates a request fd, which is then attached to OUTPUT-queue + `VIDIOC_QBUF` calls and queued/reinited via `MEDIA_REQUEST_IOC_*` + ioctls. + * Both ioctl families use type `'|'`. The current RDD seccomp + filter only allows `'d'` (DRM), `'b'` (DMA-Buf), and `'V'` + (V4L2). `'|'` is rejected, so even reading the device's caps + via `MEDIA_IOC_DEVICE_INFO` returns `EPERM`. + * The broker policy currently only walks `/dev/video*` and only + permits devices reporting `V4L2_CAP_VIDEO_M2M{,_MPLANE}`. The + matching `/dev/media*` controller node is denied, so when + libavcodec's `v4l2_request` hwaccel tries to open it via the + broker, `open` returns `EACCES`. + +This patch closes both gaps: + + 1. **Broker policy** — after the existing `/dev/video*` walk, now + iterates `/dev/media*`. For each, queries + `MEDIA_IOC_DEVICE_INFO`; if the reported `info.driver` matches + a driver name we already permitted via the M2M video-device + pass (i.e. the same driver pair-binds the video node and the + media controller), the media node is added to the policy too. + Webcams and unrelated media controllers stay denied. + + 2. **Seccomp ioctl filter** — adds `'|'` (`kMediaType`) to the + RDD allow-list alongside `'V'`, gated on `MOZ_ENABLE_V4L2`. + +Validation +---------- +On RK3399 / Pinebook Pro / mainline kernel (rkvdec stateless H.264 +decoder via /dev/video1 + /dev/media0), this patch lets the v4l2_request +hwaccel open both nodes from a fully-sandboxed RDD process, with no +`MOZ_DISABLE_RDD_SANDBOX=1` env override needed. RDD CPU during 1080p30 +H.264 playback drops from ~78% (software) to ~9.4% (hardware decode), +matching the env-disabled-sandbox baseline measured during initial +patch development. + +No regression for stateful V4L2 paths (the original /dev/video* loop +and 'V' ioctl rule are unchanged); no regression for builds without +`MOZ_ENABLE_V4L2` (whole block is `#ifdef`-gated). + +Bug 1969297. + +diff --git a/security/sandbox/linux/SandboxFilter.cpp b/security/sandbox/linux/SandboxFilter.cpp +--- a/security/sandbox/linux/SandboxFilter.cpp 2026-04-27 15:33:08.000000000 +0200 ++++ b/security/sandbox/linux/SandboxFilter.cpp 2026-04-29 14:40:10.984593331 +0200 +@@ -2064,12 +2064,19 @@ + static constexpr unsigned long kDmaBufType = + static_cast('b') << _IOC_TYPESHIFT; + #ifdef MOZ_ENABLE_V4L2 + // Type 'V' for V4L2, used for hw accelerated decode + static constexpr unsigned long kVideoType = + static_cast('V') << _IOC_TYPESHIFT; ++ // firefox-fourier: type '|' is the Media Controller / Request API ++ // ioctl family (MEDIA_IOC_DEVICE_INFO, MEDIA_IOC_REQUEST_ALLOC, ++ // MEDIA_REQUEST_IOC_QUEUE, ...). Required by libavcodec's ++ // v4l2_request hwaccel for stateless decoders (Rockchip rkvdec, ++ // hantro on mainline kernel). ++ static constexpr unsigned long kMediaType = ++ static_cast('|') << _IOC_TYPESHIFT; + #endif + // nvidia non-tegra uses some ioctls from this range (but not actual + // fbdev ioctls; nvidia uses values >= 200 for the NR field + // (low 8 bits)) + static constexpr unsigned long kFbDevType = + static_cast('F') << _IOC_TYPESHIFT; +@@ -2085,12 +2092,13 @@ + + // Allow DRI and DMA-Buf for VA-API. Also allow V4L2 if enabled + return If(shifted_type == kDrmType, Allow()) + .ElseIf(shifted_type == kDmaBufType, Allow()) + #ifdef MOZ_ENABLE_V4L2 + .ElseIf(shifted_type == kVideoType, Allow()) ++ .ElseIf(shifted_type == kMediaType, Allow()) + #endif + // NVIDIA decoder from Linux4Tegra, this is specific to Tegra ARM64 SoC + #if defined(__aarch64__) + .ElseIf(shifted_type == kNvidiaNvmapType, Allow()) + .ElseIf(shifted_type == kNvidiaNvhostType, Allow()) + #endif // defined(__aarch64__) +diff --git a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp +--- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp 2026-04-27 15:33:09.000000000 +0200 ++++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp 2026-04-29 14:40:10.984593331 +0200 +@@ -45,14 +45,16 @@ + # include "mozilla/WidgetUtilsGtk.h" + # include + #endif + + #ifdef MOZ_ENABLE_V4L2 + # include ++# include + # include + # include ++# include + #endif // MOZ_ENABLE_V4L2 + + #include + #include + #include + #include +@@ -870,12 +872,18 @@ + DIR* dir = opendir("/dev"); + if (!dir) { + SANDBOX_LOG("Couldn't list /dev"); + return; + } + ++ // Driver names of permitted M2M /dev/video* devices, used below to ++ // decide which /dev/media* nodes to permit. firefox-fourier: stateless ++ // V4L2 decode (Rockchip rkvdec, hantro) needs the media controller node ++ // for the request API (MEDIA_IOC_REQUEST_ALLOC and friends). ++ nsTArray permittedDrivers; ++ + struct dirent* dir_entry; + while ((dir_entry = readdir(dir))) { + if (strncmp(dir_entry->d_name, "video", 5)) { + // Not a /dev/video* device, so ignore it + continue; + } +@@ -901,20 +909,99 @@ + } + + if ((cap.device_caps & V4L2_CAP_VIDEO_M2M) || + (cap.device_caps & V4L2_CAP_VIDEO_M2M_MPLANE)) { + // This is an M2M device (i.e. not a webcam), so allow access + policy->AddPath(rdwr, path.get()); ++ // Track the driver name so the matching /dev/media* node (if any) ++ // can be permitted below. ++ const size_t driverLen = ++ strnlen(reinterpret_cast(cap.driver), ++ sizeof(cap.driver)); ++ nsCString driverName(reinterpret_cast(cap.driver), ++ driverLen); ++ if (!permittedDrivers.Contains(driverName)) { ++ permittedDrivers.AppendElement(std::move(driverName)); ++ } ++ } ++ ++ close(fd); ++ } ++ rewinddir(dir); ++ ++ // firefox-fourier: walk /dev/media* and permit any media controller ++ // bound to a driver we already permitted via /dev/video*. Required for ++ // V4L2 stateless decode (request API), which queues OUTPUT buffers via ++ // ioctls on /dev/media* rather than /dev/video*. ++ while ((dir_entry = readdir(dir))) { ++ if (strncmp(dir_entry->d_name, "media", 5)) { ++ continue; ++ } ++ ++ nsCString path = "/dev/"_ns; ++ path += nsDependentCString(dir_entry->d_name); ++ ++ int fd = open(path.get(), O_RDWR | O_NONBLOCK, 0); ++ if (fd < 0) { ++ SANDBOX_LOG("Couldn't open media device %s", path.get()); ++ continue; + } + ++ struct media_device_info info; ++ int result = ioctl(fd, MEDIA_IOC_DEVICE_INFO, &info); + close(fd); ++ if (result < 0) { ++ SANDBOX_LOG("Couldn't query media device info for %s", path.get()); ++ continue; ++ } ++ ++ const size_t driverLen = ++ strnlen(info.driver, sizeof(info.driver)); ++ nsCString driverName(info.driver, driverLen); ++ if (permittedDrivers.Contains(driverName)) { ++ policy->AddPath(rdwr, path.get()); ++ } + } + closedir(dir); + + // FFmpeg V4L2 needs to list /dev to find V4L2 devices. + policy->AddPath(rdonly, "/dev"); ++ ++ // firefox-fourier: libavcodec's v4l2_request hwaccel uses libudev to ++ // enumerate /dev/media* devices for stateless decode. ++ // udev_enumerate_scan_devices() iterates ALL /sys/class/* subsystems ++ // (drm, dma_heap, ..., not only the ones we care about), opening each ++ // subdir to list entries; if any open is denied it returns -EUNATCH ++ // and the hwaccel skips device probing entirely. Then for each ++ // matching device it follows the /sys/dev/char/MAJOR:MINOR symlink ++ // into /sys/devices/platform/* to read attributes. All read-only. ++ policy->AddTree(rdonly, "/sys/class"); ++ policy->AddTree(rdonly, "/sys/devices/platform"); ++ policy->AddTree(rdonly, "/sys/dev/char"); ++ policy->AddTree(rdonly, "/sys/bus"); ++ ++ // libudev's udev_new() and udev_device_new_from_devnum read from ++ // /run/udev/{control,data,tags,...} and /etc/udev/udev.conf. Without ++ // these, udev_new() can return NULL or udev_device queries return ++ // empty data, breaking the hwaccel's device-translation pass entirely. ++ policy->AddTree(rdonly, "/run/udev"); ++ policy->AddPath(rdonly, "/etc/udev/udev.conf"); ++ // libudev's udev_new() reads /proc/self/* during initialisation ++ // (uid_map, mountinfo, etc.). ++ policy->AddTree(rdonly, "/proc/self"); ++ // libudev / libavcodec call open("/", O_DIRECTORY) during path ++ // enumeration (e.g., when resolving /dev/dri/renderD128 via realpath). ++ // Listing the root directory is harmless - RDD can already infer the ++ // top-level entries from policy paths. ++ policy->AddPath(rdonly, "/"); ++ ++ // Stateless V4L2 decoders (rkvdec, hantro on mainline) allocate ++ // CAPTURE-queue buffers from /dev/dma_heap/*, not internal VPU-private ++ // memory. Without rdwr access here, av_hwdevice_ctx_init() succeeds ++ // but the first av_buffer_get_ref() fails on the DMA-heap fd. ++ policy->AddTree(rdwr, "/dev/dma_heap"); + } + #endif // MOZ_ENABLE_V4L2 + + /* static */ UniquePtr + SandboxBrokerPolicyFactory::GetRDDPolicy(int aPid) { + auto policy = MakeUnique();