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)
PoC verification
Evidence pack
findings/poc/DF-5596 Β· 26 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
struct mdglobaldatastill has the field:sys/platform/pc64/include/globaldata.h:78βint gd_currentldt;- a standard-built assembly file still consumes the macro:
sys/platform/pc64/x86_64/global.s:66-67asm .globl gd_currentldt .set gd_currentldt,globaldata + GD_CURRENTLDT(sys/platform/pc64/conf/files:129βplatform/pc64/x86_64/global.s standard)
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)
sh build.shβ compiles, with the exact kernel build flags: - pristinegenassym.cβgenassym.oβ regeneratedassym.s(byte-identical to the build tree's βdiff.buildtree_vs_regen.txt), -probe_gdcl.s(consumesGD_CURRENTLDTexactly likeglobal.s:67) against that assym βnmshowsU GD_CURRENTLDTand nogd_currentldt_probesymbol: the bug, reproduced under control.- 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 yields00000000000042d4 A gd_currentldt_probe(nm_probe_fixed.txt): fixed. offset_check.cindependently 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 0pair).
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).
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:
- 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) containsglobal.owithU GD_CURRENTLDTand nogd_currentldt; the shipped/boot/kernel/kernel(EXEC) carriesU GD_CURRENTLDTin its symbol table and zero relocations against it (shipped_kernel_evidence.txt). The build never failed. - Controlled repro. Rebuilt
genassym.ofrom pristine source with the exact kern.post.mk:76-79 command (make -V CFLAGS, minus-fno-common, plus-fcommon);genassym.shregeneratedassym.sbyte-identical to the build tree's (diff.buildtree_vs_regen.txtβ also proving determinism).probe_gdcl.sconsumesGD_CURRENTLDTexactly asglobal.s:66-67does: - unpatched assym βnm_probe_unpatched.txt:U GD_CURRENTLDT,gd_currentldt_probeabsent, 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. - Fix. One line β
ASSYM(GD_CURRENTLDT, offsetof(struct mdglobaldata, gd_currentldt));(fix.diff). Rebuilt with identical flags:diff.fix_delta.txtshows the regenerated assym.s gains exactly#define GD_CURRENTLDT 0x42d4(sits correctly betweengd_spending 0x42ccandgd_private_tss 0x42d8), and the same probe now defines00000000000042d4 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). - Contract-wide cross-check (checklist a).
offset_check.cregenerates 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). Alltd_/gd_/pcb_/trapframe/trampframe/ioapicoffsets 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_currentldtviamdcpu->(e.g. mp_machdep.c:287). The.globl gd_currentldtexport 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
.setvanishes 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.setalias) 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
fixedBaseline: 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
Confirmed kernel references
- sys/platform/pc64/x86_64/genassym.c:230-239
- sys/platform/pc64/x86_64/global.s:66-67
- sys/platform/pc64/include/globaldata.h:78
- sys/platform/pc64/conf/files:129
- sys/conf/kern.post.mk:73-79
- sys/kern/genassym.sh:10-44
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.
No comments yet.