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

genassym.c no longer emits GD_CURRENTLDT consumed by standard-built global.s β€” shipped kernel links with a dangling undefined symbol and silently exports no gd_currentldt (C-to-asm contract guard defeated)

Summary

global.s:66-67 does '.globl gd_currentldt / .set gd_currentldt,globaldata+GD_CURRENTLDT' but genassym never defines the macro: gas silently drops the .set, the kernel link tolerates the unreferenced undefined symbol, and the shipped /boot/kernel/kernel carries 'U GD_CURRENTLDT' with zero relocations and no gd_currentldt export - the build stayed green with a broken contract line. Proven by probe assembly under the kernel's own ASM_CFLAGS (symbol vanishes silently) plus byte-identical assym.s regeneration (176/176 emitted values otherwise verified; only #if 0'd IBRS/STIBP differ). No runtime consumer today, but the genassym contract is the ONLY guard against the silent memory-corruption class this family represents (DF-5586/DF-5335 lineage) and this shows it fails silently for .set-consumed macros; 7 sibling dangling symbols in locore.s:41-47 prove the class is established. TF_R10/R11 lead RESOLVED CLEAN (defined at :152-153, consumed exception.S:305-306, values verified). Restore-the-line fix validated (0x42d4 between neighbors).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5596 Β· 26 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
probe_gdcl.s β€” 693 B view raw
genassym_fixed.c β€” 11.7 KB view raw
offset_check.c β€” 18.3 KB view raw
build.sh β€” 133 B view raw
run.sh β€” 90 B view raw
build_genassym_unpatched.log β€” 1.1 KB view raw
build_probe_unpatched.log β€” 1.1 KB view raw
nm_probe_unpatched.txt β€” 63 B view raw
build_genassym_fixed.log β€” 1.0 KB view raw
build_probe_fixed.log β€” 1.1 KB view raw
nm_probe_fixed.txt β€” 69 B view raw
diff.fix_delta.txt β€” 357 B view raw
fix_gdcl_value.txt β€” 29 B view raw
assym_unpatched.s β€” 4.2 KB view raw
diff.buildtree_vs_regen.txt β€” 0 B ↓ download
diff.assym_vs_checker.txt β€” 379 B view raw
out.kernelview.raw.txt β€” 4.3 KB view raw
shipped_kernel_evidence.txt β€” 1.6 KB view raw
runner2_master.log β€” 7.3 KB view raw
asmflags.txt β€” 996 B view raw
env.txt β€” 196 B view raw
fix.diff β€” 473 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.0 KB view raw

DF-5596 β€” genassym.c does not emit GD_CURRENTLDT consumed by global.s (silent C↔asm contract break)

What

sys/platform/pc64/x86_64/genassym.c (the ASSYM generator for the pc64 platform) never emits GD_CURRENTLDT, although:

Observed effect (proven on the shipped kernel, not simulated)

The assembler accepts .set sym, <undefined> and silently drops the symbol definition; the kernel link (EXEC, -Bdynamic --export-dynamic, custom -T ldscript) tolerates the resulting unreferenced undefined symbol:

$ nm /usr/obj/usr/src/sys/X86_64_GENERIC/global.o
                 U GD_CURRENTLDT        <-- dangling
$ nm /boot/kernel/kernel | grep ' U '
                 U GD_CURRENTLDT        <-- in the SHIPPED kernel
$ nm /boot/kernel/kernel | grep -c gd_currentldt
0                               <-- the promised .globl export silently DOES NOT EXIST
$ readelf -r /boot/kernel/kernel | grep -cE 'DMAP_|addr_P|GD_CURRENTLDT'
0                               <-- no relocation consumes it (why ld let it pass)

So the build succeeds and the boot works, but the genassym safety contract ("any struct change either regenerates matching offsets or breaks the build") was silently defeated. The same mechanism leaves seven more dangling undefined symbols from locore.s:41-47 (loc_PTmap..loc_PML4pml4e, dmapbase, dmapend β€” extern variables from machine/pmap.h that nothing defines) β€” see VERDICT.md for the sweep.

