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) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 17:58:19 +00:00
parent e0c1816ff4
commit d0190e2c05
@@ -0,0 +1,236 @@
From: Markus Fritsche <mfritsche@reauktion.de>
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<unsigned long>('b') << _IOC_TYPESHIFT;
#ifdef MOZ_ENABLE_V4L2
// Type 'V' for V4L2, used for hw accelerated decode
static constexpr unsigned long kVideoType =
static_cast<unsigned long>('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<unsigned long>('|') << _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<unsigned long>('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 <glib.h>
#endif
#ifdef MOZ_ENABLE_V4L2
# include <linux/videodev2.h>
+# include <linux/media.h>
# include <sys/ioctl.h>
# include <fcntl.h>
+# include <cstring>
#endif // MOZ_ENABLE_V4L2
#include <dirent.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
@@ -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<nsCString> 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<const char*>(cap.driver),
+ sizeof(cap.driver));
+ nsCString driverName(reinterpret_cast<const char*>(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<SandboxBroker::Policy>
SandboxBrokerPolicyFactory::GetRDDPolicy(int aPid) {
auto policy = MakeUnique<SandboxBroker::Policy>();