Erica
---------- Forwarded message --------- From: Erica Windisch <[email protected]> Date: Sun, Aug 16, 2026 at 2:31 PM Subject: OpenZFS Linux open zpool manipulation and escapes via unprivileged userns To: <[email protected]> I am hopeful that upstream patches and remediation guidance will be available soon. I have been sitting on these for a bit, and have run this through AI services hosted in Singapore and the United States, and had notified CERT on 8/12/2026. In order to minimize the risk of asymmetric knowledge of these threats being used against defenders, I am opting for full-disclosure. Cheers from HOPE --Bird ## Report metadata - **Product:** OpenZFS on Linux (`spl.ko` and `zfs.ko`; out-of-tree and not part of the Linux kernel source tree) - **OpenZFS revision audited:** `9b7642df931a765509403318553f2910daf71305` (development version `2.4.99-1`, commit title "Harden recv record validation") - **Linux validation kernel:** `7.2.0-rc3-00275-gaf5e34a41cd6` - **Validation environments:** disposable QEMU guest with generic KASAN and `CONFIG_KASAN_VMALLOC=y`; a stock IncusOS appliance guest for deployment-level authorization/volume-swap validation; disposable QEMU appliance VMs for Proxmox VE, TrueNAS SCALE, and Unraid dynamic reachability testing. Release-mode OpenZFS modules unless stated otherwise. - **Research dates:** 2026-07-17 through 2026-07-25 - **Report updated:** 2026-07-25 - **Reporter:** Erica Windisch - **Tracker umbrella:** [issue #26]( https://github.com/ewindisch/linux-research-tracker/issues/26) All kernel execution used disposable QEMU/cloud-hypervisor guests — the KASAN research guest or the stock IncusOS appliance guest. Host-side work was limited to building, offline image construction, and driving those guests. ### Revision notes - **2026-07-25 (validation-chain results):** OZ-3c and OZ-6 move from **LIKELY** to **VALIDATED** with dynamic triggers in the KASAN guest (OZ-3c: UBSAN array-index-out-of-bounds at `zfs_replay.c:115`; OZ-6: KASAN null-ptr-deref in `vdev_is_concrete` and slab-out-of-bounds in `spa_ld_log_sm_cb`). QNAP QZFS static analysis validates Z1/Z2/dnode-bonus S3 and gang overrun, and BLOCKS log-spacemap S4 (feature absent). The upstream patch audit confirms every OZ finding remains **UNFIXED** at upstream master HEAD `3020c18c` (2026-07-24); only OZ-7 has an open, contested PR (#18620). Full audit: [`research/upstream-patch-audit.md`](../research/upstream-patch-audit.md). - **2026-07-25 (OZ-4 on Proxmox VE):** the Proxmox VE 8.x OZ-4 cell is upgraded from provisional to a confirmed dynamic verdict. On `6.8.12-37-pve` / OpenZFS `2.2.10-pve1`, importing the armed dnode-bonus pool (`dn_bonuslen= 0x400`, `zfs-77-d77-s3v2.img`) **from inside the delegated LXC container** (CT100 with the OZ-1 verified `/dev/zfs` passthrough) fired the `dbuf_read_bonus` overflow on `spa_tryimport → spa_load → spa_ld_trusted_config` and crashed the kernel (NULL-deref in `strcmp` during `vdev_open_children_impl`; host-side corroboration showed SLUB `LIST_POISON` heap corruption in `dbuf_sync_list`). OZ-4 is therefore **VIABLE from the delegated/privileged container** and **BLOCKED by default** (default unprivileged LXC has no `/dev/zfs`). A container→root shell was not reproduced on the stock appliance (release kernel faults deterministically; no KASLR leak). Full verdict and evidence: `linux-research/.fleet-coord/oz4-proxmox-verdict.md`; `linux-research/appliance-harness/proxmox-ve/runs/20260725T150254Z/evidence/`. - **2026-07-25 (OZ-4 on Unraid):** the Unraid 7.3.0 OZ-4 cell is upgraded from provisional to a confirmed dynamic verdict. On `6.18.29-Unraid` / OpenZFS `2.4.1-1`, importing the inflated-bonus pool (`dn_bonuslen=0x1000`, `zfs-77-d77-s3v3.img`) from a `--privileged` Docker container fails `insufficient replicas` while the structurally identical clean control imports ONLINE; the kernel survives (silent heap corruption, no panic/Oops). OZ-4 is therefore **VIABLE from `--privileged`** and **BLOCKED** by default and `--device /dev/zfs` (no `/dev/zfs` / no `CAP_SYS_ADMIN`). A container→root shell was not pursued (requires heap grooming + KASLR bypass). Full verdict and evidence: `linux-research/.fleet-coord/oz4-unraid-verdict.md`; `linux-research/appliance-harness/unraid/runs/20260725T150925Z/evidence/`. - **2026-07-25 (OZ-4 OCI delivery on IncusOS):** an OCI image (`oz4-delivery:latest`) carrying the armed pool (`d77.img`) was built with podman, repacked as an Incus image, and launched as a default unprivileged Incus container on IncusOS `202607192224` (OpenZFS `2.4.3-1`). The container received `/dev/zfs` and the ioctls were authorized (OZ-1 reachability **VIABLE**), but the host kernel could not open the file vdev at the container-relative path (`/opt/d77.img`), so the OZ-4 overflow never fired (**BLOCKED** for the in-container E2E fire). IncusOS's shell-less design removes the host-side bind-mount escape hatch that made the fire possible on Proxmox VE. Full verdict and evidence: `linux-research/.fleet-coord/oz4-oci-incusos-verdict.md`; `linux-research/incusos-spike/runs/20260725-oz4-oci/evidence/`. - **2026-07-25 (OZ-4 OCI delivery on TrueNAS SCALE):** the same OCI image (`oz4-delivery:latest`) was launched as a default unprivileged Incus container on TrueNAS SCALE 25.04.2.4. The entrypoint auto-fired the import ioctl, but the host kernel could not open the container-relative file vdev (`/opt/d77.img`), so the OZ-4 overflow never fired (**BLOCKED**). This refines the prior TrueNAS host-side **VIABLE** verdict: the overflow is reachable from a host path, but not from a self-contained OCI image in a default container. Full verdict and evidence: `linux-research/.fleet-coord/oz4-oci-truenas-verdict.md`; `linux-research/appliance-harness/truenas-scale/runs/20260725T190000Z/evidence/`. - **2026-07-25 (OZ-4 on TrueNAS):** the Dynamically tested appliances table gains an **OZ-4 column**. TrueNAS SCALE 25.04.2.4 (running OpenZFS `2.3.0-1`) is **VIABLE** for OZ-4 from the default unprivileged container: the armed dnode-bonus pool imported ONLINE with no error erevent (oversized `dn_bonuslen` accepted, not rejected) on the production non-KASAN kernel, and the same image fired `Write of size 1024 in dbuf_read_impl` via `spa_tryimport` under KASAN. The kernel survives the corruption; a container→root shell is not demonstrated (neighbor-lottery + KASLR, as in the 77b campaign). Proxmox VE and Unraid OZ-4 cells are marked **BLOCKED** by default pending their sibling fleet validation tasks. Full verdict and evidence path: `linux-research/.fleet-coord/oz4-truenas-verdict.md`. - **2026-07-25 (OZ-4 OCI delivery, round 2 — VALIDATED on TrueNAS SCALE and Unraid):** the malicious-OCI-image vector is upgraded from **BLOCKED** to **VALIDATED** on two appliances. The round-1 blocker was that the host kernel's `vdev_file_open` resolves vdev paths in the host mount namespace, so a container-internal `/opt/d77.img` is never opened. Round 2 defeats this with an **identical-path host bind mount** (the image entrypoint stages its embedded armed pool onto a bind-mounted host path visible at the same absolute path in both namespaces, then imports from that path). On **TrueNAS SCALE 25.04.2.4** the vector fires from a **default unprivileged** Incus container (no `security.privileged`, no explicit device add — only the default `/dev/zfs`, an identical-path `disk` bind, and a `1777` host staging path): the armed import **panicked the production kernel** (GPF in `__kmalloc` on the `spa_tryimport → spa_config_generate` path, crashing task `Comm: zpool` as the container's mapped uid in `ZFS_IOC_POOL_TRYIMPORT`), while the structurally identical clean control imported ONLINE. On **Unraid 7.3.0** the image-carried payload fires via `docker run --privileged` (the required and sufficient capability); the negative control showed the bind mount is not even required under Unraid's `vfs` storage driver. The Unraid result is **expected behavior** — a `--privileged` container holds full host capabilities by design — and is retained for matrix completeness only; it is not scored as a finding. The TrueNAS round-2 fire is the significant result, but note its two deployment concessions: an identical-path host bind and a world-writable host staging path must be granted to the container (as an app manifest's hostPath grant would provide). The Unraid kernel survives (silent corruption → `insufficient replicas`); the TrueNAS crash is a **reliable image-delivered container→host-kernel DoS** — no heap grooming required. IncusOS remains **BLOCKED** for this vector (shell-less appliance; no host bind-mount escape hatch). Full verdicts and evidence: `linux-research/.fleet-coord/oz4-oci-truenas2-verdict.md`, `linux-research/.fleet-coord/oz4-oci-unraid-verdict.md`; `linux-research/appliance-harness/truenas-scale/runs/20260725T193000Z/evidence/`, `linux-research/appliance-harness/unraid/runs/20260725T194343Z/evidence/`. - **2026-07-26 (OZ-4 OCI on IncusOS, round 2 — VALIDATED with ZERO host-side setup):** IncusOS flips from the fleet's only **BLOCKED** to **VALIDATED**, and the malicious-OCI-image vector now has a no-setup fire. The round-1 blocker (host kernel resolves vdev paths in the host mount namespace; shell-less appliance precludes an admin bind mount) is defeated **without any bind**: the container's own rootfs necessarily exists on the host at the predictable path `/var/lib/incus/storage-pools/local/containers/<ct>/rootfs/`, so the image's payload at `/opt/d77.img` is host-resolvable by construction. A malicious OCI image launched as a **default unprivileged** container (default profile only — no `security.privileged`, no device add, no bind) carries a static raw `ZFS_IOC_POOL_TRYIMPORT` PoC (hand-packed nvlist, libzfs bypassed; `linux-research/incusos-spike/oz4-tryimport.c`, zg-poolop.c lineage) pointed at that host path; the ioctl is issued by **non-root uid 1000**, authorized via an image-controlled file capability (`cap_sys_admin+ep` on the payload binary — an attacker image property, not an admin concession; verified differentially: cap cleared → `EPERM`). The armed import **wedges the host kernel's ZFS load path and storage stack** (`incusd` unresponsive, ioctl never returns; reproduced on a fresh kernel), while the structurally identical clean control imports in <1s. Negative controls (wrong path, nonexistent path, cleared cap) all fail as expected. On this kernel the overflow surfaces as a **wedge/storage-DoS** rather than TrueNAS's instant GPF-panic (heap-layout dependent); no root shell is claimed. Attacker preconditions: get the victim to launch the image as a default container; knowledge of the container name; nothing else. Full verdict and evidence: `linux-research/.fleet-coord/oz4-oci-incusos2-verdict.md`; `linux-research/incusos-spike/runs/20260726-oz4-oci2/evidence/`. - **2026-07-26 (OZ-4 OCI on TrueNAS SCALE, round 3 — VALIDATED with ZERO host-side setup, kernel panic):** the zero-wire rootfs-path vector fires on TrueNAS SCALE 25.04.2.4 as well, upgrading round 2's concession-laden fire. A malicious OCI image launched as a **default unprivileged** Incus container (dir storage backend, default profile only — no privileged flag, no device add, no bind, no host shell) carries the armed pool at `/opt/d77.img`, a static raw-ioctl PoC (`oz4ti`, zg-poolop/xpl-107-poolop lineage), and a **NATIVE-encoded** config nvlist naming the host-side rootfs path (`/var/lib/incus/storage-pools/default/containers/<name>/rootfs/opt/d77.img`, fully derivable from appliance defaults + the attacker-chosen container name). The import is issued by a **non-root** nested-userns user (host `UID: 2147001001`). The armed import **panicked the production kernel** (GPF in `__kmalloc` on `spa_tryimport → spa_load → spa_ld_load_special_directories`, corrupted SLUB freelist; crashing task `Comm: oz4ti`), while the structurally identical clean control imported `rc=0` / LOADED. Key mechanism details: the raw ioctl removes the libzfs userspace-stat gate, and the config must be `NV_ENCODE_NATIVE` (the kernel decodes native only; hand-rolled XDR is rejected `EFAULT`). This is a fully self-contained, image-delivered, **non-root container → host-kernel panic** — no heap grooming required; turnkey root shell not demonstrated. Full verdict and evidence: `linux-research/.fleet-coord/oz4-oci-truenas3-verdict.md`; `linux-research/appliance-harness/truenas-scale/runs/20260726T045000Z/evidence/`; PoC `linux-research/vulnlib/tools/oz4-tryimport.c` + `genconfig.c`. - **2026-07-25 (RHEL evaluation):** adds the RHEL 10.2 STIG/FIPS/SELinux deployment section under Dynamically tested appliances: OZ-1/OZ-2 are **blocked** there — primarily by the OpenZFS 2.2.10 kernel credential check (`EINVAL` for namespace-local-`CAP_SYS_ADMIN` callers), with the SELinux `container_t` → `/dev/zfs` gate (`container_use_devices=off`, toggle-verified), `fapolicyd`, DAC, device-cgroup, and STIG mount options as layered controls; includes the "how SELinux does and does not prevent" analysis and notes that OZ-3+ parser findings were not validated on 2.2.10 (version gap; no import/mount delivery path). - **2026-07-25 (structural consolidation).** The OZ-1→OZ-4 chain, the IncusOS volume-swap, the `/dev/zfs` upstream-`0666` policy argument, and the appliance verdicts are each stated **once** and cross-referenced, instead of retold across the summary, deployment, CVSS, and per-finding sections. **CVSS posture fix:** the base-score tables now contain one row per independent root cause; per-appliance and delegated/privileged variations are deployment *preconditions* and are moved to [Environmental metrics](#deployment-environmental-modifiers). All v3.1 and v4.0 vectors recomputed with the `cvss` Python library (v3.6); published scores match. No technical claims changed. - **2026-07-25:** Unraid 7.3.0 harness `pivot_root` failure resolved (initramfs rootfs, parent-mount workaround). `--device /dev/zfs` alone is **blocked** for OZ-1 (Docker default caps omit `CAP_SYS_ADMIN` → `EPERM`, recorded as a 0.0 honest negative); `--privileged` reproduces OZ-1. QNAP QuTS hero assessment updated: QZFS fork diverged ~2020; Z1/Z2/dnode-bonus-S3 likely applicable, log-spacemap/gang uncertain. - **2026-07-24:** TrueNAS SCALE 25.04.2.4 and Proxmox VE 8.x dynamic OZ-1/OZ-2 verdicts; CVSS v4.0 candidate vectors added; headline elevated to state the unprivileged container → host uid-0 shell. ## Summary Two interacting groups of defects, both confirmed: 1. **Authorization (OZ-1, OZ-2).** On Linux, OpenZFS administrative ioctl policies accept `CAP_SYS_ADMIN` in the caller's *own* user namespace, and pool-level handlers omit the dataset zone/delegation visibility gate. With a reachable `/dev/zfs` and namespace-local `CAP_SYS_ADMIN`, an unprivileged user or container can invoke host-pool operations. Vdev opens can additionally occur without enforcing the caller's filesystem or device-node DAC permissions. 2. **Parsers (OZ-3…OZ-8).** Multiple import, replay, and metadata-read paths trust attacker-controlled on-disk lengths, indices, or graph structure. Confirmed outcomes include heap/vmalloc out-of-bounds writes, a controlled heap overflow, a kernel stack overflow, unbounded fatzap traversal, and an uninterruptible SPL cache-destroy hang. The authorization defects materially change the threat model for the parser findings: paths normally treated as administrator-only import or pool-management surfaces become reachable from an unprivileged user namespace when `/dev/zfs` is exposed. This was reproduced in the KASAN guest and, for the pool-level authorization and cross-tenant dataset-replacement issues, on stock IncusOS. ### Key messages - **CISA / national CSIRTs:** Local, low-privilege container-to-host escape on systems that expose `/dev/zfs` to unprivileged containers whose tenants can create nested user namespaces to obtain namespace-local `CAP_SYS_ADMIN`. The demonstrated uid-0 shell was obtained in a research VM with KASLR and SLUB hardening disabled; the same authorization flaw is confirmed on stock IncusOS and on TrueNAS SCALE 25.04.2.4, where it enables cross-tenant storage compromise without memory corruption. Proxmox VE and Unraid default containers do **not** expose `/dev/zfs`; on Unraid, `--device /dev/zfs` alone also does **not** expose OZ-1 (Docker omits `CAP_SYS_ADMIN`), and `--privileged` is required. The OZ-1 flaw itself is present and reachable on Unraid 7.3.0 from a non-root nested user namespace. Both CVSS v3.1 and v4.0 rate the demonstrated chain High. - **OpenZFS / kernel developers:** The root cause is not a missing capability check but use of namespace-local `CAP_SYS_ADMIN` as authority over host-pool operations, combined with missing caller-DAC enforcement on vdev opens and several unvalidated on-disk parser paths. Fixes must address authorization and input validation independently; changing the host `/dev/zfs` mode alone is insufficient. - **Systems operators:** Hardening `/dev/zfs` to `0660` or hiding it from containers is defense-in-depth, not a substitute for correct in-kernel authorization. Audit every container's device-cgroup policy, capability set, and in-node `/dev/zfs` presence, and restrict the ZFS character major/minor until patches ship. OpenZFS on Linux is an out-of-tree kernel module. Security ownership and CVE decisions belong with the OpenZFS project and its vendors/CNA, not the Linux kernel CVE team. OpenZFS also has a broader practical surface than a pure filesystem: it combines filesystem code with pool and volume management, consumes file and block-device paths as vdevs, exposes zvol block devices, and can attach, replace, and mirror those devices — closer in deployment terms to the block-path/volume-management role of LVM/device-mapper than to parsing a filesystem only after `mount(2)`. ## Glossary | Term | Meaning in this report | |---|---| | **Initial (host) user namespace** | The root user namespace of the host. Capabilities held here are real host privileges. | | **Child / nested user namespace** | A user namespace created by a process (e.g., with `unshare -Ur`). UIDs and capabilities inside it are mapped and valid only within that namespace. | | **`CAP_SYS_ADMIN` (initial namespace)** | The real host administrative capability. A process holding it in the initial user namespace can perform privileged host operations. | | **Namespace-local `CAP_SYS_ADMIN`** | `CAP_SYS_ADMIN` held only inside a child user namespace. It does **not** confer host administrative authority. A non-root user can obtain it by creating a user namespace and mapping themselves to uid 0 inside it. | | **The OZ-1 defect** | OpenZFS's `zfs_secpolicy_config()` uses `ns_capable(cr->user_ns, CAP_SYS_ADMIN)`, which accepts namespace-local `CAP_SYS_ADMIN` as authority for host-pool operations. The correct check is `CAP_SYS_ADMIN` in the **initial** user namespace (or an equivalent host-level authorization check). | | **Why `--device /dev/zfs` alone is blocked in Docker** | Docker's default capability set omits `CAP_SYS_ADMIN` even in the container's user namespace, so the OpenZFS ioctl fails with `EPERM`. `--privileged` (or `--cap-add SYS_ADMIN`) grants the capability and reproduces OZ-1. | ## Findings and validation status **Validation labels:** **VALIDATED** = dynamically reproduced with a tested PoC; **LIKELY** = source-confirmed or architecturally supported but no isolated dynamic trigger yet; **BLOCKED** = investigated and found not applicable or refuted. | ID | Finding | Status | Demonstrated consequence | Validation | |---|---|---|---|---| | OZ-1 | Linux user-namespace authorization accepted for pool-level ioctls; no pool visibility/zone gate | **Confirmed dynamically and end to end** | Host-pool freeze/destroy and channel-program access from userns/container root | **VALIDATED** | | OZ-2 | Kernel-side vdev open does not enforce caller DAC; `VDEV_ATTACH` mirrors writes into a foreign zvol | **Confirmed dynamically** | Cross-tenant disk corruption and attacker-controlled content injection | **VALIDATED** | | OZ-3a | ZIL `lrc_reclen` 64-to-32-bit truncation before full-width `memcpy` | **Confirmed dynamically** | KASAN slab OOB read of 4,294,967,360 bytes; fail-fault during replay | **VALIDATED** | | OZ-3b | Hole-blkptr `TX_WRITE` length reaches unchecked zero-fill | **Confirmed dynamically** | KASAN vmalloc OOB write of 67,108,864 bytes | **VALIDATED** | | OZ-3c | `zfs_replay_xvattr` trusts `lr_attr_masksize` for a fixed stack array | **Confirmed dynamically** | UBSAN array-index-out-of-bounds at `zfs_replay.c:115` (`index 3 is out of range for type 'uint32_t [3]'`) at mount; kernel panic | **VALIDATED** | | OZ-4 | Dnode bonus length copied into a fixed-size bonus dbuf | **Confirmed dynamically; root-shell exploit demonstrated on the research configuration** | Controlled linear heap overflow, `pipe_buffer` arbitrary read/write, uid-0 shell; KASLR-on production chain not completed | **VALIDATED** | | OZ-5a | Gang-block tree has no runtime depth bound | **Confirmed dynamically** | Stack-guard fault and kernel panic with a depth-2048 chain | **VALIDATED** | | OZ-5b | Gang leaf physical sizes can exceed the leader buffer | **Confirmed dynamically** | Repeated KASAN writes 128 KiB past a 128 KiB ARC buffer | **VALIDATED** | | OZ-6 | Log-spacemap replay trusts vdev IDs and metaslab indices | **Confirmed dynamically** | KASAN null-ptr-deref in `vdev_is_concrete` (invalid vdev ID) and KASAN slab-out-of-bounds in `spa_ld_log_sm_cb` (OOB `vdev_ms[]` index) at writable import | **VALIDATED** | | OZ-7 | fatzap `la_next` is not runtime-bounded and cycles are not detected | **Confirmed dynamically** | KASAN heap OOB access/write and an in-kernel infinite loop | **VALIDATED** | | OZ-8 | SPL cache destroy cancels no grow work and waits on an unwoken waitqueue | **Confirmed dynamically** | Permanent uninterruptible sleep during cache destruction | **VALIDATED** | The earlier hypothesis of an ACL count-truncation information leak was **refuted** (**BLOCKED**): the byte-bound check at `module/os/linux/zfs/zfs_acl.c:615-618` bounds the read. It is intentionally excluded as a vulnerability. ### Lifecycle classification "Mount-time" below means activation or replay associated with mounting a ZFS filesystem dataset. It does not include `zpool import`, zvol minor creation, pool-management ioctls, or later filesystem access. | ID | Trigger phase | VFS mount required? | Classification | |---|---|---|---| | OZ-1 | Runtime `/dev/zfs` pool-control and channel-program ioctls | **No** | Authorization failure, not a mount-time image bug | | OZ-2 | Pool create and runtime vdev open/attach/mirror operations | **No** | Block-path and volume-management authorization failure | | OZ-3a, OZ-3b | ZIL replay | **Mixed** | Dataset replay is mount-associated; zvol replay can occur during import/minor creation without a VFS mount | | OZ-3c | Filesystem-dataset ZIL xvattr replay | **Normally yes** | Mount/replay-time parser issue in the identified route | | OZ-4 | Pool import or a later dnode bonus read | **No in the demonstrated chain** | Demonstrated during import before any dataset mount | | OZ-5 | Gang-block read | **No inherent requirement** | Can trigger during import if pool metadata uses the gang block; demonstrated file-data route triggered on post-mount read | | OZ-6 | Writable pool import and log-spacemap replay | **No** | Import-time pool-recovery issue before dataset mount | | OZ-7 | fatzap metadata traversal | **Yes in the demonstrated route** | Demonstrated after import and mount during directory enumeration; not confined to the `mount(2)` operation itself | | OZ-8 | SPL cache destruction/module teardown while grow work exists | **No** | Runtime teardown/concurrency issue | ## Threat model and applicability ### Linux mount-time policy The published [Linux kernel threat model]( https://kernel.org/doc/html/next/process/threat-model.html) says bugs triggered by mounting a corrupted or malicious filesystem image are generally not treated as Linux kernel security bugs because storage is assumed administrator-controlled, unless a filesystem explicitly promises hardened handling of untrusted media. The same document directs reports involving out-of-tree modules to the relevant vendor, and the [Linux kernel CVE process]( https://www.kernel.org/doc/html/latest/process/cve.html) leaves deployment-specific applicability decisions to downstream users and vendors. That policy is relevant context, but it neither assigns OpenZFS ownership to the Linux kernel nor describes every issue here. A ZFS pool import is a storage-pool activation operation, not a VFS mount. A zvol may be replayed and exposed as a block device without mounting a ZFS filesystem. Pool-control ioctls, vdev path opens, attach/mirror operations, and cache teardown are runtime storage-management surfaces. Even for import- or replay-time parser bugs, OZ-1 changes the privilege premise on affected deployments by allowing a low-privileged container caller to reach operations ordinarily reserved for the host administrator. ### `/dev/zfs` is upstream-shipped `0666`; exposure is a precondition, not the bug There is no Linux ABI rule mandating one universal mode for `/dev/zfs`. The [current upstream OpenZFS Linux rule]( https://raw.githubusercontent.com/openzfs/zfs/master/udev/rules.d/90-zfs.rules.in ) explicitly contains: ```udev KERNEL=="zfs", MODE="0666", OPTIONS+="static_node=zfs" ``` Mode `0666` is therefore not solely a permissive test change or an IncusOS mistake: it is OpenZFS's shipped upstream policy. That design necessarily relies on per-ioctl authorization to protect privileged operations after any local process opens the multiplexed control device. Linux [udev rules](https://man7.org/linux/man-pages/man7/udev.7.html) allow vendor, distribution, and local rules to override `OWNER`, `GROUP`, and `MODE`; a host using `0660`/`0600` is valid hardening but does not correct an in-kernel authorization check and must not be assumed on other installations. **Exposing `/dev/zfs` is a precondition; the vulnerability is OZ-1.** A reachable node still requires the caller to hold `CAP_SYS_ADMIN` (in any user namespace, per OZ-1). This is why `--device /dev/zfs` alone in Docker does not reproduce OZ-1 — the device is present but the default capability set omits `CAP_SYS_ADMIN` and the ioctl fails `EPERM` (see [deployment environmental modifiers](#deployment-environmental-modifiers)). The exposure precondition therefore has two independent parts: a usable node *and* namespace-local `CAP_SYS_ADMIN`. Container policy adds independent controls beyond the node: the container's device controller must permit access, and the [cgroup v2 device-controller]( https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#device-controller ) keys the decision on access type plus char/block type and major/minor, not the pathname — so host-path DAC is not sufficient if a container has another permitted node for the same device number. Conversely, the upstream host udev rule does not by itself require an orchestrator to expose that device inside a container. Explicit Incus character-device passthrough is separately configurable ([`unix-char` docs]( https://linuxcontainers.org/incus/docs/main/reference/devices_unix_char/), documented default `0660`), distinct from both the upstream `0666` policy and the container-local behavior observed on IncusOS. For least privilege, systems that do not intentionally delegate ZFS administration should restrict the host node to root or a tightly controlled administrative group, deny the ZFS character-device major/minor to containers that do not intentionally receive ZFS delegation, and avoid granting containers `CAP_SYS_ADMIN`. Hiding the host pathname or changing only the host node from `0666` to `0660` is defense-in-depth, not a substitute for correct ioctl authorization. ## The chain: OZ-1 → OZ-4 The two findings compose into an unprivileged-to-host-root chain, executed end to end in the research environment. It is told once here and referenced elsewhere. 1. **Reach `/dev/zfs` with namespace-local `CAP_SYS_ADMIN` from a low-privilege position.** In the affected deployment a default unprivileged container exposes a usable `/dev/zfs` (upstream udev requests `0666`) and a non-root tenant can create a nested user namespace holding namespace-local `CAP_SYS_ADMIN`. 2. **OZ-1 converts reachability into host-pool authority.** Pool-level ioctls accept that namespace-local capability as host-pool authority and omit the dataset zone/delegation gate, so the tenant imports an attacker-authored pool image through a host-visible file vdev. This removes the normal administrator-controlled-storage precondition of the import-time parser bugs. 3. **OZ-4 fires at import and is groomed into code execution.** The dnode bonus-buffer overflow, fired during pool import, was groomed via a userspace-only heap groom into kernel arbitrary read/write; overwriting `modprobe_path` then yielded a uid-0 shell. **Demonstrated configuration:** uid-0 shell from an unprivileged (uid 1000) user-namespace position in the KASAN research guest, with KASLR and SLUB freelist randomization/hardening **disabled**. The userspace-only, fully blind Stage-2 run printed `XPL77-ROOT-SHELL-ACHIEVED` with all four uid fields zero; no helper LKM or runtime instrumentation was used. That result establishes the code-execution impact of OZ-4; it is not a sanitizer-only inference. **Not claimed.** The production-hardened (KASLR-on) root shell was **not** completed. With KASLR enabled the forged pipe array needs a runtime `anon_pipe_buf_ops` address and the physical page for `modprobe_path`; the write-only dnode overflow cannot recover either. Freelist randomization makes adjacency probabilistic and freelist hardening makes a miss more likely fatal. On stock IncusOS, steps 1–2 and a full cross-tenant storage compromise were confirmed against the production signed module from a default unprivileged Alpine container; the OZ-4 root shell was **not** run on the appliance. This is an exploit-reliability and environmental limitation, not evidence that the chain is impossible on a production-hardened host. The KASAN interpretation of OZ-4, the attempted-`kasan=off` detail, and the unverified Z1-leak KASLR-bootstrap hypothesis are stated in [OZ-4](#oz-4-dnode-bonus-dbuf-heap-overflow). Full exploit-development evidence, including the root-shell transcript and production-hardening assessment, is in [the 77b report](../research/zfs-escape-chain/77b-exploit-poc.md). ## IncusOS deployment The affected model is an unprivileged Incus container tenant on an IncusOS host that uses OpenZFS for host-managed storage. The attacker has the ordinary ability to execute code as a non-root user inside their own default unprivileged container — no host credentials, `security.privileged`, an explicit `unix-char` device, ZFS delegation, a crafted pool image, or control of a host pathname. ### Conditions validated | Condition | Validated result | Security meaning | |---|---|---| | **Device exposure** | A default unprivileged Alpine container receives `/dev/zfs` as `crw-rw-rw-` (`10:249`); RO and RW `open(2)` both succeed. `/etc/zfs` is absent and is not the boundary. | Removes device-node DAC as a barrier to reaching the ioctl interface. Mode alone does not grant authority for a correctly authorized ioctl. | | **No attacker-created device** | The visible node is not owned by container root; `mknod(c,10,249)` is denied `EPERM`. | Denying `mknod` is insufficient mitigation because the usable node is already supplied. The finding does not rely on `CAP_MKNOD`. | | **Non-root / nested-userns reachability** | A non-root container user can open the node before and after `unshare -Ur`; the nested namespace maps uid 0 to the non-root parent uid, yielding namespace-local `CAP_SYS_ADMIN`. | Nested userns is enabled. This does **not** confer host `CAP_SYS_ADMIN` and is not by itself an escape. | | **OZ-1** | Pool-control policy accepts `CAP_SYS_ADMIN` relative to the caller's userns; pool-level paths omit the zone/delegation gate. A non-root user after `unshare -Ur` got `rc=0` from raw `POOL_GET_HISTORY` against the host `local` pool and read host pool history. | Not a missing capability check — use of the wrong authority: namespace-local admin capability treated as host-pool authority. | | **Storage-object identity weakness** | An unauthorized caller destroyed a detached victim custom-volume dataset, cloned attacker content at the same canonical name, and a normal Incus attach later mounted that replacement for a different tenant. | Requires a detached target; the test did not force-detach an in-use volume. Establishes cross-tenant integrity/availability impact, not a host-root shell or arbitrary victim-file read. | The device, non-root nested-userns, and host-pool authorization conditions were validated together on the affected default IncusOS deployment; the cross-tenant replacement consequence was also dynamically demonstrated on stock IncusOS. ### Demonstrated attack sequence 1. From the unprivileged attacker container, `POOL_GET_HISTORY` disclosed host pool history and a channel program recursively enumerated the otherwise hidden host dataset tree, including custom volumes and other tenants. 2. The attacker snapshotted its own container dataset through a channel-program sync task. 3. The attacker destroyed a detached victim custom-volume dataset and cloned the attacker snapshot at the victim's canonical dataset name. 4. A second container attached the named custom volume through the normal Incus API; Incus set properties on and mounted the replacement dataset. 5. The second tenant read the attacker's marker, `PWNED-BY-ATTACKER`; the original victim marker was absent because the original dataset had been destroyed. Real dataset names were learned through the same authorization bypass, so prior knowledge of the host's internal layout was not required. The volume swap required the target custom volume to be **detached**; an attached dataset's long hold prevented the destroy in the tested path. It needed no crafted pool image, no ZFS mount by the attacker, no host uid 0, no initial-userns capability, and no ZFS delegation. ### Proven impact and limits | Property | Demonstrated impact on stock IncusOS | |---|---| | Confidentiality | Host pool history and full dataset topology across tenant boundaries. History exposed the host path of the pool encryption key, but not the key contents. No victim-file read primitive. | | Integrity | Destruction of a detached victim custom volume and substitution of attacker-controlled content under its trusted canonical name; the normal Incus runtime delivered that content to a second tenant. | | Availability | Loss of the original detached victim dataset and contents. `POOL_FREEZE` passed authorization but caused no observable write stall; a pool-wide outage was not exercised. | | Boundary crossed | A non-root process in an unprivileged default-profile container reached host-managed OpenZFS storage and affected a different tenant without host credentials, ZFS delegation, a tenant-created device node, or an explicit Incus ZFS device configuration. | Secure Boot, the signed unified kernel image, dm-verity, TPM-bound host storage, and ZFS native encryption did not prevent this runtime attack: the stock signed module was already loaded, the pool key was active, and the operations used the live decrypted pool. These controls continue to protect other boundaries but do not replace authorization at `/dev/zfs` or provenance validation during attach. This IncusOS validation does **not** reproduce the separately verified OZ-4 host-root exploit through the appliance tenant path, disclose encryption-key contents, or bypass production hardening. Full environment, controls, and negative results: [IncusOS end-to-end report](../research/incusos-containers/35b-incusos-e2e.md). ### Pre-mount delivery primitive (untested) Dataset or volume replacement is a potential **pre-mount delivery primitive** for a *separate* parser vulnerability: the attacker replaces a detached storage object while retaining the canonical name trusted by the orchestrator; a later legitimate attach/restart/recovery/automount causes the orchestrator or another guest to consume the replacement, which the parsing kernel then processes in its own security context. For a filesystem-backed Incus custom volume, the demonstrated consumer was the host Incus runtime. For a zvol or other block-backed volume, an analogous name-replacement weakness could deliver attacker-controlled block contents to a VM; filesystem probing, journal replay, or automount inside that guest could trigger a vulnerability in ZFS or a different filesystem implementation — e.g. delivery of a crafted UDF volume followed by guest automount could supply the mount action for [UDF-2's mount-time LVID bounds violation](udf-security-findings.md#udf-2-lvid-implementation-use-pointer-escapes-the-buffer). That OpenZFS-to-UDF transport chain has **not** been tested. The IncusOS test proved valid-dataset replacement and a later host mount, but **not** a mount-time memory-corruption trigger; such a chain requires separate validation and must not be collapsed into OZ-1's standalone score. ## Deployment applicability Merely shipping OpenZFS does not establish equal exposure. The import/replay/ metadata findings require a vulnerable code revision and a path by which the host imports/replays/mounts/reads attacker-influenced pool data; OZ-1 requires a user-namespace caller to reach `/dev/zfs` *and* hold namespace-local `CAP_SYS_ADMIN`; OZ-2 additionally requires usable victim file or block-device pathing; the IncusOS volume swap depends on Incus-specific dataset naming and attach behavior and must not be generalized to another storage manager without testing; and a system with no active pool and no loaded module is not exposed merely because the package exists. ### Dynamically tested appliances Proxmox VE, TrueNAS SCALE, and Unraid were booted in disposable QEMU VMs using the `linux-research/appliance-harness/` reuse of the IncusOS QEMU harness. Verdicts apply to the tested versions and default configurations. | Appliance | Default `/dev/zfs` + caps in unprivileged container | OZ-1 | OZ-2 foreign-zvol mirror | OZ-4 dnode-bonus heap overflow | Evidence | |---|---|---|---|---|---| | **Proxmox VE 8.x** | **No** — default unprivileged LXC has no `/dev/zfs`; `mknod(10,249)` blocked by cgroup/AppArmor. | **BLOCKED** by default; **VIABLE** with explicit bind mount `lxc.mount.entry: /dev/zfs dev/zfs none bind,create=file 0 0`, cgroup rule `c 10:249 rwm`, and `lxc.apparmor.profile: unconfined`. | **VIABLE** — `zpool attach -f attackerpool <file> /dev/zd0` attached the victim zvol as a degraded mirror leg. | **BLOCKED** by default; **VIABLE** from the delegated/privileged container — confirmed dynamically on `6.8.12-37-pve` / OpenZFS `2.2.10-pve1`. Importing the armed pool (`dn_bonuslen=0x400`, `zfs-77-d77-s3v2.img`) **from inside** the delegated LXC (CT100, `/dev/zfs` + full caps + `/import`) fired the overflow on `spa_tryimport → spa_load → spa_ld_trusted_config` and **crashed the kernel**: NULL-deref in `strcmp` during `vdev_open_children_impl` (crashing task `Comm: ld-linux-x86-64` = the container's own `zpool` issuing `ZFS_IOC_POOL_TRYIMPORT`); host-side corroboration produced a second oops in `dbuf_sync_list` with `R14/R15 = dead000000000122/0100` (SLUB `LIST_POISON` = heap corruption). The overflow is unfixed in master `9b7642df` (`bonuslen = MIN(dn_bonuslen, dn_phys->dn_bonuslen)`, both on-disk; `ASSERT` bound compiled out of the release `zfs.ko`). A container→root shell was not reproduced on the stock appliance (release kernel faults deterministically; no KASLR leak); the research-configuration root shell (77b) stands separately. Full verdict: `linux-research/.fleet-coord/oz4-proxmox-verdict.md`. | `linux-research/appliance-harness/proxmox-ve/runs/20260725T150254Z/evidence/` (OZ-4 container fire); `…/runs/20260725T145230Z/evidence/` (OZ-4 host fire); `…/runs/20260724T180353Z/evidence/` (OZ-1/OZ-2 + default-`/dev/zfs` BLOCKED) | | **TrueNAS SCALE 25.04.2.4** | **Yes** — default unprivileged Incus-based container receives `/dev/zfs` as `crw-rw-rw- 10:249`, usable for RW `open(2)` with no guest ZFS package or explicit `unix-char` device. | **VIABLE** — container root reads host `boot-pool` history via raw `ZFS_IOC_POOL_GET_HISTORY`; a non-root container user in a nested userns obtains the same `rc=0`. | **VIABLE** — `zpool attach -f attackerpool /tmp/attacker.img /dev/zvol/boot-pool/victim-oz2` produced a DEGRADED mirror with the victim zvol as a leg. | **VIABLE** from host-side import — armed pool (`dn_bonuslen=0x1000`, `zfs-77-d77-s3v3.img`) imported ONLINE with no error erevent on the production `2.3.0-1` kernel (oversized bonus accepted, not rejected); same image fired `Write of size 1024 in dbuf_read_impl` via `spa_tryimport` under KASAN. Kernel survives; root-shell not demonstrated (neighbor-lottery + KASLR, as in 77b). **OCI delivery VALIDATED (round 3, zero host-side setup)** — a malicious OCI image launched as a default unprivileged container (dir storage backend, default profile only, no bind/device/privileged flag) carries the armed pool plus a static raw-`ZFS_IOC_POOL_TRYIMPORT` PoC and a NATIVE-encoded config naming the host-side rootfs path (`/var/lib/incus/storage-pools/default/containers/<name>/rootfs/opt/d77.img`, predictable from appliance defaults + container name); issued by a **non-root** nested-userns user (host `UID: 2147001001`), the armed import **panics the production kernel** (GPF in `__kmalloc` on `spa_tryimport → spa_load → spa_ld_load_special_directories`), while the clean control imports `rc=0` / LOADED. Round 2 (identical-path `disk` bind + `1777` staging path — two deployment concessions) also fired from a default unprivileged container and panicked the kernel; round 1 (container-relative path) was BLOCKED. Full verdicts: `linux-research/.fleet-coord/oz4-truenas-verdict.md`, `linux-research/.fleet-coord/oz4-oci-truenas-verdict.md` (round 1), `linux-research/.fleet-coord/oz4-oci-truenas2-verdict.md` (round 2), `linux-research/.fleet-coord/oz4-oci-truenas3-verdict.md` (round 3). | `linux-research/appliance-harness/truenas-scale/runs/20260725T160000Z/evidence/` (OZ-4 host); `…/runs/20260725T190000Z/evidence/` (OZ-4 OCI round 1); `…/runs/20260725T193000Z/evidence/` (OZ-4 OCI round 2); `…/runs/20260726T045000Z/evidence/` (OZ-4 OCI round 3, zero-wire kernel panic); `…/runs/20260724T183838Z/evidence/` (OZ-1/OZ-2) | | **Unraid 7.3.0** | **No** — a default Docker container is not passed `/dev/zfs`. A `--device /dev/zfs` container receives the node but Docker's default capability set omits `CAP_SYS_ADMIN`, so `ZFS_IOC_POOL_GET_HISTORY` returns `EPERM` (OZ-1 blocked through that path); a `--privileged` container receives all capabilities and reproduces OZ-1 (`rc=0`, full pool history). (The initial runc `pivot_root` failure was traced to the initramfs rootfs and resolved.) | **BLOCKED** in the default and `--device` containers; **VIABLE** with `--privileged` (or `--cap-add SYS_ADMIN`). Separately confirmed on the host from a non-root nested-userns process (`rc=0` against `testpool`). | **VIABLE** — `zpool attach -f attackerpool /tmp/attacker.img /dev/zvol/testpool/victim-oz2` attached the victim zvol as a mirror leg (came up ONLINE rather than DEGRADED). | **BLOCKED** by default and `--device /dev/zfs` (no `/dev/zfs` / no `CAP_SYS_ADMIN`); **VIABLE** from `--privileged` — confirmed dynamically on `6.18.29-Unraid` / OpenZFS `2.4.1-1`. Importing the inflated-bonus pool (`dn_bonuslen=0x1000`, `zfs-77-d77-s3v3.img`) from a `--privileged` Docker container fails `insufficient replicas` while the structurally identical clean control imports ONLINE; the kernel survives (silent heap corruption, no panic/Oops). Root shell not pursued (requires heap grooming + KASLR bypass). **OCI delivery VALIDATED** — a self-contained malicious image (`oz4-oci:latest`) carrying both pool images was `docker load`ed and fired the same differential end-to-end via `--privileged` + identical-path bind; the negative control showed the bind is unnecessary under Unraid's `vfs` storage driver (the image-internal staging path is kernel-reachable), so `--privileged` is the sole active ingredient. Note: a `--privileged` container holds full host capabilities by design, so this is **expected behavior, not a finding** — it is recorded for matrix completeness and carries no CVSS row. Full verdicts: `linux-research/.fleet-coord/oz4-unraid-verdict.md`, `linux-research/.fleet-coord/oz4-oci-unraid-verdict.md`. | `linux-research/appliance-harness/unraid/runs/20260725T150925Z/evidence/` (OZ-4); `…/runs/20260725T194343Z/evidence/` (OZ-4 OCI); `…/runs/20260725T111104Z/evidence/` (OZ-1/OZ-2) | | **IncusOS 202607192224** | **Yes** — default unprivileged Incus container receives `/dev/zfs` as `crw-rw-rw- 10:249`. | **VIABLE** — container root reads host pool history; cross-tenant dataset replacement demonstrated. | **VIABLE** — cross-tenant custom-volume destroy/clone demonstrated. | **VALIDATED (round 2, zero host-side setup)** — a malicious OCI image launched as a default unprivileged container (default profile only) places its armed pool in its own rootfs, which exists on the host at the predictable path `/var/lib/incus/storage-pools/local/containers/<ct>/rootfs/`; a **non-root uid 1000** (authorized via an image-controlled `cap_sys_admin+ep` file capability) issues a raw hand-packed `ZFS_IOC_POOL_TRYIMPORT` (libzfs bypassed) at that host path, and the host kernel opens the vdev → `dbuf_read_bonus` overflows `kmalloc-512` → the host ZFS load path and storage stack **wedge** (reproduced on a fresh kernel; `incusd` unresponsive). The clean control imports in <1s. On this kernel the overflow surfaces as a wedge/storage-DoS rather than TrueNAS's GPF-panic (heap-layout dependent). Round 1 (container-relative path) was BLOCKED. Full verdicts: `linux-research/.fleet-coord/oz4-oci-incusos-verdict.md` (round 1), `linux-research/.fleet-coord/oz4-oci-incusos2-verdict.md` (round 2); PoC `linux-research/incusos-spike/oz4-tryimport.c`. | `linux-research/incusos-spike/runs/20260726-oz4-oci2/evidence/` (OZ-4 OCI round 2); `…/runs/20260725-oz4-oci/evidence/` (OZ-4 OCI round 1); `…/runs/20260724-perm-pristine/` (OZ-1/OZ-2) | The OZ-4 values for **Proxmox VE 8.x**, **TrueNAS SCALE 25.04.2.4**, **Unraid 7.3.0**, and **IncusOS** are confirmed by the fleet runs (`.fleet-coord/oz4-proxmox-verdict.md`, `.fleet-coord/oz4-truenas-verdict.md`, `.fleet-coord/oz4-unraid-verdict.md`, `.fleet-coord/oz4-oci-incusos-verdict.md`, `.fleet-coord/oz4-oci-truenas-verdict.md`, `.fleet-coord/oz4-oci-truenas2-verdict.md`, `.fleet-coord/oz4-oci-unraid-verdict.md`). All four are **BLOCKED** by default for the OZ-4 end-to-end fire without any container privilege or host path; Proxmox VE and Unraid are **VIABLE** from delegated/privileged containers, and TrueNAS SCALE is **VIABLE** from the host-side import path. The **malicious OCI image delivery vector is VALIDATED with zero host-side setup on both IncusOS and TrueNAS SCALE** (the self-referential host-rootfs path makes the image's own payload host-resolvable by construction; a default unprivileged container's **non-root** user fires the overflow via a raw `ZFS_IOC_POOL_TRYIMPORT`). On **TrueNAS SCALE** the zero-wire armed import **panics the production kernel** (GPF in `__kmalloc` on the `spa_load` path; crashing task host `UID: 2147001001`); on **IncusOS** it **wedges the host ZFS load path and storage stack** (`incusd` unresponsive), reproduced on a fresh kernel — the panic-vs-wedge difference is heap-layout dependent, the primitive is the same. On Unraid the OCI vector also fires, but only from `--privileged` — **expected behavior for a fully privileged container, not a finding**. No tested appliance remains BLOCKED for the OCI delivery vector. #### RHEL 10.2 STIG + FIPS + SELinux enforcing (OpenZFS 2.2.10-1 kABI kmod) Tested 2026-07-24/25 on a custom Image Builder RHEL 10.2 guest (kernel `6.12.0-211.39.1.el10_2`, FIPS mode enabled, OpenSCAP STIG profile applied, SELinux `Enforcing`/targeted, OpenZFS `2.2.10-1` kABI kmod, user namespaces enabled, `fapolicyd` active, `/tmp`+`/var/tmp` `noexec`, no udisks2/automount). This is the **non-default configuration** relevant to hardened RHEL estates. Full evidence: `linux-research/rhel/oz12-rhel10-findings.md` (context matrix run in both Enforcing and Permissive for clean attribution, SELinux policy enumeration, and podman runtime scenarios). **Per-finding verdicts on this configuration:** | Finding | Verdict | Blocking mechanism | |---|---|---| | OZ-1 (userns pool-ioctl authorization bypass) | **BLOCKED** | Kernel/OpenZFS credential check — all pool ioctls (`POOL_GET_HISTORY`, `CHANNEL_PROGRAM`, `POOL_CREATE`, `POOL_DESTROY`) return `EINVAL` for namespace-local-CAP_SYS_ADMIN callers in both Enforcing and Permissive. SELinux is *not* the blocker here; the 2.2.10 build's check is strict (initial-namespace authority required). | | OZ-2 (vdev-open DAC bypass) | **BLOCKED** | Same kernel credential check (`POOL_CREATE` over a root-only `0700` vdev returns `EINVAL`); victim vdev DAC correctly returns `EACCES` on direct open. | | OZ-3 / OZ-4 and the parser family | **NOT VALIDATED on 2.2.10** | The parser findings were confirmed on OpenZFS `2.4.99` master; RHEL's kmod is 2.2.10 — code-equivalence per finding was not established. Delivery is additionally absent on this host: pool import/management requires the same ioctls the credential check blocks, and there is no udisks2/automount mount-time path (loop devices are `root:disk 0660`, test user not in `disk`). | **Which mechanisms *did* prevent (ordered by where the attack actually stops):** 1. **Kernel / OpenZFS credential model** — the primary control on this build: unauthorized userns callers get `EINVAL` at the ioctl. This is what stops OZ-1/OZ-2 even for `unconfined_t` host users, where SELinux imposes nothing. 2. **SELinux `container_t` device gate** — `/dev/zfs` is `system_u:object_r:device_t`; the only policy path from `container_t` is `allow container_domain device_node:chr_file {...} [container_use_devices]`, and `container_use_devices --> off` on the target. Verified by toggling the boolean: off → `open("/dev/zfs")` fails `EACCES`; on → succeeds; off → fails again. A containerized attacker is stopped at the device-open step in Enforcing mode, before any ioctl. 3. **fapolicyd** (STIG) — independently blocks execution of untrusted binaries by non-root users; stops the attack before SELinux or ZFS is reached. (Stopped temporarily during testing to isolate attribution.) 4. **DAC** — on vdev files (direct open `EACCES`) and on `/dev/zfs` ownership (mode 666, so not a barrier for uid — by design; see `/dev/zfs` exposure discussion in this report). 5. **Container runtime + device cgroup** — default podman exposes no `/dev/zfs`; `-v /dev/zfs:/dev/zfs` (preserving the host `device_t` label) fails with `EPERM` from the device cgroup, not SELinux; `--device` works but **relabels** the node to `container_file_t` (a runtime decision, not a policy weakness). 6. **STIG mount options** — `/tmp` and `/var/tmp` `noexec` blocked the initial PoC delivery path (binary had to be staged via `podman cp` into the container rootfs instead). **How SELinux does and does not prevent (the nuanced answer):** - **Does:** gate the *first* contact for confined domains — `container_t` (and therefore default podman/docker containers and OpenShift restricted SCCs) cannot open `/dev/zfs` while `container_use_devices` is off. Policy enumeration (4,718 types) shows **no hidden path**: no transition out of `container_t` to any domain with `device_t` access, no unconditional device rule for it, and the only domains with unconditional `device_node` access (`container_device_t`, `container_device_plugin_t`, `spc_t`) require an explicit administrator/runtime/SCC decision. - **Does not:** mediate ioctl-level authorization. Once a process can open `/dev/zfs` — and every unconfined host user can, in Enforcing — SELinux has no say over which ZFS ioctls succeed. The control that decides OZ-1/OZ-2 there is the kernel/OpenZFS credential check. SELinux also does not constrain `unconfined_t` (host users), does not stop `spc_t`/privileged containers (by design), does not prevent an administrator from enabling `container_use_devices` or passing `--device`, and offers no protection against the OZ-3+ parser family should any import/replay delivery path open up. - **Net:** on this exact configuration SELinux is defense-in-depth, not the mitigation. If a vulnerable OpenZFS build (with the OZ-1 namespace-local-`CAP_SYS_ADMIN` acceptance) were installed, the kernel-side precondition would fail for *unconfined host users too* — and SELinux's device gate would become the *decisive* control for containerized attackers only. Keep it Enforcing, and audit `container_use_devices` and privileged-SCC usage as the effective exceptions list. These verdicts raise the severity of the headline chain on TrueNAS SCALE (default unprivileged container satisfies the `/dev/zfs` + nested-userns precondition) and reduce it on Proxmox VE and Unraid (precondition not met by default). They confirm that explicit administrative passthrough makes the same authorization flaws viable on Proxmox VE and Unraid, and that OZ-1/OZ-2 are present and reachable on Unraid 7.3.0. ### Untested / static-only candidates The products below have **not** been dynamically reproduced; they are triage candidates only. | Operating system / product | Documented ZFS integration | Relevance & open questions | |---|---|---| | **Proxmox Backup Server** | Installer supports ext4/XFS/ZFS; ships a Proxmox kernel with ZFS. ([installation]( https://pbs.proxmox.com/docs/installation.html), [ZFS sysadmin]( https://pbs.proxmox.com/docs/sysadmin.html#zfs-on-linux)) | High availability impact if the backup pool is hit. Default installer selection is not ZFS; no low-privilege `/dev/zfs` exposure identified. Verify package revision and automated import/maintenance inputs. | | **Ubuntu / Ubuntu-derived** | ZFS in default kernel packages; `zfsutils-linux`. ([Ubuntu ZFS](https://wiki.ubuntu.com/Kernel/Reference/ZFS), [OpenZFS Ubuntu]( https://openzfs.github.io/openzfs-docs/Getting%20Started/Ubuntu/index.html)) | Relevant only where the module is loaded and a pool is used. Ubuntu/LXD hosts deserve separate review (ZFS driver, zvol block mode, snapshots/clones, optional `zfs.delegate`); delegation enabled is not proof of the IncusOS bypass. | | **LXD with the ZFS storage driver** | LXD recommends ZFS and supports optional `zfs.delegate`. ([ZFS driver]( https://documentation.ubuntu.com/lxd/latest/reference/storage_zfs/), [init]( https://documentation.ubuntu.com/lxd/latest/howto/initialize/)) | High-priority when delegation is enabled. LXD docs say ordinary containers receive a minimal ephemeral `/dev` and do not list `/dev/zfs` among default devices, so the OZ-1 precondition should not be assumed for a default LXD container. | | **Debian** | `zfs-dkms` in `contrib` (SPA/DMU/ZVOL/ZPL). ([package]( https://packages.debian.org/trixie/zfs-dkms)) | Optional, not a default filesystem. Review package revisions and any container/device delegation by higher-level software. Proxmox treated separately. | | **NixOS** | Manual/modules support ZFS pools, boot-time import, ZFS root. ([manual](https://nixos.org/manual/nixos/stable/), [ZFS options]( https://nixos.org/manual/nixos/stable/options)) | Optional but deeply integrated when enabled. OZ-1 still requires a separately exposed `/dev/zfs`. | | **Alpine Linux** | Packages OpenZFS; documents auto import/mount and root-on-ZFS; Extended image ships modules. ([Alpine ZFS]( https://wiki.alpinelinux.org/wiki/ZFS), [OpenZFS Alpine]( https://openzfs.github.io/openzfs-docs/Getting%20Started/Alpine%20Linux/index.html)) | Optional on installed systems. An Alpine container guest is not itself affected unless the host exposes its OpenZFS device; the relevant kernel/module are the host's. | | **Fedora, RHEL/Rocky/Alma/CentOS, openSUSE, Arch, Gentoo, Slackware** | OpenZFS publishes install/root-on-ZFS guidance, generally via optional repos. ([distribution index]( https://openzfs.github.io/openzfs-docs/Getting%20Started/index.html)) | Affected only where OpenZFS is installed and loaded. Treat version, backport, Secure Boot/module-signing, pool-import automation, and `/dev/zfs` delegation as host-specific. | | **QNAP QuTS hero** | Linux-based ZFS NAS with pool/shared-folder/LUN/snapshot/clone/virtualization/container features. ([docs]( https://docs.qnap.com/operating-system/quts-hero/5.1.x/en-us/about-quts-hero-CAAE5DD0.html), [raw-CLI warning]( https://www.qnap.com/en-us/how-to/faq/article/why-should-i-avoid-using-raw-zfs-cli-commands-on-my-quts-hero-nas)) | **Static-first; source-validated from the `qosgpl` 5.2.0 CDDL tarball:** QZFS is an OpenZFS fork diverged ~2020, feature-frozen, with opaque backport discipline. **Z1, Z2, and dnode-bonus S3 are VALIDATED** (code shape present; Z1 is weaker than upstream — no `reclen > end - lrp` check); **gang overrun is VALIDATED (source)**; **log-spacemap S4 is BLOCKED** (feature entirely absent from the fork). Tenant-driven `ZFS_IOC_POOL_IMPORT` through Container Station remains plausible but unverified (container profile closed-source). Treat QNAP as a separate implementation; the dominant RCE surface on QNAP is the management plane, not the OpenZFS parser chain. Full static analysis: `linux-research/.fleet-coord/validate-qnap-qzfs-verdict.md`. | TrueNAS CORE, FreeBSD, illumos, and QNAP QES are not Linux and are not candidates for the Linux-specific user-namespace policy flaw. Some on-disk parser code is shared across OpenZFS platforms, so those projects may still need a separate source/runtime review; applicability must not be inferred from the Linux results. ### Suggested verification priority 1. **Proxmox Backup Server** — packaged revision and automated import/replay/ maintenance paths; default installer selection is not ZFS, so score OZ-1 only if a real low-privilege `/dev/zfs` path is found. 2. **Ubuntu/LXD** — default containers vs explicitly delegated ZFS volumes; determine exactly when the host device and pool namespace become reachable. 3. **QNAP QuTS hero** — static-first; Z1/Z2/dnode-bonus-S3 **VALIDATED**, gang **VALIDATED (source)**, log-spacemap S4 **BLOCKED** (feature absent). Build a QNAP QEMU harness to dynamically confirm Z1/Z2/dnode-bonus S3 on QuTS hero firmware, and determine whether Container Station exposes `/dev/zfs` to tenants before treating the upstream chain as reachable. 4. **General-purpose distributions** — parser-version review and automated import/replay surfaces; test OZ-1 only where a real low-privilege `/dev/zfs` path exists. 5. **Unraid delegated-container follow-up** — `--device /dev/zfs` alone is **BLOCKED** for OZ-1 (no `CAP_SYS_ADMIN`); `--privileged`/`--cap-add SYS_ADMIN` reproduces OZ-1. Survey Community Applications templates that request `--device /dev/zfs` or privileged mode to assess real-world exposure. ## Impact and CVSS scoring guidance This report contains independent root causes. They should receive **separate CVE records and separate scores**; one score for the entire document would obscure both the directly exploitable authorization failures and the different preconditions of the parser bugs. Accordingly, the tables below contain **one row per root cause** plus the single demonstrated chain; deployment and configuration variation is handled in [Environmental metrics](#deployment-environmental-modifiers), not by multiplying base-score rows. The vectors are **candidate vectors for coordinator review**, not assignments. They follow the [FIRST CVSS v3.1]( https://www.first.org/cvss/v3.1/specification-document) and [v4.0](https://www.first.org/cvss/v4.0/specification-document) definitions and were recomputed with the `cvss` Python library (v3.6). - `PR:L` means code execution by a non-root user in the tested default-profile unprivileged container, not host administrative access. - `PR:H` models direct host-administrator import/replay/mount/read. It is not a claim that every deployment requires host administration. - `S:C` / Subsequent System impacts model the container/tenant authority and the host or victim tenant as different scopes. A coordinator that treats them as one authority may choose `S:U`; the v3.1 `S:U` alternatives for OZ-1, OZ-2, and the headline chain are **7.3, 7.1, and 7.0** respectively. - `I:L` records a confirmed or source-certain kernel-memory write without upgrading it to complete host integrity. `C:N` is retained where an OOB read did not return data to the attacker. Source-only entries are provisional; evidence confidence must not be encoded by changing Attack Complexity unless the attack itself depends on an external condition. **Scoring the route, not multiplying rows.** A standalone parser bug reachable only through host-admin import/mount may warrant `PR:H`; the same parser reached through OZ-1 has the confirmed low-privilege precondition represented by `PR:L` — score that specific bug with `PR:L/S:C` rather than inventing a composite row. OZ-1's import route removes the `PR:H` administrator precondition of every import/replay parser row. OZ-1's dataset-replacement consequence can precede and arrange a later mount by a trusted consumer; raise chained impact only where the attacker can construct the required malformed state and the consumer predictably mounts/replays it — the current IncusOS evidence proves valid-dataset replacement and a later host mount, not a mount-time memory-corruption trigger. Do not combine multiple independent root causes into one CVE record, and do not use the in-tree administrator-mounted-image policy to assign `PR:H` mechanically to every OpenZFS finding. ### CVSS v3.1 candidate vectors | Finding | Candidate CVSS v3.1 vector | Score | Evidence and scoring boundary | |---|---|---:|---| | **OZ-1** (default unprivileged container exposing `/dev/zfs` to a non-root tenant who can create a nested user namespace) | `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:H` | **8.7 High** | Upstream ships `0666`; the authorization flaw treats namespace-local `CAP_SYS_ADMIN` as host-pool authority. End-to-end evidence shows unauthorized destroy/clone that removed victim data and substituted attacker content. No host-root shell, victim-file read, forced detach of an in-use volume, or pool-wide outage was demonstrated through this chain. | | **OZ-2** (foreign zvol attach and persistent mirror writes) | `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:H` | **8.4 High** | Dynamically confirmed on IncusOS, Proxmox VE (`zd0` degraded mirror leg), and Unraid 7.3.0. No victim read primitive; label destruction and continuing attacker-controlled writes support high integrity and availability. | | **Container escape to host root shell (headline chain):** OZ-4 reached through OZ-1's unprivileged import path, demonstrated in the research VM | `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H` | **7.8 High** | The uid-0 shell demonstrates complete host CIA from a non-root, container-equivalent position. `AC:H` reflects the required heap grooming and the disabled KASLR/SLUB freelist hardening. Not reproduced on production-hardened IncusOS. Chained scenario, not OZ-4's standalone base vector. *(Hypothetical ceiling, not demonstrated: if the OZ-4 exploit were reliable against a default-profile container, `AC:L` gives 8.8.)* | | **Malicious OCI image → host-kernel memory corruption/DoS (OZ-4, image-delivered):** default unprivileged container, non-root user, zero host-side setup | `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:H` | **7.3 High** | VALIDATED with zero host-side setup on TrueNAS SCALE (production-kernel panic; non-root host `UID: 2147001001`) and IncusOS (host ZFS load path / storage stack wedged, reproduced on a fresh kernel). `AC:L` — no heap grooming or race required; the un-groomed import fires reliably. `I:L` — kernel-memory corruption without a demonstrated controlled compromise; `A:H` — host kernel/storage DoS. `PR:L` models the default-container launch position; delivery requires the victim to run the attacker's image. Root-shell escalation not demonstrated on a production kernel. | | **OZ-4** alone as an administrator-import parser bug | `CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H` | **6.4 Medium** | The exploit establishes the impact ceiling; `PR:H` excludes OZ-1 and `AC:H` retains the demonstrated configuration/grooming dependencies. | | **OZ-3a** (ZIL record-length truncation) | `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H` | **4.4 Medium** | KASAN confirmed the ~4 GiB OOB read/fail-fault during replay. No bytes returned; no integrity consequence; `C:N/I:N`, `A:H`. | | **OZ-3b** (ZIL hole-write zero-fill) | `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H` | **5.1 Medium** | KASAN confirmed a 67,108,864-byte vmalloc OOB zero-write. `I:L` = kernel-memory modification without controlled host compromise. | | **OZ-3c** (ZIL xvattr stack overwrite) | `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H` | **5.1 Medium** | UBSAN confirmed array-index-out-of-bounds at `zfs_replay.c:115` (`index 3 is out of range for type 'uint32_t [3]'`) at mount from a crafted pool image; kernel panic. Previously source-confirmed; now VALIDATED dynamically. `I:L` = stack overwrite without demonstrated controlled host compromise. | | **OZ-5a** (gang-tree recursion) | `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H` | **4.4 Medium** | Dynamically confirmed stack-guard fault and panic from a valid depth-2048 chain. | | **OZ-5b** (gang-leaf leader-buffer overrun) | `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H` | **5.1 Medium** | KASAN confirmed attacker-controlled image bytes written 128 KiB past a 128 KiB ARC allocation before checksum rejection. Exploitation not attempted. | | **OZ-6** (log-spacemap index validation) | `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H` | **5.1 Medium** | KASAN confirmed NULL-ptr deref in `vdev_is_concrete` and slab-out-of-bounds `vdev_ms[]` selection/range-tree mutation during writable import. Previously source-confirmed; now VALIDATED dynamically. | | **OZ-7** (fatzap chain traversal) | `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:L` | **3.4 Low** | KASAN confirmed OOB accesses/write and a crafted cycle kept one kernel operation in an unbounded loop. No exfiltration, persistent host modification, panic, or system-wide outage. | | **OZ-8** (SPL cache-destroy race) | `CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L` | **1.9 Low** | Dynamically confirmed permanent uninterruptible sleep of the destroy thread. `AC:H` = required in-flight grow race; `A:L` = blocked teardown, not a host-wide outage. The untested live-object UAF is excluded. | ### CVSS v4.0 candidate vectors CVSS v4.0 retires Scope and splits impact into Vulnerable System and Subsequent System. Scores are base-score only; adjust with Environmental metrics per deployment. | Finding | Candidate CVSS v4.0 vector | Score | Notes | |---|---|---:|---| | **OZ-1** | `CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:H/SC:L/SI:H/SA:H` | **8.4 High** | Host-pool disclosure and destruction from a non-root tenant; low subsequent confidentiality, high subsequent integrity/availability. | | **OZ-2** | `CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:H/SA:H` | **8.3 High** | No confidentiality impact; high integrity/availability on the victim device. Verified on IncusOS, Proxmox VE, Unraid 7.3.0. | | **Container escape to host root shell (headline chain)** | `CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H` | **8.8 High** | `AC:H`/`AT:P` reflect the demonstrated heap grooming and disabled KASLR/SLUB hardening. | | **Malicious OCI image → host-kernel memory corruption/DoS (OZ-4, image-delivered)** | `CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N` | **6.9 Medium** | VALIDATED with zero host-side setup on TrueNAS SCALE (kernel panic, non-root issuer) and IncusOS (host storage-stack wedge, reproduced). No grooming required; no demonstrated controlled compromise (`VI:L`), host availability impact `VA:H`. | | **OZ-4** alone | `CVSS:4.0/AV:L/AC:H/AT:P/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H` | **8.7 High** | Even with `PR:H`, the demonstrated uid-0 shell keeps severity High in v4.0. | For the standalone OZ-3, OZ-5, OZ-6, OZ-7, and OZ-8 rows, begin with `AV:L/AC:L/AT:N/PR:H/UI:N` (or `AC:H/AT:P` where the attack depends on a race or heap placement) and carry forward only the demonstrated Vulnerable System impacts. Do not add Subsequent System impact without an observed cross-boundary consequence, and do not set high confidentiality or integrity merely because KASAN reported an OOB access. ### Deployment Environmental modifiers The base vectors above assume the `/dev/zfs` + nested-userns capability precondition is satisfied. These are deployment/configuration facts, scored with Environmental metrics (`MPR`, `MVC`, `MVI`, `MVA`, `MS`), **not** separate base vectors: - **TrueNAS SCALE 25.04.2.4** satisfies the precondition in a **default** unprivileged Incus container — the OZ-1 base vector applies directly. - **Proxmox VE** default unprivileged LXC and **Unraid** default Docker do **not** expose `/dev/zfs`; on those platforms the OZ-1/OZ-2 base vectors apply **only after an administrator adds explicit device passthrough** (Proxmox VE delegated LXC bind-mount + cgroup/AppArmor relaxation). This is a configuration choice, not a distinct vulnerability. - **Unraid Docker capability matrix (dynamically verified):** - `--device /dev/zfs` **alone** — node present, but Docker's default capability set omits `CAP_SYS_ADMIN`, so `ZFS_IOC_POOL_GET_HISTORY` returns `EPERM`. **OZ-1 blocked; not a security finding** (`CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:N` = **0.0**, recorded as an honest negative). - `--privileged` (or `--cap-add SYS_ADMIN`) — `/dev/zfs` present and all capabilities granted; `ZFS_IOC_POOL_GET_HISTORY` returned `rc=0` with full pool history. The same OZ-1 base vector applies; the precondition is an intentional privileged-posture choice. - On the Unraid 7.3.0 host, OZ-1 was also confirmed from a non-root nested user namespace, and OZ-2 by foreign-zvol mirror attach. Score the underlying authorization flaw with the base vector, then apply Environmental metrics to reflect the actual deployment's container-device and capability policy. Environmental metrics should raise priority for platforms that expose `/dev/zfs` to unprivileged containers where nested user namespaces are enabled, auto-import tenant storage, or host mutually distrusting tenants on zvols. ## OZ-1: Linux user-namespace pool-control authorization bypass ### Root cause `zfs_secpolicy_config()` in `module/zfs/zfs_ioctl.c:1344-1353` reaches the Linux policy implementation in `module/os/linux/zfs/policy.c:55,68-71`. The check is effectively: ```c ns_capable(cr->user_ns, CAP_SYS_ADMIN) ``` This asks whether the credential has `CAP_SYS_ADMIN` in the credential's own user namespace. A process that maps itself to uid 0 in a newly created user namespace satisfies that test even though it has no capability in the initial user namespace. Dataset operations have additional zone/delegation checks, but the pool-level handlers protected by `zfs_secpolicy_config()` do not consistently apply an equivalent pool-visibility or global-zone restriction. `spa_open()` also has no namespace visibility filter. ### Confirmed reachability In the KASAN research guest the harness deliberately changed the devtmpfs node to `0666` to test the upstream-open surface. A process starting as uid 1000 created a user namespace, mapped itself to uid 0, and invoked raw OpenZFS ioctls without any dataset delegation. Confirmed operations: `ZFS_IOC_POOL_FREEZE` and `ZFS_IOC_POOL_DESTROY` against a host pool; pool history and property operations; `ZFS_IOC_CHANNEL_PROGRAM`, including iteration over datasets not visible through the normal dataset ioctl gate and sync-task operations without the normal ioctl delegation policy; and import of an attacker-authored file vdev at a host-visible path. The same policy gap was confirmed on **stock IncusOS with OpenZFS 2.4.3** — the attacker confined to an unprivileged default-profile Alpine container (`uid 0 -> host uid 1000000`), no `security.privileged`, no ZFS delegation, with an accessible `0666` `10:249` `/dev/zfs` node. From that position: `POOL_GET_HISTORY` returned host pool history including the pool encryption key's host path; a channel program recursively enumerated the host dataset tree that normal zone-gated ioctls hid; `POOL_FREEZE` passed authorization (no observable write stall on this build); and channel-program sync tasks performed the cross-tenant volume swap described in [the chain](#the-chain-oz-1--oz-4) and [IncusOS deployment](#incusos-deployment). This run verifies OZ-1 and the dataset-replacement consequence; it does **not** reproduce the OZ-4 exploit under IncusOS production hardening. The gap was also reproduced on **TrueNAS SCALE 25.04.2.4** from a default unprivileged Incus-based container (both container root and a non-root user after `unshare -Ur` got `rc=0` from raw `ZFS_IOC_POOL_GET_HISTORY` against the host `boot-pool`), on **Proxmox VE 8.x** with `/dev/zfs` explicitly passed through to an otherwise unprivileged LXC container, and on **Unraid 7.3.0** (from a `--privileged` Docker container and from a non-root host process after `unshare -Ur`; a `--device /dev/zfs`-only container returned `EPERM` for want of `CAP_SYS_ADMIN`). Detailed mechanism and the wider ioctl inventory: [zone-gate analysis](../research/zfs-zonegate-bypass/README.md). ### Impact Host authorization-boundary failure when `/dev/zfs` is reached with namespace-local `CAP_SYS_ADMIN` by an unprivileged user namespace or container: host-pool integrity and availability can be affected; pool history and events can cross the namespace boundary; channel programs can bypass the dataset-ioctl visibility/delegation layer; and attacker-authored pool images can reach the import parser set when a usable host-visible path exists. No claim depends on a memory-corruption exploit — host-pool operations were accepted directly by the authorization policy. ### Recommended fix - Require `CAP_SYS_ADMIN` in the initial user namespace, an explicit global-zone check, or equivalent host-level authorization for global pool operations. - Apply a pool-visibility policy before `spa_open()` or centrally in the pool-modifying ioctl registration path, rather than auditing each handler independently. - Apply delegation and zone filtering inside channel-program dataset iteration and sync-task authorization. - Treat `/dev/zfs` exposure as privileged until these policy boundaries are enforced in-kernel. - As defense-in-depth, restrict the host node to root or a tightly controlled administrative group, deny the ZFS character-device major/minor to containers that do not intentionally receive ZFS delegation, and avoid granting containers `CAP_SYS_ADMIN`. ## OZ-2: vdev open bypasses caller DAC ### Root cause Pool-create and vdev-attach requests accept paths supplied by the caller, but the eventual kernel-side opens do not consistently enforce whether that caller could open the same object: block-device lookup/open checks device cgroups but does not perform the ordinary inode DAC check against the original caller; file vdev opens can run on taskq threads with kernel credentials and the initial mount namespace; and the userspace `zpool` preflight's failed `zero_label()` result is not a reliable security boundary because the raw ioctl remains available and the failure is discarded in the client path. Full path analysis: [zvol DAC report](../research/zvol-dac-bypass/README.md). ### Dynamic evidence >From userns root with no delegation: direct userspace reads of a root-owned mode-0600 zvol failed with `EACCES`; raw `ZFS_IOC_POOL_CREATE` using that zvol as a vdev succeeded and overwrote label regions; file vdevs behind mode-0700 root-owned directories also opened; and `ZFS_IOC_VDEV_ATTACH` attached a foreign zvol as a mirror leg of an attacker-controlled pool. After attach, writes to the attacker pool were mirrored into the foreign zvol — nineteen of twenty test patterns observed on the victim device, none on the hidden-zvol control ([VDEV_ATTACH report](../research/zfs-vdevattach/README.md)). The same mirror-leg behavior was reproduced on Proxmox VE 8.x (`/dev/zd0`, DEGRADED), TrueNAS SCALE 25.04.2.4 (`/dev/zvol/boot-pool/victim-oz2`, DEGRADED), and Unraid 7.3.0 (`/dev/zvol/testpool/victim-oz2`, ONLINE). This is a storage-manager and block-device pathing vulnerability, not a mount-time filesystem-parser bug: ZFS accepted a path naming another block-like object, opened it in the kernel despite the caller's direct-access failure, and used normal mirror semantics to persist attacker-controlled writes. The victim zvol could itself back a VM or another filesystem; it need not contain or mount ZFS. ### Recommended fix - Carry the ioctl caller's credentials to vdev open and require the same permission that a direct caller-side open would require. - Make failed label-open/zero operations fatal in libzfs preflight. - Combine with OZ-1's host-level pool-operation gate; either fix alone leaves another unsafe path. ## OZ-3: ZIL replay record validation ZIL replay allocates a 32 MiB scratch buffer and consumes records from attacker-authored, checksum-valid log blocks. Checksums are unkeyed and therefore do not establish trust in a foreign pool image. ### OZ-3a: record-length truncation In `module/zfs/zil.c:501,548-549`, the on-disk `uint64_t lrc_reclen` is assigned to a signed `int`, so bounds checks validate only the truncated value. `zil_replay_log_record()` later reloads the original full-width field and passes it to `memcpy()` at `zil.c:4702,4734`. A record length of `0x1_0000_0040` passes as 64 bytes in `zil_parse()` and becomes ~4 GiB again in the copy. KASAN reported a slab-out-of-bounds read of 4,294,967,360 bytes during replay. ### OZ-3b: hole-write zero-fill For an inline-less `TX_WRITE` whose block pointer is a hole, `zil_read_log_data()` at `module/zfs/zil.c:316-335` zero-fills using an attacker-controlled length without bounding it to the remaining replay scratch buffer. KASAN reported a 67,108,864-byte vmalloc out-of-bounds write. ### OZ-3c: xvattr mask size `zfs_replay_xvattr()` in `module/zfs/zfs_replay.c:96-165` copies a record-controlled attribute mask into fixed `xva_reqattrmap[3]` storage. The relevant capacity assumption is enforced only by `ASSERT`, absent in release builds. Source-confirmed stack overwrite; a standalone dynamic trigger was not completed. Full source analysis, triggers, controls, and KASAN excerpts: [confirmed ZIL report](../confirmed/zfs-zil-replay/README.md). ### Recommended fix - Keep record lengths in a single unsigned type and reject any value larger than the remaining log block and replay scratch-buffer capacity. - Add type-specific runtime validators before copying or extending any replay record, analogous to the receive-record hardening at the audited revision. - Validate write length, block-pointer shape, ACL/xvattr sizes, and all variable arrays before dispatch. ## OZ-4: dnode bonus-dbuf heap overflow `dnode_create()` copies `dn_nblkptr`, `dn_bonuslen`, and slot metadata from disk without release-build runtime validation (`module/zfs/dnode.c:577-624`). In `dbuf_read_bonus()` (`module/zfs/dbuf.c:1425-1444`): `max_bonuslen` is 320 bytes for a one-slot dnode; `kmem_alloc(max_bonuslen)` allocates that fixed-size destination; and `memcpy(..., bonuslen)` uses an on-disk 16-bit length that can be much larger. Original static report: [zfs-dnode-bonus](../research/zfs-dnode-bonus/README.md). Later image-crafting proved an attacker controls the copy's content and length (up to ~64 KiB) from a 320-byte allocation in the `kmalloc-512` class, firing during pool import from the same userns/container-equivalent position as OZ-1. Corrected crafter mechanics and sanitizer evidence: [controlled-write report](../research/zfs-escape-chain/77-corruption-escape.md). ### Exploit development and demonstrated ceiling The weaponization campaign converted this overflow into a no-ROP `pipe_buffer` primitive: grow a dataset child ZAP to fatzap to remove a competing 512-byte scratch allocation; spray and hole-punch pipe rings so the bonus dbuf is allocated adjacent to a live `pipe_buffer` array; overwrite the adjacent array's `page`, `offset`, `len`, `ops`, and flags; use ordinary pipe reads/writes as kernel arbitrary read/write; replace `modprobe_path`; and trigger module autoload through an unregistered socket protocol family. The result and its hardening caveats are stated once in [the chain](#the-chain-oz-1--oz-4). Full exploit-development evidence: [77b report](../research/zfs-escape-chain/77b-exploit-poc.md). ### KASAN interpretation KASAN materially changes how this bug must be interpreted. On this kernel, `__asan_memcpy()` checks the entire range and skips the actual copy when the range crosses poisoned shadow. An earlier KASAN "Write of size 1064" therefore proved an attempted OOB copy but did **not** prove that controlled bytes were written; the subsequent report corrects that earlier conclusion. The completed exploit did not disable KASAN — the attempted `kasan=off` boot parameter did not reliably disable KASAN on this config and was ignored. Instead, the heap groom placed the victim pipe array in a same-slab span that KASAN considered addressable, so the real `memcpy` completed and the root-shell proof followed. On an ordinary non-KASAN production kernel the sanitizer cannot abort the copy, but KASLR and freelist hardening remain independent exploitation constraints. The proposed ZIL Z1 leak would provide the missing KASLR bootstrap from the same import surface, but it is **not verified**: the truncation forces a minimum copy of ~4 GiB and cannot be tuned to a small adjacent heap read; KASAN rejects that cross-redzone copy before any leaked bytes reach the replay buffer. Source inspection confirmed the same truncation in IncusOS's OpenZFS 2.4.3, but runtime verification on a non-KASAN, KASLR-enabled IncusOS appliance was blocked by test-appliance reinstall/TPM boot problems. This remains an escalation hypothesis, not evidence for a completed production KASLR bypass. ### Recommended fix Perform release-build validation of the entire dnode physical layout before publishing it: validate `dn_nblkptr`, `dn_extra_slots`, and `dn_bonuslen` together; reject any layout whose block-pointer and bonus regions exceed the dnode slot capacity; and use the validated allocation size as the copy bound. ## OZ-5: gang-block depth and leader-buffer bounds ### OZ-5a: unbounded recursion `zio_gang_tree_issue()` recursively walks the assembled gang tree at `module/zfs/zio.c:3011-3047` with no depth, node-count, or cycle limit. A crafted chain of 2,048 valid gang headers reached the stack guard page; the panic RIP and repeated frames were in `zio_gang_tree_issue`. ### OZ-5b: leaf overrun `zio_read_gang()` derives a child ABD at the current leader offset but issues the read using the constituent block's physical size. The ABD bookkeeping is clamped to the leader's remaining size, but the raw linear buffer pointer is not. The only check that constituent sizes sum to the leader size is an `ASSERT`. Inflating one leaf from 128 KiB to 256 KiB caused repeated 4 KiB KASAN writes up to 128 KiB past a 128 KiB `zio_buf_comb_131072` allocation; integrity verification occurred only after the bytes had been written. ([dynamic gang-block report](../research/zfs-gang-block/103-zfs-gang-dynamic.md)) ### Recommended fix - Impose runtime maximum depth and total-node limits while assembling and issuing gang trees; detect cycles or repeated header identities. - Before every leaf read, require `offset <= leader_size && psize <= leader_size - offset`. - Reject, rather than `ASSERT`, inconsistent total physical sizes. ## OZ-6: log-spacemap replay vdev and metaslab indices At writable import, `spa_ld_log_sm_cb()` in `module/zfs/spa_log_spacemap.c:1262-1316` trusts both the decoded vdev ID (dereferencing `vdev_lookup_top()` without checking for `NULL`) and `offset >> vd->vdev_ms_shift` as an index into `vd->vdev_ms[]` (without a runtime bound against `vd->vdev_ms_count`). The subsequent code dereferences the selected `metaslab_t *` and mutates its range trees. Space-map range checks are `ASSERT`-only, and log space maps are opened with `sm_size = UINT64_MAX`, which would make the nominal upper bound ineffective even in a debug build. Confirmed from source ([report](../research/zfs-log-spacemap/README.md)); no dynamic crash or corruption log is claimed. ### Recommended fix - Reject unknown/non-concrete vdev IDs before dereference. - Check the derived metaslab index against `vdev_ms_count`. - Give log space maps their real addressable range and convert on-disk range assumptions from `ASSERT` to runtime validation. ## OZ-7: fatzap chunk-chain validation Fatzap leaf traversal follows on-disk `la_next` chunk indices without a release-build range check and without cycle detection (`module/zfs/zap_leaf.c:338-366`). Two crafted variants were exercised: a cyclic chain repeatedly copied past the destination name buffer and remained in an in-kernel loop; and out-of-range indices were honored as further chunks outside the leaf buffer. KASAN reported slab out-of-bounds accesses in `zap_leaf_array_read()`; the cycle produced 1,810 multi-shot reports before harvest and remained runnable. ([dynamic report](../research/zfs-spl-zap-acl/104-dynamic-confirmation.md)) ### Recommended fix - Reject every chunk index outside the leaf's chunk count. - Bound traversal by the number of chunks and reject cycles. - Validate chunk type and the aggregate copied length before copying. ## OZ-8: SPL cache-destroy hang In `spl_kmem_cache_destroy()`: the recorded grow-work task ID remains `TASKQID_INVALID`, so cancellation does not identify the in-flight work; and destroy waits on a stack-local waitqueue that the grow completion path never wakes. The race was triggered dynamically — destroy entered with grow work in flight; the grow reference later returned to zero, but the destroy thread remained in uninterruptible `D` state for the duration of the run; an unraced control returned immediately. The release-build destroy-with-live-objects UAF hypothesis was not pursued and is not claimed. ([dynamic SPL report](../research/zfs-spl-zap-acl/104-dynamic-confirmation.md)) ### Recommended fix - Store and cancel the actual grow-work task ID. - Wait on the cache's shared waitqueue, or explicitly wake the waitqueue used by destroy when grow references change. - Make outstanding-object/refcount checks runtime-enforced during teardown. ## Suggested remediation order 1. **OZ-1 and OZ-2:** restore the Linux privilege boundary and caller access checks. These reduce immediate cross-tenant impact and return parser findings to an administrator/import threat model. 2. **OZ-3, OZ-4, OZ-5, and OZ-7:** add centralized runtime validation before replaying or traversing attacker-controlled metadata. 3. **OZ-6:** reject invalid log-spacemap vdev/metaslab coordinates during import. 4. **OZ-8:** correct teardown work cancellation and waitqueue ownership. Where practical, malformed-pool tests should run against release-mode modules; many of the affected invariants currently exist only as debug `ASSERT`s. ## Evidence index - ZIL replay: [confirmed report](../confirmed/zfs-zil-replay/README.md) and [validation logs](../confirmed/zfs-zil-replay/logs/) - Dnode bonus overflow: [source report](../research/zfs-dnode-bonus/README.md), [dynamic controlled-write follow-up](../research/zfs-escape-chain/77-corruption-escape.md), and [root-shell exploit-development report](../research/zfs-escape-chain/77b-exploit-poc.md) - Gang blocks: [source report](../research/zfs-gang-block/README.md) and [dynamic follow-up](../research/zfs-gang-block/103-zfs-gang-dynamic.md) - Log spacemap: [source report](../research/zfs-log-spacemap/README.md) - fatzap and SPL: [source/refutation report](../research/zfs-spl-zap-acl/README.md) and [dynamic follow-up](../research/zfs-spl-zap-acl/104-dynamic-confirmation.md) - Linux authorization: [userns gate](../research/zfs-gate/README.md), [pool zone-gate analysis](../research/zfs-zonegate-bypass/README.md), and [IncusOS end-to-end result](../research/incusos-containers/35b-incusos-e2e.md) - Vdev caller access: [pool-create/DAC result](../research/zvol-dac-bypass/README.md) and [mirror amplification](../research/zfs-vdevattach/README.md) ## Coordination checklist - [ ] Re-test against the upstream-selected supported branch/revision. - [ ] Split CVE assignments by independent root cause as requested by upstream. - [ ] Provide private reproducers and full serial logs through the coordinated disclosure channel. - [ ] Agree on whether OZ-1/OZ-2 are Linux-port defects or documented behavior requiring a deployment-level change. - [ ] Verify fixes with malformed-input regression tests and release-mode modules.
