Heap buffer overflow in sysctl_jail_list (kern.jail.list) via unsigned underflow in size arithmetic
| Field | Value |
|---|---|
| ID | DF-0053 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write; CWE-190 Integer Overflow or Wraparound |
| File | sys/kern/kern_jail.c |
| Lines | 671 (types), 688 (alloc), 704-710 (ksnprintf+jlsused), 733-741 (IP loop) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-06-29 |
| Reported | pending |
Escalation flag: High-severity, kernel heap overflow reachable by any unprivileged local user via a sysctl read. Recorded
new; coordinated disclosure per the 90-day embargo.
Summary
sysctl_jail_list() sizes its output buffer as count * 1024 bytes (one
1024-byte budget per jail) but the per-jail formatted output ("%d %s %s" =
pr_id + pr_host + fullpath) can reach ~1287 bytes (6 + 256 + 1023).
ksnprintf returns the would-be (untruncated) length, so jlsused += count
makes jlsused exceed jlssize. On the next write (jlssize - jlsused)
underflows (both are unsigned int, :671) to ~UINT_MAX, which is
zero-extended to size_t for ksnprintf; ksnprintf/snprintf_func then
writes the full formatted string starting at jls + jlsused โ past the buffer
end โ into adjacent kernel heap. The IP loop (:733-741) has the identical
defect. Reachable by any unprivileged, unjailed user via sysctl kern.jail.list
(the only check is jailed()==0 at :679).
Root cause
unsigned int jlssize, jlsused; /* :671 unsigned -> underflow */
...
jlssize = (count * 1024); /* :688 1024 per jail */
jls = kmalloc(jlssize + 1, M_TEMP, M_WAITOK | M_ZERO);
...
count = ksnprintf(jls + jlsused, (jlssize - jlsused), /* :704 ksnprintf returns would-be len */
"%d %s %s", pr->pr_id, pr->pr_host, fullpath);
...
jlsused += count; /* :710 adds would-be len -> can exceed jlssize */
...
/* IP loop: */
if ((jlssize - jlsused) < (strlen(oip) + 1)) /* :733 unsigned underflow -> huge -> false */
...ERANGE...
count = ksnprintf(jls + jlsused, (jlssize - jlsused), " %s", oip); /* :737 huge size */
jlsused += count; /* :741 */
With a jail whose fullpath exceeds ~770 bytes, the first jail's formatted
output (~1287) exceeds the 1024-byte budget. jlsused jumps to 1287 (> jlssize
= 1024). The IP loop's (jlssize - jlsused) = (1024 - 1287) = unsigned
underflow โ ~4 billion โ the bounds check at :733 (huge < small = false, does
not stop) โ ksnprintf writes the IP string at jls + 1287 (287 bytes past the
1025-byte allocation) โ heap overflow.
Threat model & preconditions
- Attacker position: any local unprivileged, unjailed user.
- Privileges gained or impact: kernel heap overflow. The overflow content is
the jail's formatted IP/hostname/path (set by root at
jail(2)time โ not attacker-shaped in the default case, but deterministic). Reliable kernel panic (local DoS); with heap grooming and a known jail config, potentially local privilege escalation. The overflow length grows with the number of long-path jails. - Required config or capabilities: at least one jail with a
cache_fullpath()output exceeding ~770 bytes (a deep chroot path created by root โ realistic for container/hosting setups). - Reachability:
sysctl kern.jail.list(orsysctlbyname) as any unprivileged, unjailed user.
Proof of concept
PoC source: findings/poc/DF-0053/jail_list_overflow.sh
Phase 1 (root): create a jail with a deep chroot path (>770 chars). Phase 2
(any user): sysctl kern.jail.list โ heap overflow.
Run
sh findings/poc/DF-0053/jail_list_overflow.sh
Expected output
Kernel panic (heap corruption / slab assertion) or silent heap corruption.
Impact
Kernel heap overflow from an unprivileged sysctl read โ a serious memory- corruption defect. The trigger is trivial (read a sysctl); the precondition (a long-path jail) is realistic on jail/container hosts. High.
Recommended fix
Clamp jlsused to jlssize whenever truncation occurs, and bounds-check before
every write:
--- a/sys/kern/kern_jail.c
+++ b/sys/kern/kern_jail.c
@@ -710 +710,5 @@
- jlsused += count;
+ if (count >= (int)(jlssize - jlsused))
+ jlsused = jlssize;
+ else
+ jlsused += count;
@@ -733 +737,5 @@
- if ((jlssize - jlsused) < (strlen(oip) + 1)) {
+ if (jlsused >= jlssize ||
+ (jlssize - jlsused) < (strlen(oip) + 1)) {
(applying the same clamp at the IP-loop jlsused += count at :741).
References
sys/kern/kern_jail.c:671โunsigned int jlssize, jlsused.sys/kern/kern_jail.c:704-710,737-741โ the would-be-length + underflowing size.- CWE-787 Out-of-bounds Write; CWE-190 Integer Overflow or Wraparound.
Timeline
- 2026-06-29 Discovered during automated file-by-file audit of
sys/kern/kern_jail.c. - pending Reported to DragonFlyBSD security contact.
PoC verification
Evidence pack
findings/poc/DF-0053 ยท 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | Reproduce instructions + impact summary | 4.7 KB | โ raw |
| VERDICT.md | verdict | Full narrative: reproduced? mechanism path:line, exploit chain, PoC changes | 8.5 KB | โ raw |
| jail_list_trigger.c | trigger-source | Unprivileged sysctl(jail.list) reader; reports OOB read/write byte counts | 4.9 KB | view raw |
| setup_jail_v3.sh | setup-script | Root phase 1: build deep-path jail (host=255 + path=967 + 4 IPs) | 1.7 KB | view raw |
| setup_jail_v2.sh | setup-script | Earlier setup helper (superseded by v3) | 1.8 KB | view raw |
| setup_jail.sh | setup-script | Original setup helper from initial PoC scaffolding | 1.7 KB | view raw |
| jail_list_overflow.sh | trigger-script | Original shell-based trigger (python fallback) | 7.3 KB | view raw |
| build.sh | build-script | cc -O2 -o jail_list_trigger jail_list_trigger.c | 185 B | view raw |
| run.sh | run-script | Two-phase orchestrator (setup as root, trigger as user) | 1.1 KB | view raw |
| build.log | build-log | Final successful build output | 13 B | view raw |
| run.log | run-log | Decisive run: BUG CONFIRMED with byte accounting | 1.2 KB | view raw |
| run.3x.log | run-log | Three back-to-back runs (byte-identical; deterministic) | 3.8 KB | view raw |
| leak_sample.txt | leak-sample | OOB tail bytes across runs | 1.4 KB | view raw |
| env.txt | environment | uname, cc version, sysctl jail.list, jls | 2.3 KB | view raw |
| dmesg.txt | dmesg | Full guest dmesg (no slab warnings; INVARIANTS only catches UAF) | 6.4 KB | view raw |
| fix.diff | suggested-fix | git-apply-able fix: clamp jlsused<=jlssize at :710/:741 + underflow-proof guard at :733 (supersedes finding proposal) | 1.4 KB | view raw |
| fix_notes.md | fix-notes | Root-cause recap, what each hunk does, why this supersedes the finding's 2-hunk proposal | 3.3 KB | โ raw |
DF-0053 โ PoC (master DEV re-verification)
Status: REPRODUCED on DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT (built
2026-06-29, x86_64, X86_64_GENERIC config with INVARIANTS).
The prior not_reproduced verdict was an OID-name typo: the sysctl is
jail.list (top-level, SYSCTL_OID(_jail, OID_AUTO, list, ...) at
sys/kern/kern_jail.c:757), NOT kern.jail.list. With the correct OID the
bug fires immediately.
The bug
sysctl_jail_list (sys/kern/kern_jail.c:661) sizes its output buffer as
count * 1024 bytes (one 1024-byte budget per jail) but the per-jail
formatted output ("%d %s %s" = pr_id + pr_host + fullpath,
kern_jail.c:704-706) can reach ~1282 bytes (MAXHOSTNAMELEN=256 +
cache_fullpath MAXPATHLEN=1024 + small jid). ksnprintf returns the
would-be (untruncated) length (sys/kern/subr_prf.c:549 retval++ is
unconditional; snprintf_func at :494 only writes while remain>=2), so
jlsused += count (kern_jail.c:710) makes jlsused exceed jlssize. The
IP loop's bounds check (jlssize - jlsused) (:733) then underflows (both
are unsigned int, :671) to ~UINT_MAX โ check (huge < strlen(oip)+1) is
FALSE โ ksnprintf(jls+jlsused, ~UINT_MAX, " %s", oip) at :737 writes the IP
string at jls+jlsused past the buffer end โ kernel heap OOB write. The
final SYSCTL_OUT(req, jls, jlsused) at :749 copies jlsused bytes from the
allocation to userspace โ kernel heap OOB read (info leak of adjacent slab
slack).
Reachability
The handler's only check is jailed(td->td_ucred) (:679); any
unprivileged unjailed user can read the sysctl. Precondition: at least one
jail whose host + fullpath formatted length exceeds ~1024 bytes (a deep
chroot path created by root โ realistic on jail/container hosts).
Reproduce
# Phase 1 (root): create the long-path jail ssh dfbsd # or: vm.sh run_root 'sh /path/to/setup_jail_v3.sh 60 4' sh findings/poc/DF-0053/setup_jail_v3.sh 60 4 # Phase 2 (any user): trigger the bug ssh dfbsd-maxx cd findings/poc/DF-0053 && cc -O2 -o jail_list_trigger jail_list_trigger.c ./jail_list_trigger
Or use the convenience scripts:
./build.sh # cc -O2 -o jail_list_trigger jail_list_trigger.c sudo sh ./run.sh setup # root: provision the jail su maxx -c ./run.sh # maxx: trigger (or any unprivileged user)
Expected output (bug present)
[+] BUG DF-0053 CONFIRMED:
kernel returned 1262 bytes
jlssize (count*1024) = 1024
kmalloc bucket (alloc) = 1152
OOB READ vs jlssize = 238 bytes
OOB READ vs actual alloc end = 110 bytes (info leak of adjacent slab slack)
OOB WRITE (IPs written past alloc end during IP loop) also occurred in kernel heap
non-zero bytes in OOB-vs-alloc region: 37 (our written IPs + any stale slab data)
The kernel returned 1262 bytes from a buffer that is logically 1024 bytes
(jlssize) and physically 1152 bytes (the slab bucket zoneindex() rounds
kmalloc(1025) up to โ sys/kern/kern_slaballoc.c:663 (1025+127) & ~127 =
1152). The trailing 110 bytes (offsets 1152..1262) were never part of the
allocation โ they are adjacent slab slack that the kernel both wrote into
(the IP strings, OOB write) and copied out to userspace (OOB read / info
leak).
A fixed kernel would clamp jlsused to jlssize whenever truncation occurs,
and would never return more bytes than jlssize.
Impact
- OOB write into adjacent kernel heap slab memory (the IP strings, ~36 bytes
with 4 IPs; grows linearly with the jail's IP count up to jail(8)'s IP-parse
limit). Content is the jail's formatted IP/hostname/path โ set by root at
jail(2)time, not directly attacker-shaped, but the write POSITION and LENGTH are attacker-observable and the trigger is freely repeatable. With heap grooming this is a memory-corruption primitive that could be converted to local privilege escalation (corrupt an adjacentstruct file/ucred/ function-pointer victim in the 1152-byte bucket). - OOB read / info leak of up to ~110 bytes of adjacent slab slack to
userspace per read. In this run the leaked bytes are zero (the adjacent
chunk was freshly
M_ZERO-allocated) plus our own written IPs; with heap grooming (spray the 1152-byte bucket with pointer-bearing objects before triggering), this leaks kernel pointers / stale slab data. - Local DoS is straightforward (the OOB write corrupts adjacent slab; repeated triggers can panic a less-fortunate kernel layout โ not observed in 50x rapid repeats on this build, but the corruption is real).
High severity: confirmed memory corruption + info leak, reachable by any unprivileged local user via a sysctl read, with a realistic precondition (a long-chroot-path jail).
DF-0053 โ Verdict (master DEV re-verification)
Verdict: REPRODUCED on DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT
(X86_64_GENERIC, INVARIANTS, built 2026-06-29).
Why the prior run said "not_reproduced"
The prior test (on 6.4.2-RELEASE) reported "KEY VERSION DIFF: the vulnerable
sysctl is named jail.list on 6.4.2 (kern.jail.list is 'unknown o...')".
That was mis-read as a version difference. It is not โ it is an OID-name
typo in the original finding markdown. The OID has always been the top-level
jail.list (declared SYSCTL_OID(_jail, OID_AUTO, list, ...) at
sys/kern/kern_jail.c:757); there is no kern.jail.list. The same OID exists
on both 6.4.2 and 6.5-DEVELOPMENT (master DEV). With the correct name, the bug
fires immediately on master DEV.
Confirmed on master DEV
$ sysctl -aN | grep -iE '^jail\.' jail.jailed jail.list <-- the vulnerable OID jail.defaults.vfs_mount_fusefs ... $ sysctl -d jail.list jail.list: List of active jails
sysctlnametomib("jail.list") resolves to OID {258, 257} (258 = jail
subtree). Reading it as the unprivileged user maxx (uid 1001, not in wheel):
[*] MIB for jail.list: 258 257
[*] kernel reports jail.list length = 1262 bytes
[*] sysctl read returned 1262 bytes
[*] 1 jail lines -> jlssize = 1024 bytes (kmalloc(1024+1) -> bucket 1152)
[+] BUG DF-0053 CONFIRMED:
kernel returned 1262 bytes
jlssize (count*1024) = 1024
kmalloc bucket (alloc) = 1152
OOB READ vs jlssize = 238 bytes
OOB READ vs actual alloc end = 110 bytes (info leak of adjacent slab slack)
OOB WRITE (IPs written past alloc end during IP loop) also occurred in kernel heap
non-zero bytes in OOB-vs-alloc region: 37 (our written IPs + any stale slab data)
Reproduced 3ร (byte-identical output โ see run.3x.log). Hammered 50ร
back-to-back: no panic, but every read does a fresh OOB write into adjacent
slab memory.
Mechanism (cited path:line)
Setup: a single jail with a deep chroot path (60 levels ร lllllllllllllll
= path_len 967), MAXHOSTNAMELEN-1 = 255-char hostname, and 4 IPv4 IPs.
Created as root via jail(2) (sys/kern/kern_jail.c:255 sys_jail, gated by
caps_priv_check_self(SYSCAP_NOJAIL_CREATE) at :265 โ the only privilege
boundary; reading jail.list has no such gate).
Trigger: any unprivileged user reads sysctl jail.list โ handler
sysctl_jail_list (sys/kern/kern_jail.c:661):
- Types (
:671):unsigned int jlssize, jlsused;โ both 32-bit unsigned. - Size + alloc (
:688-689):jlssize = (count * 1024);โ1024for 1 jail.jls = kmalloc(jlssize + 1, M_TEMP, M_WAITOK | M_ZERO);โ requests 1025 bytes.zoneindex()(sys/kern/kern_slaballoc.c:663) rounds to bucket(1025+127) & ~127 = 1152. Physical allocation is 1152 bytes. - First ksnprintf (
:704):count = ksnprintf(jls + jlsused, (jlssize - jlsused), "%d %s %s", pr->pr_id, pr->pr_host, fullpath);- size arg =1024 - 0 = 1024. - format expands to"11 " + 255*'h' + " " + 967-char path= 1226 bytes. -snprintf_func(sys/kern/subr_prf.c:494) writes only whileremain >= 2โ writes 1023 chars + NUL intojls[0..1023](truncated, in-bounds). - ButPCHAR(subr_prf.c:549) doesretval++unconditionally โ the return value is the would-be length 1226, not the truncated length. - jlsused += count (
:710):jlsused = 0 + 1226 = 1226. Nowjlsused > jlssize(1226 > 1024). - IP loop bounds check (
:733):if ((jlssize - jlsused) < (strlen(oip) + 1))โ(1024 - 1226)inunsigned intarithmetic =0xFFFFFB6E(~4294966062).4294966062 < 9is FALSE โ does NOT tripERANGE. - IP loop ksnprintf (
:737):count = ksnprintf(jls + jlsused, (jlssize - jlsused), " %s", oip);- writes atjls + 1226with size~UINT_MAXโ no truncation. -jls + 1226is 74 bytes past the 1152-byte allocation end โ OOB WRITE of" 10.0.0.1"(9 bytes) into adjacent slab memory. - repeats for each IP โ OOB writes atjls+1226, +1235, +1244, +1253(4 IPs).jlsusedgrows to 1262. - SYSCTL_OUT (
:749):error = SYSCTL_OUT(req, jls, jlsused);- copiesjlsused = 1262bytes from the 1152-byte allocation to userspace. - bytes[1152..1262](110 bytes) are past the allocation end โ OOB READ / info leak of adjacent slab slack.
The leaked bytes observed:
- Offsets [1152..1225]: zero (slab slack, freshly zeroed by M_ZERO).
- Offsets [1226..1261]: the IPs we wrote (" 10.0.0.4 10.0.0.3 ..." in
SLIST-reverse order).
Exploit chain (memory-corruption primitive)
Bucket: the 1152-byte slab zone (zoneindex of size 1025; align 128).
Victim objects: any kmalloc(N) whose N โ (1024, 1152] lands in the same
zone. Candidate victim objects containing attacker-interesting fields in this
size range should be grepped in sys/ (function-pointer ops vectors,
struct ucred */struct file */struct proc * pointers, refcounts, uids).
Grooming: spray the 1152-byte bucket (sockets, pipes, file opens, mmaps)
to fill partial slabs; punch a hole adjacent to the next jail.list buffer;
trigger the sysctl โ OOB write the IP strings into the victim object.
Conversion: depends on the victim โ overwrite a function pointer โ pivot;
overwrite a ucred * โ forge; overwrite a refcount โ UAF โ re-claim โ write.
Content control: the OOB write content is the jail's formatted IP strings,
set by root at jail(2) time โ not directly attacker-shaped in the default
unprivileged-attacker model. A semi-privileged attacker (one who can create
jails) fully controls it. The OOB write POSITION/LENGTH is attacker-observable
(via jls) and the trigger is freely repeatable, so the corruption primitive
is deterministic even with root-set content (corrupt โ observe via leak โ
re-corrupt).
Where this verification stopped: confirmed the OOB write + OOB read
primitive end-to-end on master DEV. Did not develop a full heap-grooming +
victim-corruption โ uid=0 chain (the OOB write content is not
attacker-controlled in the strict unprivileged model, so a uid0 chain would
require either (a) root cooperation to set IP strings to gadget-shaped bytes,
or (b) finding a victim object in the 1152-byte bucket whose corruption with
printable-IP bytes yields a usable primitive). The realistic impact ceiling
on the strict model is local DoS + info leak of adjacent slab (with
grooming for pointer leakage); with jail-creation capability it escalates to
full memory corruption โ likely privesc. 50ร repeated triggers did not panic
this build (the adjacent slab chunks were benign), but the corruption is real
and a less-fortunate heap layout would crash.
PoC changes vs the original
- OID name: original PoC/finding used
kern.jail.list; corrected to the actual top-leveljail.list(SYSCTL_OID(_jail, OID_AUTO, list, ...)atkern_jail.c:757). This was the sole blocker for the priornot_reproducedverdict โ not a version difference. - Setup script: rewrote as
setup_jail_v3.shโ fully self-contained, builds the deep path onemkdirper level (becausemkdir -psilently truncates long paths on DragonFly), copies a staticsleeperbinary into the chroot as/s, launchesjail(8)backgrounded so the prison survives ssh disconnect (the sleeper keeps it alive). - Trigger: rewrote
jail_list_trigger.cto (a) usesysctlnametomib()for the OID, (b) compare the returned byte count against bothjlssize(count*1024) AND the actual slab bucket (1152), and (c) hexdump the OOB tail so the leaked bytes are visible. - Did NOT need to change: the underlying bug claim, the cited
path:lines, or the recommended fix โ all are accurate against the audited master DEV tree.
Decisive evidence
run.logโ single decisive run;[+] BUG DF-0053 CONFIRMEDwith the byte accounting.run.3x.logโ three back-to-back runs, byte-identical (deterministic because the adjacent slab wasM_ZERO-zeroed; the OOB read LENGTH is proven regardless of content variance).leak_sample.txtโ the OOB tail bytes.env.txtโuname -a,cc --version,sysctl jail.list,jls.dmesg.txtโ full guest dmesg (no slab-corruption warnings on this build โ INVARIANTS inkern_slaballoc.conly catches use-after-free via theweirdarypattern at:1566-1572, not real-time OOB-write detection).- No panic (
panic.txtnot applicable โ guest stayed up across 50+ triggers).
DF-0053 โ Fix notes (post-verification, authored by df-bsd-pocrunner)
fix.diff is a standalone, git apply-able unified diff against the read-only
sys/ tree (never applied here). Verified to apply cleanly via both
git apply --check (rc=0) and patch --dry-run -p1 against
sys/kern/kern_jail.c at repo HEAD.
Root cause (confirmed by verification)
sysctl_jail_list() (sys/kern/kern_jail.c:661) sizes its buffer as
jlssize = count * 1024 (:688) but advances the write cursor jlsused
(:671, unsigned int) by the would-be length that ksnprintf returns
(:710 after the per-jail ksnprintf at :704, and :741 after each IP
ksnprintf at :737). That length can exceed the per-jail budget, so
jlsused can exceed jlssize. Once it does, every unsigned
(jlssize - jlsused) underflows to ~UINT_MAX: the IP-loop bounds check
at :733 (huge < strlen(oip)+1 โ FALSE) stops firing, ksnprintf at :737
gets a ~UINT_MAX size and writes past the allocation end (OOB write), and
the final SYSCTL_OUT(req, jls, jlsused) at :749 copies jlsused > jlssize
bytes out (OOB read / info leak).
What the fix does
Restores and enforces the invariant jlsused <= jlssize at every cursor
advance, and makes the IP-loop bounds check underflow-proof:
:710(after the per-jailksnprintf) โ clamp: if the would-be length is>=the remaining space, setjlsused = jlssizeinstead of advancing.:733(IP-loop bounds check) โ add an explicitjlsused >= jlssize ||short-circuit so the unsigned subtraction can never underflow and bypass the check (defense-in-depth).:741(after each IPksnprintf) โ same clamp as:710.
With the invariant held, (jlssize - jlsused) is always a small, valid
non-negative value, so the ksnprintf size args (:704, :737) and the
SYSCTL_OUT length (:749) can never overflow. The terminator still lands in
the kmalloc(jlssize + 1, ...) slack byte. When the buffer fills, the IP-loop
guard trips ERANGE and goto end returns cleanly (no OOB write, no OOB read).
Why this supersedes the finding markdown's ## Recommended fix
The finding proposed only two hunks: the clamp at :710 and the guard at
:733. That pair is incomplete for the multi-prison case: if prison N's
last IP ksnprintf returns a would-be length that over-advances jlsused
past jlssize at :741, the IP-loop guard at :733 never re-trips (there is
no next IP), LIST_FOREACH advances to prison N+1, and the per-jail
ksnprintf at :704 receives (jlssize - jlsused) which underflows and
overflows the buffer โ re-opening the exact defect. Adding the third clamp
at :741 (hunk 3) is what actually restores the invariant for all iteration
orders and closes the bug fully. The clamp expression, guard form, and
:710/:733 hunks otherwise match the finding's proposal.
Verification of the diff itself
$ git apply --check findings/poc/DF-0053/fix.diff # rc=0 $ patch --dry-run -p1 < findings/poc/DF-0053/fix.diff # "checking file sys/kern/kern_jail.c", no rejects
(Not applied to sys/ โ the audit tree is read-only. Behavioral confirmation
that clamping jlsused to jlssize eliminates the overflow is implied by the
trigger PoC: a fixed kernel would return at most jlssize bytes and the
[+] BUG DF-0053 CONFIRMED markers would not fire.)
Confirmed kernel references
- sys/kern/kern_jail.c:661
- sys/kern/kern_jail.c:671
- sys/kern/kern_jail.c:679
- sys/kern/kern_jail.c:688
- sys/kern/kern_jail.c:689
- sys/kern/kern_jail.c:704
- sys/kern/kern_jail.c:710
- sys/kern/kern_jail.c:733
- sys/kern/kern_jail.c:737
- sys/kern/kern_jail.c:749
- sys/kern/kern_jail.c:757
- sys/kern/subr_prf.c:494
- sys/kern/subr_prf.c:549
- sys/kern/kern_slaballoc.c:663
Detail
Exploit chain
Trigger: unprivileged sysctl(jail.list) read. Primitive 1 (OOB write): once jlsused > jlssize after the first ksnprintf (which returns the would-be length via subr_prf.c:549 retval++), the IP loop's (jlssize-jlsused) underflows unsigned -> ksnprintf at kern_jail.c:737 writes IP strings at jls+jlsused past the 1152-byte alloc end into adjacent slab. Primitive 2 (OOB read): SYSCTL_OUT at kern_jail.c:749 copies jlsused bytes (1262) from the 1152-byte alloc to userspace -> leaks adjacent slab slack. Bucket: 1152-byte slab zone (zoneindex of size 1025). Stopped at: confirmed OOB write + OOB read end-to-end on master DEV; did not develop a full heap-grooming -> victim-corruption -> uid0 chain because the OOB write content (IP strings) is set by root at jail(2) time, not directly attacker-controlled in the strict unprivileged-attacker model. With jail-creation capability the attacker fully controls write content -> escalation to privesc. Realistic ceiling on the strict model: local DoS (the OOB write corrupts adjacent slab) + info leak of adjacent slab (with grooming for kernel-pointer leakage).
Evidence (decisive lines)
[+] BUG DF-0053 CONFIRMED: kernel returned 1262 bytes; jlssize (count*1024) = 1024; kmalloc bucket (alloc) = 1152; OOB READ vs jlssize = 238 bytes; OOB READ vs actual alloc end = 110 bytes (info leak of adjacent slab slack); OOB WRITE (IPs written past alloc end during IP loop) also occurred in kernel heap; non-zero bytes in OOB-vs-alloc region: 37 (the written IPs ' 10.0.0.4 10.0.0.3 10.0.0.2 10.0.0.1' + zeroed slab slack). 3x byte-identical; no panic in 50x repeats. Full logs in findings/poc/DF-0053/run.log + run.3x.log.
PoC changes
Fixed the OID name from 'kern.jail.list' to the actual top-level 'jail.list' (SYSCTL_OID(_jail, OID_AUTO, list, ...) at kern_jail.c:757) in both the trigger and the shell helper -- this was the sole reason for the prior 'not_reproduced' verdict and is NOT a version difference (same OID on 6.4.2 and 6.5-DEV). Rewrote the setup as setup_jail_v3.sh: fully self-contained, builds the deep chroot path one mkdir per level (mkdir -p silently truncates long paths on DragonFly), copies a static sleeper into the chroot as /s, backgrounds jail(8) so the prison survives ssh disconnect. Rewrote jail_list_trigger.c to use sysctlnametomib for the OID, compare the returned byte count against both jlssize (count*1024) and the actual slab bucket (1152), and hexdump the OOB tail so the leaked bytes are visible. Added build.sh, run.sh, VERDICT.md, manifest.json, and full untrimmed logs (build.log, run.log, run.3x.log, leak_sample.txt, env.txt, dmesg.txt). The underlying bug claim, cited path:lines, and recommended fix are all accurate against the audited master DEV tree -- no source-trace corrections needed.
Verified recommended fix
Clamp jlsused<=jlssize at kern_jail.c:710 and :741 and add an explicit jlsused>=jlssize guard at :733 (3 hunks) -- supersedes the finding 2-hunk proposal, which is incomplete for the multi-prison case; full git-apply-able diff at findings/poc/DF-0053/fix.diff.
Verdict
REPRODUCED on DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT (master DEV). The prior 'not_reproduced' was an OID-name typo, not a version difference: the vulnerable sysctl is the top-level 'jail.list' (SYSCTL_OID(_jail, OID_AUTO, list, ...) at sys/kern/kern_jail.c:757), NOT 'kern.jail.list'. With the correct OID, the unsigned-underflow overflow fires immediately. Setup: root creates a jail with host=255 + a 967-char deep-chroot path + 4 IPs (setup_jail_v3.sh). Trigger: unprivileged user reads sysctl 'jail.list' (sysctlnametomib resolves to {258,257}). Decisive evidence: the kernel returned 1262 bytes for a buffer that is logically jlssize=1024 and physically a 1152-byte slab bucket (zoneindex rounds kmalloc(1025) to (1025+127)&~127=1152, sys/kern/kern_slaballoc.c:663). That is a 238-byte OOB read vs jlssize / 110-byte OOB read vs the actual alloc end, AND a 36-byte OOB write of the IP strings into adjacent kernel heap during the IP loop. Reproduced 3x byte-identical; 50x rapid repeats did not panic (adjacent slab chunks were benign) but the corruption is real.