β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5362

CIS chain-graph cycles never terminate: pccard_scan_cis livelock (CPU burn, wedged socket thread, unreleased CIS window; unbounded leak amplifier)

Summary

Termination needs longlink_present==0 AND mfc done, but a LONGLINK inside a FOLLOWED chain re-arms longlink_present - LINKTARGET + LONGLINK_A->0 forms a cycle with identical walker state revisiting forever: no hop budget, no visited set, every chain re-parsed from scratch. One evil card = one CPU spun in kernel, socket events never serviced, CIS window never released; with FUNCID/CONFIG/CFTABLE in the cycle it becomes unbounded kmalloc exhaustion (DF-5361 amplifier). Model-verified (cycle detector fingerprints repeat after 5 steps/2 follows; leakloop +1 cfe/cycle). Hop-budget-64 fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5362 Β· 22 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 10.2 KB ↓ raw
extract.sh β€” 4.2 KB view raw
rig_shims.h β€” 7.6 KB view raw
rig_main.c β€” 16.7 KB view raw
run_matrix.sh β€” 2.4 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 119 B view raw
build.log β€” 2.0 KB view raw
run.log β€” 2.7 KB view raw
fixed_run.log β€” 3.2 KB view raw
baseline_run.log β€” 4.9 KB view raw
matrix.log β€” 64.1 KB view raw
fix.diff β€” 917 B view raw
extract_manifest.txt β€” 1.4 KB view raw
postboot_extract_manifest.txt β€” 1.5 KB view raw
postboot_run.log β€” 6.8 KB view raw
env.txt β€” 997 B view raw
kernel_apply.log β€” 65 B view raw
kernel_build.tail.log β€” 2.6 KB view raw
verdict.json β€” 5.3 KB view raw
manifest.json β€” 2.2 KB view raw

DF-5362 -- CIS chain-graph cycles never terminate: pccard_scan_cis livelock (CPU burn, wedged socket thread, unreleased CIS window; unbounded leak amplifier)

File: sys/bus/pccard/pccard_cis.c (verify pass, GLM 5.3). Full narrative: VERDICT.md.

Reproduce (verbatim production parser, guest toolchain, no PCMCIA HW needed)

./build.sh          # sh extract.sh .  &&  cc -O2 -Wall -o rig rig_main.c
./run.sh            # baseline scenarios: ./rig livelock && ./rig leakloop && ./rig livelock_alarm
sh run_matrix.sh    # A/B: baseline vs each single-fix variant, all scenarios

Expected decisive output (DF-5362): BASELINE: livelock -> read budget 200000 exhausted with pccard_scan_cis NOT returned; the bus-read offset stream is periodic with period 12 reads (deterministic cycle); livelock_alarm -> SIGALRM kill after 2 s wall with ~630-695 MILLION bus reads executed and the scan still running (kernel-mode CPU burn analogue); leakloop -> +1 cfe per 22-read cycle, LIVE cfe=4545 at cap 100000 and 9090 at cap 200000 (exactly linear; kernel has no cap). WITH fix.diff (f5362): all three return normally after 65 link hops (budget 64), leakloop LIVE cfe=64 regardless of budget; matrix shows no other scenario changed; benign valid card (3 hops) unaffected.

What it proves

The rig executes the byte-identical production parser extracted from the guest's /usr/src (sha256-gated; extract_manifest.txt) through the real entry pccard_read_cis(). Kernel entry is hardware-gated (no PCMCIA bridge in QEMU) -- see VERDICT.md for the method, the A/B matrix, and the booted-combined-fix-kernel validation.

Recommended fix: Add a chain-hop budget (64) to the link-follow loop; exceeding it device_prints and goto done, exactly like the existing truncation path at :164-167. (fix.diff; validated, see fixed_run.log / matrix.log / postboot_run.log).

Note: poc_pccard_cis2.c is the seeded hand-written model from the earlier model-only verdict (status was untested); it is kept for history and is SUPERSEDED by the verbatim rig (extract.sh + rig_main.c + rig_shims.h) which reproduces its numbers on the byte-identical production parser.

VERDICT.md
↓ download raw

DF-5362 -- VERDICT

Verdict in one line

REPRODUCED (verbatim-rig level; deterministic) -- impact: dos. The defect manifests in the byte-identical production sys/bus/pccard/pccard_cis.c code executed on the DragonFly guest; the kernel entry path itself is hardware-gated (no PCMCIA bridge in QEMU).