Reproduce (on the audit QEMU guest)

  1. sh build.sh β€” compiles, with the exact kernel build flags: - pristine genassym.c β†’ genassym.o β†’ regenerated assym.s (byte-identical to the build tree's β€” diff.buildtree_vs_regen.txt), - probe_gdcl.s (consumes GD_CURRENTLDT exactly like global.s:67) against that assym β†’ nm shows U GD_CURRENTLDT and no gd_currentldt_probe symbol: the bug, reproduced under control.
  2. Fix validation: genassym_fixed.c (one added ASSYM line) β†’ regenerated assym gains #define GD_CURRENTLDT 0x42d4 (diff.fix_delta.txt) and the same probe now yields 00000000000042d4 A gd_currentldt_probe (nm_probe_fixed.txt): fixed.
  3. offset_check.c independently recomputes all 176 emitted values from the same headers as a separate TU β€” 176/176 match (diff.assym_vs_checker.txt; the only two extra lines are an artifact of the checker generator emitting the #if 0 pair).

Expected output

  • nm_probe_unpatched.txt: U GD_CURRENTLDT, missing probe symbol.
  • nm_probe_fixed.txt: 00000000000042d4 A gd_currentldt_probe.

No panic, no memory corruption β€” this is a build-contract defect whose security relevance is the proven silent-failure mode of the contract enforcement itself (the DF-5586/DF-5335 exception.S family shows what happens when this contract drifts in a field that is consumed).

VERDICT.md
↓ download raw

DF-5596 VERDICT

Status: reproduced (build-diff evidence class). Impact: none at runtime today; the defect is a silently-broken C↔assembly contract and a defeated build-time guard. Confidence: certain.

The claim under test

Pass-2 checklist item (b)/(c) for sys/platform/pc64/x86_64/genassym.c: "any missing ASSYM for fields the assembly touches" / "stale or removed-field references". An exhaustive extraction of every GD_*/TD_*/PCB_*/TF_*/TR_*/PM_*/LWP_*/RQF_*/IOAPIC_*/CPULOCK_*/CPUMASK_*/ MACHINTR_*/SPEC_CTRL_*/SIGF_*/TDF_*/TDPRI_* token from all assym.s consumers (13 platform .s files, 3 cpu .s files, 2 nvmm module .S files) against the 179 ASSYM definitions in genassym.c leaves exactly three unmatched:

  • PCB_USERLDT β€” swtch.s:571, inside #if 0 /* JG */ (dead code)
  • LWP_SWITCH_OPTIMIZE β€” swtch.s:97,110,189 (comment + #undef)
  • GD_CURRENTLDT β€” global.s:67, LIVE (this finding)

Reproduction (guest, DragonFly 6.5-DEVELOPMENT #0, cc 8.3)

Chain of evidence, all captured in this pack:

  1. Shipped-kernel proof. The running kernel's own build tree (/usr/obj/usr/src/sys/X86_64_GENERIC, source MD5-identical to the audit tree) contains global.o with U GD_CURRENTLDT and no gd_currentldt; the shipped /boot/kernel/kernel (EXEC) carries U GD_CURRENTLDT in its symbol table and zero relocations against it (shipped_kernel_evidence.txt). The build never failed.
  2. Controlled repro. Rebuilt genassym.o from pristine source with the exact kern.post.mk:76-79 command (make -V CFLAGS, minus -fno-common, plus -fcommon); genassym.sh regenerated assym.s byte-identical to the build tree's (diff.buildtree_vs_regen.txt β€” also proving determinism). probe_gdcl.s consumes GD_CURRENTLDT exactly as global.s:66-67 does: - unpatched assym β†’ nm_probe_unpatched.txt: U GD_CURRENTLDT, gd_currentldt_probe absent, assembly exit 0. Bug reproduced. - a displacement-style use (movq GD_NOSUCH(%rax),%rbx) also assembles cleanly (probe on guest: exit 0) β€” only the final link of a relocation-referenced undefined symbol would fail; .set-consumed macros never even get that far. The loud boundary is narrower than the genassym design assumes.
  3. Fix. One line β€” ASSYM(GD_CURRENTLDT, offsetof(struct mdglobaldata, gd_currentldt)); (fix.diff). Rebuilt with identical flags: diff.fix_delta.txt shows the regenerated assym.s gains exactly #define GD_CURRENTLDT 0x42d4 (sits correctly between gd_spending 0x42cc and gd_private_tss 0x42d8), and the same probe now defines 00000000000042d4 A gd_currentldt_probe (nm_probe_fixed.txt). Baseline bad behavior (missing symbol) is gone. A full kernel rebuild+reboot is not required for a non-corruption finding; the targeted same-flags rebuild exercises the exact production code path (kern.post.mk rules for genassym.o/assym.s and the .s compile flags verbatim β€” asmflags.txt).
  4. Contract-wide cross-check (checklist a). offset_check.c regenerates every ASSYM expression in a separate TU compiled with the kernel's own CFLAGS: 176/176 values match the generated assym.s (diff.assym_vs_checker.txt; the two + lines are SPEC_CTRL_IBRS/STIBP which genassym.c intentionally #if 0s β€” an artifact of my checker generator, not a mismatch). All td_/gd_/pcb_/trapframe/trampframe/ioapic offsets consumed by exception.S/swtch.s/ipl.s/locore.s/apic match the compiled kernel layout. TF_R10/TF_R11 are defined (genassym.c:152-153) and consumed (exception.S:305-306) β€” that lead resolves clean.

Why it matters / impact ceiling

  • Today: zero runtime effect β€” nothing references the missing symbol by relocation; C code touches gd_currentldt via mdcpu-> (e.g. mp_machdep.c:287). The .globl gd_currentldt export that ddb/kgdb scripts and future assembly would rely on simply does not exist, with no build diagnostic.
  • Systemically: the audit's premise for this file β€” "a mismatch is a silent memory-corruption class" β€” is usually checked by the build breaking. This case proves the check is not unconditional: an ASSYM consumed through .set vanishes silently from the shipped kernel. The same shape in a field that is consumed (e.g. someone "temporarily" removing an ASSYM that exception.S still uses through a .set alias) would not be caught at build time either. The 7 sibling dangling symbols from locore.s:41-47 (dmapbase, dmapend, loc_PTmap, … β€” extern variables from pmap.h that no longer exist) show the class is not hypothetical; they are a separate cleanup in locore.s/pmap.h, not genassym.c.
  • vkernel64 has the identical dangling use (sys/platform/vkernel64/x86_64/global.s:65); its genassym.c needs the same line if the export is to be real there (follow-up, out of this file's scope).

Classification

Reproduced: yes (the contract break manifests exactly as described, on the shipped kernel and under controlled rebuild). Exploit chain: none β€” no attacker-reachable primitive (build-time defect, no runtime consumer). Recommended fix applied and validated: fix.diff (one ASSYM line).

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 reproduced

Baseline: probe.o carries U GD_CURRENTLDT and no gd_currentldt definition (assembly+link succeed). Patched (one ASSYM line, identical build flags): assym.s gains exactly #define GD_CURRENTLDT 0x42d4 and probe.o defines absolute gd_currentldt_probe=0x42d4. Bad behavior gone; contract restored.

diff.fix_delta.txt, fix_gdcl_value.txt, nm_probe_unpatched.txt, nm_probe_fixed.txt
↓ fix.diffper-fix-DF-5596

Confirmed kernel references

Detail

Evidence (decisive lines)

shipped_kernel_evidence.txt (nm global.o + kernel: 'U GD_CURRENTLDT', gd_currentldt absent, 0 relocs); nm_probe_unpatched.txt (bug under controlled build); nm_probe_fixed.txt ('00000000000042d4 A gd_currentldt_probe'); diff.fix_delta.txt (exactly one added define); diff.buildtree_vs_regen.txt (empty: regen identical to build tree); diff.assym_vs_checker.txt (176/176 offsets match independent TU)

PoC changes

seed had no runnable PoC; authored probe_gdcl.s (mirrors global.s consumption shape), genassym_fixed.c (one added ASSYM line), offset_check.c (mechanically generated from genassym.c, separate-TU value cross-check). First runner attempt failed (-I. must resolve the kernel build dir; -nostdinc forbids stdio.h) -- fixed by cd $BD and a manual printf declaration; attempts=2.

Verified recommended fix

Add ASSYM(GD_CURRENTLDT, offsetof(struct mdglobaldata, gd_currentldt)); to genassym.c (fix.diff); optionally also audit-remove the dead locore.s:41-47 aliases.

Verdict

genassym.c never emits GD_CURRENTLDT although struct mdglobaldata still has gd_currentldt (globaldata.h:78) and the standard-built global.s:66-67 consumes it via .globl/.set. Proven on the shipped kernel: global.o and /boot/kernel/kernel both carry an undefined GD_CURRENTLDT, the promised gd_currentldt export silently does not exist, zero relocations consume the dangling symbol, and the build never failed -- the genassym C<->asm contract guard is silently defeated for .set-consumed macros under this link configuration. No runtime memory-safety impact today (nothing references the missing symbol); the security relevance is the demonstrated silent-failure mode of the offset-contract enforcement itself. One-line fix validated with the exact kernel build rules: regenerated assym.s gains #define GD_CURRENTLDT 0x42d4 and the consumer probe resolves to an absolute symbol 0x42d4.