โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0053

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

sys/kern/kern_jail.c:

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 (or sysctlbyname) 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.

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

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
FileTypeDescriptionSize
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
README.md readme Reproduce instructions + impact summary
โ†“ download 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 adjacent struct 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).

VERDICT.md verdict Full narrative: reproduced? mechanism path:line, exploit chain, PoC changes
โ†“ download raw

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):

  1. Types (:671): unsigned int jlssize, jlsused; โ€” both 32-bit unsigned.
  2. Size + alloc (:688-689): jlssize = (count * 1024); โ†’ 1024 for 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.
  3. 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 while remain >= 2 โ†’ writes 1023 chars + NUL into jls[0..1023] (truncated, in-bounds). - But PCHAR (subr_prf.c:549) does retval++ unconditionally โ€” the return value is the would-be length 1226, not the truncated length.
  4. jlsused += count (:710): jlsused = 0 + 1226 = 1226. Now jlsused > jlssize (1226 > 1024).
  5. IP loop bounds check (:733): if ((jlssize - jlsused) < (strlen(oip) + 1)) โ†’ (1024 - 1226) in unsigned int arithmetic = 0xFFFFFB6E (~4294966062). 4294966062 < 9 is FALSE โ†’ does NOT trip ERANGE.
  6. IP loop ksnprintf (:737): count = ksnprintf(jls + jlsused, (jlssize - jlsused), " %s", oip); - writes at jls + 1226 with size ~UINT_MAX โ†’ no truncation. - jls + 1226 is 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 at jls+1226, +1235, +1244, +1253 (4 IPs). jlsused grows to 1262.
  7. SYSCTL_OUT (:749): error = SYSCTL_OUT(req, jls, jlsused); - copies jlsused = 1262 bytes 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-level jail.list (SYSCTL_OID(_jail, OID_AUTO, list, ...) at kern_jail.c:757). This was the sole blocker for the prior not_reproduced verdict โ€” not a version difference.
  • Setup script: rewrote as setup_jail_v3.sh โ€” fully self-contained, builds the deep path one mkdir per level (because mkdir -p silently truncates long paths on DragonFly), copies a static sleeper binary into the chroot as /s, launches jail(8) backgrounded so the prison survives ssh disconnect (the sleeper keeps it alive).
  • Trigger: rewrote jail_list_trigger.c to (a) use sysctlnametomib() for the OID, (b) compare the returned byte count against both jlssize (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 CONFIRMED with the byte accounting.
  • run.3x.log โ€” three back-to-back runs, byte-identical (deterministic because the adjacent slab was M_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 in kern_slaballoc.c only catches use-after-free via the weirdary pattern at :1566-1572, not real-time OOB-write detection).
  • No panic (panic.txt not applicable โ€” guest stayed up across 50+ triggers).
fix_notes.md fix-notes Root-cause recap, what each hunk does, why this supersedes the finding's 2-hunk proposal
โ†“ download raw

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:

  1. :710 (after the per-jail ksnprintf) โ€” clamp: if the would-be length is >= the remaining space, set jlsused = jlssize instead of advancing.
  2. :733 (IP-loop bounds check) โ€” add an explicit jlsused >= jlssize || short-circuit so the unsigned subtraction can never underflow and bypass the check (defense-in-depth).
  3. :741 (after each IP ksnprintf) โ€” 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).

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

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.