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();