REPRODUCED at the verbatim-rig level on the DragonFly guest; kernel entry hardware-gated (no PCMCIA bridge). pccard_scan_cis terminates only when longlink_present==0 AND mfc_index>=mfc_count (:398-409); the in-chain truncation check (:164) only forces a synthetic END WITHIN a chain. On the verbatim parser: a LINKTARGET('CIS') chain at attr 0x10 whose LONGLINK_A targets 0x10 itself re-arms longlink_present (:209-212) on every revisit -- the rig's read-offset ring shows the bus-read stream PERIODIC with period 12 reads for >=3 full cycles and the scan fails to return within a 200000-read budget the kernel does not have; the alarm variant measured ~630-695 million bus reads in 2 seconds of wall clock and was still spinning when killed (one CPU burned in kernel mode; on the socket event thread the insertion never completes and bus_release_resource at :441 never runs). With FUNCID/CONFIG/CFTABLE in the cycle each iteration allocates one cfe (leakloop): LIVE cfe=4545 at budget 100000 vs 9090 at 200000 -- exactly +1 cfe per 22-read cycle, unbounded inside a single insertion (with DF-5361 the pf is never freed either). mfc hops alone are bounded (mfc_index monotonic <= 50); only the LONGLINK re-arm makes the graph cyclic. Fix validated: hop budget 64 -- all livelock scenarios return normally (65 CARD_SET_RES_FLAGS switches = 64 hops + initial), leakloop capped at 64 cfes independent of budget, benign 3-hop card unaffected; combined-five-fix kernel built -Werror, booted, rig re-driven from the running kernel's patched source: all terminate.

Method -- the verbatim rig (why this is production code, not a model)

The guest has no PCMCIA/CardBus bridge (env.txt: no /dev/pccard, no pccardc, 0 pccard dmesg lines), so pccard_scan_cis cannot be entered at runtime by card insertion. Instead of hand-replicating the parser, the rig machine- extracts the production parser byte-identical from the guest's own kernel source* and executes it:

  • extract.sh sed-extracts, from /usr/src/sys/bus/pccard/pccard_cis.c, lines 51-444 (DPRINTF macros, PCCARD_CIS_SIZE, struct cis_state, pccard_read_cis, pccard_scan_cis) and 612-1294 (pccard_parse_cis_tuple, decode_funce), plus the needed struct/macro ranges of pccardvar.h (tuple struct, read macros, A_MEM flags) and the constant headers pccard_cis.h/pccardreg.h whole.
  • The extraction is sha256-gated: all four pccard files plus sys/sys/queue.h must match the audited pristine tree (acdd2484... for pccard_cis.c); extract_manifest.txt in this pack is the guest-generated record. The userland <sys/queue.h> is hash-identical to the kernel's, so the STAILQ macros the parser expands are the production ones.
  • rig_shims.h (the only hand-written code that touches the parse path) emulates the kernel environment: bus_alloc_resource returns a 4096-byte mapping (PCCARD_CIS_SIZE) inside a 64 KiB guard buffer -- [4096,65536) is a 0xA5 canary zone standing in for whatever the bridge maps adjacent to the CIS window, and offsets >= 65536 are recorded but never dereferenced (the userland analogue of a CardBus master abort); CARD_SET_RES_FLAGS swaps attribute/common images; kmalloc/kfree are canary-banded and ledgered. Every bus_space_read_1 is counted and bounds-checked against the real mapping size.
  • rig_main.c builds crafted CIS images (attribute-memory spacing: tuple at index t has code at bus offset 2t, length at 2t+2, body byte i at 2t+4+2i, per the verbatim pccard_cis_read_1/pccard_tuple_read_1 macros, pccardvar.h:255-280) and calls the real entry point pccard_read_cis(&sc) in a forked, crash-instrumented child.

So the code that crashes / leaks / loops below is the exact sys/bus/pccard/pccard_cis.c text that ships in the guest's kernel, compiled on the guest toolchain (cc 8.3).

A/B discipline

run_matrix.sh builds six rig variants from the SAME pristine extraction -- baseline plus one per fix, each fix applied ONLY to a copy of the extracted file (patch -p1, applied_fix.diff shows only the fix hunk) -- and runs every scenario under every variant. Each fix flips exactly its own scenario and leaves every other scenario (including the negative controls) untouched; see matrix.log and the summary table in this file.

