Quirk-apply fast path trusts stale pf / absent pf_last state: latent write-after-free and NULL-page write (unreachable with the shipped quirk table)
Summary
The 'if (pf_last == q->pf)' fast path conflates 'same template as last success' with 'pf is that template's live copy': the cfe-allocation error path kfrees pf without invalidating pf_last (later re-use -> STAILQ_INSERT_TAIL writes 8 bytes at +40/+48 into freed memory) and the wipe loop always exits pf==NULL so a NULL-pf table entry makes line 270 compute &NULL->cfe_head = 0x28 (deterministic NULL-page write). Machine-checked: the shipped 8-entry table can never take the fast path (matchmatrix over all 6 card identities; match sets <=2 with distinct templates) - dead code today; latent ceiling is one quirk-table edit repeating a template + one M_NOWAIT failure during card insertion. Verbatim-copy rig with failpoint allocator: FREED-WRITE at +41 (control CLEAN), nulltemplate SIGSEGV at 0x28/0x30, both fixed twins CLEAN. Guards fix validated in-rig.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5585 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 5.4 KB | β raw | |
| poc_quirkfastpath.c | β | 24.3 KB | view raw | |
| build.sh | β | 108 B | view raw | |
| run.sh | β | 104 B | view raw | |
| build.log | β | 170 B | view raw | |
| run.log | β | 3.3 KB | view raw | |
| run.2.log | β | 3.3 KB | view raw | |
| fidelity.diff | β | 1.4 KB | view raw | |
| fix.diff | β | 786 B | view raw | |
| env.txt | β | 635 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 5.9 KB | view raw |
DF-5585 β pccard_cis_quirks fast path trusts stale pf/absent pf_last state: write-after-free / NULL-page write landmines (latent; unreachable with the shipped table)
File: sys/bus/pccard/pccard_cis_quirks.c (pass 2, GLM 5.3). Full narrative: VERDICT.md.
Reproduce (deterministic userland rig of the verbatim kernel loop)
./build.sh # cc -O2 -Wall -o poc_quirkfastpath poc_quirkfastpath.c ./run.sh # ./poc_quirkfastpath all
Expected decisive output (see run.log; exit 1 because findings>0): FREED-WRITE[repeat]: block ... (kmalloc #9) modified: first diff at +41 ... RESULT: DF-5585 repeat: quirk#3 fast path (pf_last==q->pf) STAILQ_INSERT_TAIL'd a cfe into the pf kfree'd at pccard_cis_quirks.c:282 -> write-after-free child died on SIGSEGV writing near address 0x28/0x30 ... RESULT: DF-5585 nulltemplate: NULL pf template + pf_last(NULL)==q->pf fast path -> &pf->cfe_head deref at offset 40 of NULL RESULT: DF-5585 matchmatrix: shipped table fast-path reachable by ANY card identity: NO Built and executed on the DragonFly guest (DragonFly 6.5-DEVELOPMENT x86_64, cc 8.3). The guest has no PCMCIA bridge (see env.txt), so the kernel path itself is not enterable; the rig embeds byte-identical copies of pccard_cis_quirk_match() (:201-216) and pccard_check_cis_quirks() (:218-292) β fidelity.diff shows the only differences are the 3 documented shim substitutions β plus a failpoint allocator that snapshots blocks at kfree time (kernel kfree does not scrub).
Scenarios: negative (clean install), shippedfail (shipped-table shape +
cfe-alloc failure -> CLEAN: today's kernel is safe), repeat (write-after-free),
fixed_repeat (fix.diff guard -> CLEAN), nulltemplate (NULL-page write,
SIGSEGV child), fixed_nulltemplate (guard -> CLEAN), matchmatrix
(machine-checked proof that NO card identity the shipped 8-entry table can
match ever takes the fast path).
What it proves: the fast path if (pf_last == q->pf) at :263 dereferences
pf under an invariant ("pf is the live copy built for template pf_last")
that the cfe-allocation error path at :280-285 breaks by kfree'ing pf
without invalidating it, and that a NULL q->pf entry breaks by matching
pf_last == NULL == q->pf right after the wipe loop (which always exits with
pf == NULL, :248-257). Unreachable with the shipped quirk table (every
card's match set is <= 2 entries with distinct templates β proven by the
matchmatrix); one table edit (a repeated/alternating template, exactly how
:179-186 already shares templates across product ids) plus one M_NOWAIT
failure during card insertion turns it into kernel heap corruption.
Recommended fix (fix.diff, validated in-rig by the fixed_* scenarios):
NULL-guard table entries + reset pf_last on the :281-285 error path.
DF-5585 β VERDICT
Finding: pccard_check_cis_quirks() fast path (if (pf_last == q->pf),
sys/bus/pccard/pccard_cis_quirks.c:263-270) dereferences pf under an
invariant the function's own error paths do not maintain. Two latent facets:
-
Write-after-free β In the else-branch, when the cfe
kmalloc(M_NOWAIT)fails (pccard_cis_quirks.c:280-285), the freshly allocatedpfiskfreed (line 282) but the localpfkeeps pointing at it andpf_laststill names the previous successful template. If any LATER matching quirk re-uses that template (pf_last == q->pftrue), the fast path runsSTAILQ_INSERT_TAIL(&pf->cfe_head, ...)(line 270) into the freed pf β an 8-byte pointer write atoffsetof(struct pccard_function, cfe_head)(=40 on amd64) plus a second write at +48, both attacker-influenced only in the sense that a quirk-table maintainer chooses the table; the trigger conditions are (a) one failed allocation during card insertion and (b) a table containing a repeated/alternating template among the entries one card can match. -
NULL-page write β The wipe loop (pccard_cis_quirks.c:248-257) always exits with
pf == NULL(pfis the loop variable; its exit condition ispf != NULLfailing). If a table entry hasq->pf == NULLand it is the first match,pf_last == NULL == q->pftakes the fast path and line 270 computes&((struct pccard_function *)NULL)->cfe_head= address 0x28, writing throughstqh_lastat 0x30 β a deterministic kernel panic (NULL page is unmapped in the kernel), and a NULL-deref write landmine.
Reachability today (the honest gate): NOT reachable. The quirk match
(pccard_cis_quirks.c:201-216) requires (manufacturer, product) equality
(three 3COM pairs, each contributing a func0/func1 entry pair) or
INVALID/INVALID plus cis1_info[0]/[1] string equality (SVEC vs NDC strings
differ β pccarddevs:327,332). Therefore every card identity's match set is
at most 2 entries whose templates always differ, consecutive matches never
share a template, the fast path is DEAD CODE for the shipped table, and a
cfe-alloc failure can only strike the last matching entry. This is
machine-checked by the rig's matchmatrix scenario over all 6 card
identities. Additionally the whole path requires a physical PCMCIA card
insertion (pccard.c:198, after pccard_read_cis), and this QEMU guest has no
PCMCIA bridge, so the kernel path was not enterable for testing. Reported
Info / hardening (latent landmine): one future quirk-table edit β e.g.
adding a second cfe or another product id re-using an existing template,
the exact pattern lines 179-186 already use to share pccard_3cxem556_func0
across two product ids β converts facet 1 into kernel heap corruption
reachable by card insertion under memory pressure.
Rig proof (what was actually run): poc_quirkfastpath.c embeds
byte-identical copies of pccard_cis_quirk_match() and
pccard_check_cis_quirks() (fidelity.diff: only the 3 documented shim
substitutions β device_t devβsc parameter, dropped PCCARD_SOFTC line,
qlog in the dead bootverbose block). The rig's allocator snapshots every
block at kfree time (kernel kfree does not scrub memory) and audits freed
blocks for post-free modification.
shippedfail(shipped-table shape, cfe alloc of quirk#2 fails at the failpoint): CLEAN β negative control proving the shipped kernel is safe under this failure.repeat(3-entry table re-using the func0 template; same failpoint): FREED-WRITE detected β block from kmalloc call #9 (the pf freed at :282) modified at +41 (the stored cfe pointer lands at cfe_head.stqh_first, offset 40..47), i.e.STAILQ_INSERT_TAILwrote into freed memory. Exit 1.nulltemplate(pf==NULL entry after a non-empty wipe): child process died on SIGSEGV writing near 0x28/0x30 β the userland image of the kernel NULL-page write.fixed_repeat/fixed_nulltemplate(verbatim copy + fix.diff's two guards): CLEAN β both facets eliminated; the fixed run installs two function copies instead of corrupting freed memory.- Deterministic: run.log and run.2.log identical.
Impact ceiling: none in the shipped kernel (dead code path); latent heap write-after-free / NULL-write one table-entry away. Threat actor for the live variant is whoever can insert a card whose CIS matches a quirk β physical access, same threat model as the DF-5355..5364 family.
Root cause lines: sys/bus/pccard/pccard_cis_quirks.c:263 (fast-path
predicate trusts pf_last==q->pf β pf live), :270 (insert through pf),
:280-285 (kfree of pf without invalidating the tracking state),
:248-257 (wipe leaves pf == NULL while pf_last == NULL too).
Fix: fix.diff β (1) skip malformed entries (q->pf == NULL ||
q->cfe == NULL), (2) reset pf_last = NULL on the :281-285 error path so
the invariant "pf_last names the template of the live pf" always holds.
Validated in-rig (fixed_* scenarios). Kernel-rebuild fix validation was not
performed: the bug is unreachable in the shipped kernel, so there is no
baseline kernel behavior to reproduce/eliminate (fix_status: not_testable).
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
not_testableKernel-rebuild fix validation not applicable: the bug is unreachable in the shipped kernel (fast path is dead code for the shipped table -- matchmatrix), so no baseline kernel behavior exists to reproduce and eliminate. The fix was validated at rig level instead: fixed_repeat and fixed_nulltemplate (verbatim loop + fix.diff's two guards) run the same failing scenarios with zero freed-memory writes and no fault (run.log).
findings/poc/DF-5585/run.log (fixed_repeat / fixed_nulltemplate scenarios: 'freed-memory audit ... CLEAN'); findings/poc/DF-5585/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-5585/run.log: 'FREED-WRITE[repeat]: block ... (kmalloc #9) modified: first diff at +41' (write into the pf kfree'd at pccard_cis_quirks.c:282); 'child died on SIGSEGV writing near address 0x28/0x30' (NULL-template facet); 'RESULT: DF-5585 matchmatrix: shipped table fast-path reachable by ANY card identity: NO'; fixed_* scenarios 'CLEAN'; fidelity.diff (rig function bodies identical to sys/ apart from 3 documented shims); VERDICT.md for the full narrative
PoC changes
Authored from scratch (no seed): verbatim copies of pccard_cis_quirk_match() and pccard_check_cis_quirks() with 3 documented shim substitutions (device_t->sc parameter, dropped PCCARD_SOFTC line, qlog in the dead bootverbose block); failpoint allocator with freed-block snapshot audit (kernel kfree does not scrub); fixed variant carries only the two fix.diff guard lines. One mid-run correction: initial nulltemplate framing claimed the wipe leaves pf dangling -- wrong, the wipe loop variable exits NULL, making the facet a NULL-page write (rig child SIGSEGV at 0x28/0x30), which the final version reports gracefully via fork.
Verified recommended fix
fix.diff: skip malformed quirk entries (q->pf/q->cfe NULL) and reset pf_last=NULL on the :281-285 cfe-allocation error path so the fast-path invariant always holds
Verdict
Latent memory-corruption landmines in the quirk-apply loop, machine-proven in a verbatim rig but NOT reachable in the shipped kernel (rig status honestly 'untested' at the kernel level: the guest has no PCMCIA bridge AND the path is dead code today). Facet 1 (write-after-free): the fast path 'if (pf_last == q->pf)' at pccard_cis_quirks.c:263-270 dereferences pf assuming it is the live copy of template pf_last, but the cfe-allocation error path at :280-285 kfrees the just-allocated pf (line 282) without invalidating that tracking state; any later matching quirk re-using the pf_last template then runs STAILQ_INSERT_TAIL(&pf->cfe_head,...) into freed memory -- 8-byte pointer writes at offsetof(pccard_function, cfe_head)=40 and +48. Rig 'repeat' scenario detects exactly this (FREED-WRITE, first diff +41, 5 bytes) while the shipped-table-shape control 'shippedfail' stays CLEAN. Facet 2 (NULL-page write): the wipe loop :248-257 always exits with pf==NULL (loop variable), so a table entry with q->pf==NULL matches pf_last==NULL==q->pf and line 270 computes &((struct pccard_function*)NULL)->cfe_head = 0x28, writing through stqh_last at 0x30 -- rig 'nulltemplate' child dies on SIGSEGV at exactly that address; kernel equivalent is a deterministic NULL-page write panic. UNREACHABILITY PROOF (why impact=none): the match semantics at :201-216 constrain every card identity to a match set of <=2 entries (three 3COM (manufacturer,product) pairs, each a func0/func1 pair with distinct templates; SVEC vs NDC cis1_info string sets are disjoint -- pccarddevs:327,332), so consecutive matches never share a template, the fast path is dead code for the shipped table, and a cfe-alloc failure can only strike the final matching entry -- machine-checked by the rig 'matchmatrix' over all 6 card identities. One quirk-table edit (a repeated/alternating template, the exact sharing pattern :179-186 already uses across two product ids) plus one M_NOWAIT failure during card insertion converts facet 1 into kernel heap corruption; physical-card threat model shared with the DF-5355..5364 family. Fix validated in-rig (fixed_repeat/fixed_nulltemplate CLEAN): NULL-guard table entries + reset pf_last on the :281-285 error path (fix.diff). Kernel-rebuild fix validation not performed: no baseline behavior exists to eliminate (bug unreachable in shipped kernel).
No comments yet.