jail(2) id >= 100000 shares per-prison sysctl nodes (DF-0054 truncation at scale): silent loss of all per-jail capability controls + sysctl_oid freed-under-reference UAF at teardown
| Field | Value |
|---|---|
| ID | DF-2802 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:H |
| CWE | CWE-416 (enabled by CWE-197 truncation) |
| File | sys/kern/kern_jail.c |
| Lines | 992-994 (name), 828β835 (teardown ordering), kern_sysctl.c:375-376/:446-455 |
| Area | kern |
| Confidence | likely |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
From the 100000th jail(2) since boot, prison ids are 6 digits but prison_sysctl_create names the sysctl node with ksnprintf(id_str, 6, ...) β every 10-id band shares one node name. sysctl_add_oid shares the existing node (oid_refcnt++) and all 11 per-jail capability leaf adds fail ("can't re-use a leaf"): the jail silently loses every per-jail control. The shared node is referenced by multiple sysctl_ctx_lists, and sysctl_ctx_free's dry run consumes an oid_refcnt it must not: each sharer teardown decrements twice but added once, so the sysctl_oid + children are kfree'd while other prisons still reference them β UAF reads/writes in M_SYSCTLOID on every later sharer teardown. Evidence: flood begins exactly at id 100000 (78,760 console lines by ~114k jails); ids proven strictly unique over 5000 logged assignments; serialized churn produces no flood (overlap required); 200k-iteration churn left the guest permanently wedged.
Threat model & preconditions
Host-root jail churn is routine (service jails, CI); after 100k cumulative creations every subsequent jail has broken capability management and each teardown operates on freed/reused sysctl oids β latent kernel heap corruption plus demonstrated whole-guest wedge.
Proof of contest
VERIFIED (findings/poc/DF-2802/): stock flood starts exactly at id 100000; 200k run wedged the guest permanently (24,896-line flood then silent kernel). Patched (#1 kernel with id_str[12] + dry-run guard + create-before-publish): 105k churn, ZERO flood lines, sysctl -a rc=0, guest healthy.
Recommended fix
Render ids fully (id_str[12] + sizeof), stop the dry run from consuming
references (if (del) oid_refcnt--), plus the DF-2799
create-before-publish ordering β diff in the pack.
References
- DF-0054 (the underlying truncation, pass 1), DF-2799, DF-2737
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_jail.c (GLM 5.3); at-scale flood + wedge reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2802 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| jailchurn.c | β | 2.5 KB | view raw | |
| jailtrace.c | β | 1.1 KB | view raw | |
| wedge_console.txt | β | 1.4 KB | view raw | |
| flood_evidence.txt | β | 406 B | view raw | |
| jids.log | β | 155.2 KB | view raw | |
| storm1_run.log | β | 4.0 KB | view raw | |
| run.patched.log | β | 172 B | view raw | |
| env.txt | β | 373 B | view raw | |
| fix.diff | β | 5.1 KB | view raw | |
| verdict.json | β | 5.5 KB | view raw |
Fix verification
fixedPatched kernel: 105,000-iteration churn crossing id 100000 produced zero 'can't re-use a leaf' lines (stock: 78,760 within ~90s of crossing), churn completed, sysctl -a rc=0, zero leftover jail.
findings/poc/DF-2802/run.patched.log; fix.diff (kern_jail.c id_str + kern_sysctl.c dry-run guard + ordering)
Confirmed kernel references
Detail
Exploit chain
host-root jail churn (routine on jail-hosting/CI hosts over machine lifetime): cumulative jails >= 100000 -> every new jail shares its truncation-band node -> silent per-jail capability-management loss for all later jails + shared-node refcount undercount at teardown -> freed-under-reference sysctl_oid -> later sharer teardowns read/write freed M_SYSCTLOID memory (latent kernel heap corruption, root-gated trigger).
Evidence (decisive lines)
['findings/poc/DF-2802/flood_evidence.txt β flood onset at id 100000 crossing, 78,760 total lines, jid uniqueness', 'findings/poc/DF-2802/jids.log β 5000 assigned jids, zero duplicates', 'findings/poc/DF-2802/wedge_console.txt β storm1 permanent wedge: 24,896-line console flood then silent kernel, ssh dead, QEMU alive', 'findings/poc/DF-2802/storm1_run.log β storm1 driver log', 'findings/poc/DF-2802/run.patched.log β fixed kernel: 105k churn, zero flood since reboot, sysctl -a rc=0, guest healthy']
PoC changes
jailtrace.c added (serialized churn logging the kernel-assigned jid of every jail) to prove id uniqueness and the concurrency requirement; flood counting switched to timestamp-filtered /var/log/messages because the log persists across reboots.
Verified recommended fix
id_str[12] with ksnprintf(id_str, sizeof(id_str), ...) so all ids < JAIL_MAX render uniquely; sysctl_remove_oid_locked must not consume oid_refcnt when del==0; create the prison tree before publishing the prison.
Verdict
REPRODUCED (sharing primitive certain; teardown UAF code-certain; wedge demonstrated in combination). From the 100000th jail(2) since boot, prison ids are 6 digits but prison_sysctl_create formats the sysctl node name with ksnprintf(id_str, 6, ...) (kern_jail.c:992-994, DF-0054), so ids 100000-100009 all name their node '10000' etc. sysctl_add_oid shares the existing node (oid_refcnt++, kern_sysctl.c:446-455) and all 11 per-jail capability leaf adds fail ('can't re-use a leaf' x11) β the prison silently loses every per-jail control. Evidence on stock: concurrent churn crossed id 100000 within ~90s and flooded the console with 78,760 leaf lines by ~114k jails; instrumented run proved ids 108261-113260 are strictly unique (0 duplicates), i.e. collisions are purely truncation; a serialized 5000-jail run (full teardown between creates) produced only +11 flood lines vs +5500 expected, proving sharing requires the create-vs-teardown overlap (prison_free releases jail_lock at kern_jail.c:828 BEFORE prison_sysctl_done at :835). The security-critical half: sysctl_ctx_free() dry-runs each ctx entry with del=0 but sysctl_remove_oid_locked() decrements oid_refcnt even in the dry run (kern_sysctl.c:375-376), so a shared node loses 2 references per sharer teardown while each sharer added 1 β the sysctl_oid and its children list are kfree'd while up to 9 other prisons' pr_sysctl_tree/ctx entries still reference them (use-after-free reads/writes in M_SYSCTLOID on every later sharer teardown; allocator-reuse can smash live oids). A 200k-iteration churn left the guest permanently wedged (console flood then silence, ssh dead) β attribution between this corruption and DF-2799's cycle could not be made unique in that run because it contained interactive sysctl -a reads; the post-quiesce tree of a 114k run happened to settle clean, so the UAF is stochastic rather than deterministic. On the patched kernel (id_str[12] full-length names + dry-run refcnt guard + create-before-publish ordering) a 105,000-iteration churn crossed id 100000 with ZERO flood lines, sysctl -a returned rc=0 and the guest stayed healthy.
No comments yet.