matrix.log summary (baseline + each single-fix variant)

scenario baseline f2319 f2320 f5361 f5362 f5363
nullderef_a SEGV si_addr=0x0 ret clean SEGV SEGV SEGV SEGV
nullderef_b SEGV si_addr=0x30 ret clean SEGV SEGV SEGV SEGV
wildlink_long OOB=1 @16777216 same OOB=0 same same same
wildlink_mfc OOB=2 max 67108864 same OOB=0 same same same
mfcleak LIVE cfe=2 leaked same same LIVE cfe=0 same same
livelock NONTERM (periodic) same same same ret (hop cap) same
leakloop NONTERM, cfe grows same same same ret, cfe=64 same
checksum OOB=2045 max 8184 same same same same OOB=0
checksum_ok OOB=0 same same same same same
benign (valid) clean, LIVE cfe=3 clean clean LIVE cfe=2 clean clean

Kernel-level fix validation (combined five-fix kernel)

  1. All five fix.diffs patch -p1-applied to the guest's /usr/src (kernel_apply.log; patched pccard_cis.c sha256 67334ef6...).
  2. make -j6 nativekernel KERNCONF=X86_64_GENERIC -- rc=0, 38k-line log; pccard_cis.c recompiled under -Werror in both kernel and KLD_MODULE contexts (full log in DF-2319's pack, tail here: kernel_build.tail.log).
  3. make installkernel -- rc=0; rebooted into 6.5-DEVELOPMENT #1: Wed Oct 14 13:35:33 UTC 2026, kernel sha256 c73653c698f0f6149208e4e9b1cf536769430529384fe219b37c0b393e8ee01a (env.txt).
  4. All five DF markers verified present in the RUNNING kernel's source; the rig re-extracted from that patched tree (extract.sh reports SOURCE_STATE=PATCHED, postboot_extract_manifest.txt) and re-driven: every scenario clean -- no SIGSEGV, no OOB read, no non-termination, leaks drained, negative controls still pass (postboot_run.log).
  5. Guest reset to the with-src clean-source snapshot afterwards; pristine sha256 acdd2484... re-verified (guest left up and clean).

Note: in-kernel behavioral exercise of the parser is impossible on this guest (no PCMCIA bridge) -- the same gating as the DF-1040/DF-1041 precedent; the booted-kernel validation above proves compile+boot+source-of-running-kernel, and the rig (verbatim same text) proves behavior.

Non-termination evidence

  • livelock: 200000-read budget exhausted with pccard_scan_cis NOT returned; ring analysis: "bus-read offset stream is PERIODIC, period=12 reads, >=3 full cycles observed" -- a deterministic cycle in the production walker (LINKTARGET + LONGLINK_A + END = 12 bus reads per revolution).
  • livelock_alarm: SIGALRM kill after 2 s wall clock with ~570-695,000,000 bus reads executed (varies with host timing; hundreds of millions either way) and the scan still running -- the kernel-mode CPU-burn analogue; on the socket event thread this also wedges insertion handling and leaks the CIS window (bus_release_resource :441 unreached).
  • leakloop: same cycle carrying CONFIG+CFTABLE: LIVE cfe=4545 at budget 100000 and 9090 at 200000 (+1 cfe per 22 reads, exactly linear).
  • Under the fix: 65 CARD_SET_RES_FLAGS switches (budget 64 + initial map) and normal return; leakloop LIVE cfe=64 at ANY budget.

Fix (fix.diff -- validated)

Add a chain-hop budget (64) to the link-follow loop; exceeding it device_prints and goto done, exactly like the existing truncation path at :164-167.

  • rig A/B: fixed_run.log (this pack) + matrix.log -- only this finding's scenarios change; negative controls (checksum_ok, benign valid card) stay green under every fix variant.
  • kernel: see "Kernel-level fix validation" above.

Kernel references

Exploit chain

evil card -> pccard_attach_card (socket event thread) -> pccard_read_cis -> pccard_scan_cis -> cycle {LINKTARGET validates, LONGLINK_A re-arms longlink, END jumps back to the same LINKTARGET} -> infinite kernel-mode loop: CPU livelock + wedged socket thread + leaked CIS window (:441 unreached); with FUNCID/CONFIG/CFTABLE inside the cycle it is unbounded kmalloc exhaustion (+1 cfe/iteration, DF-5361 amplifier)

Environment

Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (audits) / booted fixed kernel #1 for fix validation (env.txt). cc 8.3 [DragonFly]. Artifacts: extract_manifest.txt (verbatim proof), run.log (baseline decisive), fixed_run.log (fix A/B), baseline_run.log (full pristine run), matrix.log (6x11 A/B), postboot_run.log (running-kernel source), env.txt, build.log.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. (1) Single-fix rig A/B: hop budget 64 -> livelock and leakloop return normally (65 flag switches), leakloop LIVE cfe=64 at any read budget, livelock_alarm returns before the 2 s alarm, benign 3-hop card unaffected. (2) Combined five-fix kernel build+boot+running-source re-run: all scenarios terminate.

fix.diff; fixed_run.log; matrix.log (single-fix A/B); kernel_apply.log + kernel_build.tail.log + env.txt + postboot_extract_manifest.txt + postboot_run.log (combined-fix kernel build/boot/running-source rerun)
↓ fix.diffper-fix-DF-5362

Confirmed kernel references

Detail

Exploit chain

evil card -> pccard_attach_card (socket event thread) -> pccard_read_cis -> pccard_scan_cis -> cycle {LINKTARGET validates, LONGLINK_A re-arms longlink, END jumps back to the same LINKTARGET} -> infinite kernel-mode loop: CPU livelock + wedged socket thread + leaked CIS window (:441 unreached); with FUNCID/CONFIG/CFTABLE inside the cycle it is unbounded kmalloc exhaustion (+1 cfe/iteration, DF-5361 amplifier)

Evidence (decisive lines)

run.log (livelock: 'READ BUDGET EXHAUSTED ... has NOT returned' + 'RING: ... PERIODIC, period=12 reads'; livelock_alarm: 'SIGALRM after wall-clock budget; bus_reads=630837765'; leakloop LIVE cfe=4545@100k / 9090@200k in matrix.log); fixed_run.log (f5362: normal return, 65 flag switches = hop budget, LIVE cfe=64); postboot_run.log (all terminate)

PoC changes

Upgraded the seeded model to the verbatim rig; leakloop initially terminated because a NO_LINK tuple in chain 0 disarmed the longlink (exactly the mechanism this finding describes) -- removed it so the LONGLINK_A re-arm drives the cycle; ring-based periodicity detection and read-budget longjmp added to shims.

Verified recommended fix

Add a chain-hop budget (64) to the link-follow loop; exceeding it device_prints and goto done, exactly like the existing truncation path at :164-167.

Verdict

REPRODUCED at the verbatim-rig level on the DragonFly guest; kernel entry hardware-gated (no PCMCIA bridge). pccard_scan_cis terminates only when longlink_present==0 AND mfc_index>=mfc_count (:398-409); the in-chain truncation check (:164) only forces a synthetic END WITHIN a chain. On the verbatim parser: a LINKTARGET('CIS') chain at attr 0x10 whose LONGLINK_A targets 0x10 itself re-arms longlink_present (:209-212) on every revisit -- the rig's read-offset ring shows the bus-read stream PERIODIC with period 12 reads for >=3 full cycles and the scan fails to return within a 200000-read budget the kernel does not have; the alarm variant measured ~570-695 million bus reads in 2 seconds of wall clock and was still spinning when killed (one CPU burned in kernel mode; on the socket event thread the insertion never completes and bus_release_resource at :441 never runs). With FUNCID/CONFIG/CFTABLE in the cycle each iteration allocates one cfe (leakloop): LIVE cfe=4545 at budget 100000 vs 9090 at 200000 -- exactly +1 cfe per 22-read cycle, unbounded inside a single insertion (with DF-5361 the pf is never freed either). mfc hops alone are bounded (mfc_index monotonic <= 50); only the LONGLINK re-arm makes the graph cyclic. Fix validated: hop budget 64 -- all livelock scenarios return normally (65 CARD_SET_RES_FLAGS switches = 64 hops + initial), leakloop capped at 64 cfes independent of budget, benign 3-hop card unaffected; combined-five-fix kernel built -Werror, booted, rig re-driven from the running kernel's patched source: all terminate.