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

vkernel64 genassym.c emits only 5 of the 26 GD_* macros global.s consumes β€” 13 restorable per-CPU exports (gd_currentldt, gd_reqflags, gd_common_tss, gd_savefpu, gd_fpu_lock, gd_fpending/gd_ipending/gd_spending, gd_cnt, gd_ss_eflags, gd_intr_nesting_level, gd_common_tssd, gd_tss_gdt) silently vanish from every VKERNEL64 build (DF-5596 twin at 21x scale, task_log lead confirmed)

Summary

global.s builds its .globl export block from 26 GD_* macros but genassym defines only 5: gas resolves each missing macro as an undefined external, the .set alias AND its .globl export silently vanish, assembly exits 0 under -Werror, and the vkernel's userland cc link tolerates zero-relocation undefineds. Proven end-to-end on the real build tree: nm shows U for all 21 missing macros in global.o AND the final binary, link exit 0. 13 of the 21 back existing mdglobaldata fields and are restored by fix.diff (57 defines, 18 exports, 0 dangling, values independent-TU verified). No runtime consumer today, but this is the C-asm offset contract's only build-time guard failing silently at scale on the platform with the weakest link gate. DF-5596 twin at 21x; the same shape in a relocation-consumed macro is the silent memory-corruption class.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5658 Β· 28 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 8.0 KB ↓ raw
verdict.json β€” 4.7 KB view raw
manifest.json β€” 1.5 KB view raw
offset_check.c β€” 6.7 KB view raw
offset_check.out β€” 1.2 KB view raw
nm_global_pristine.txt β€” 1.0 KB view raw
final_binary_baseline.txt β€” 1.1 KB view raw
final_binary_fixed.txt β€” 688 B view raw
assym.pristine.s β€” 1022 B view raw
assym.fixed.s β€” 1.3 KB view raw
checker.cmd β€” 885 B ↓ download
checker_build.log β€” 0 B ↓ download
build.log β€” 2.0 KB view raw
run.log β€” 2.2 KB view raw
vkbuild.log β€” 497.5 KB ↓ download
link.log β€” 53 B view raw
link2.log β€” 0 B ↓ download
pristbuild.log β€” 1.9 KB view raw
fixbuild.log β€” 1.9 KB view raw
fixbuild2.log β€” 994 B view raw
fix_build.log β€” 2.9 KB view raw
fix_run.log β€” 852 B view raw
genassym.fixed.c β€” 6.1 KB view raw
global.fixed.s β€” 3.6 KB view raw
fix.diff β€” 2.7 KB view raw
hashes.txt β€” 156 B view raw
env.txt β€” 563 B view raw

DF-5658 β€” vkernel64 genassym.c emits only 5 of the 26 GD_ macros global.s consumes: 13 silently-vanished .set exports for fields that EXIST (DF-5596 twin, 13Γ—), plus the 8 GD_PRV_ dangles split out as DF-5659

Evidence class: build-diff on the guest's VKERNEL64 build tree (the vkernel is a user program linked with plain cc β€” see sys/platform/vkernel64/conf/Makefile:23-25).

What to run

Everything was executed on the QEMU guest (DragonFly 6.5-DEVELOPMENT, env.txt) whose /usr/src is MD5-identical to the audit tree for all 9 files involved (genassym.c, global.s, swtch.s, locore.s, fork_tramp.s, conf/files, VKERNEL64 config, globaldata.h, thread.h β€” MD5s in env.txt).

# 1. create the VKERNEL64 build tree
cd /usr/src/sys/config && config VKERNEL64          # -> /usr/src/sys/compile/VKERNEL64
cd /usr/src/sys/compile/VKERNEL64

# 2. regen assym.s through the REAL kern.post.mk rules (see build.log)
make assym.s global.o                                # both exit 0, -Werror
nm global.o                                          # -> nm_global_pristine.txt / run.log

# 3. independent value check (offset_check.c, exact genassym CFLAGS)
sh $(sed 's|genassym.c|/tmp/offset_check.c|' checker.cmd) -Wno-nested-externs -o /tmp/oc.o
cc /tmp/oc.o -o /tmp/oc && /tmp/oc                    # -> offset_check.out (65 lines)

# 4. end-to-end link proof (build-id stub works around the unrelated
#    kern_mib.c __build_id buster β€” see VERDICT.md "Incidental")
cc -g -Wl,--export-dynamic -o /tmp/kernel.dangleproof $(make -V SYSTEM_OBJS) \
   vers.o /tmp/bidstub.o -pthread -Wl,--hash-style=sysv -Wl,-z,max-page-size=0x200000
nm /tmp/kernel.dangleproof | grep ' U GD_'           # -> 21 undefined
objdump -r /tmp/kernel.dangleproof | grep -c GD_     # -> 0 relocations

# 5. fix validation
cd /usr/src && patch -p1 < fix.diff                  # genassym.c +13 ASSYM, global.s -8 PRV
cd sys/compile/VKERNEL64 && rm -f genassym.o assym.s global.o
make assym.s global.o locore.o swtch.o fork_tramp.o  # -> fix_build.log
# relink as in step 4 -> /tmp/kernel.fixed
nm /tmp/kernel.fixed | grep -c ' U GD_'              # -> 0
nm /tmp/kernel.fixed | grep -E ' [ADB] gd_'          # -> 18 exports (fix_run.log)

Expected results

Step pristine patched
assym.s defines 44 57
global.o defined gd_*/globaldata 5+1 18+1
global.o undefined ASSYM macros 21 (U GD_CURRENTLDT, …) 0
final binary undefined GD_* 21 0
final binary gd_* exports 5 18
relocations against GD_* 0 0
checker vs assym.s values 44/44 match β€”

Key restored values (independent-TU verified, offset_check.out): gd_currentldt=0x3f14 (right after gd_spending=0x3f0c+gd_sdelayed=0x3f10, matching globaldata.h:74-78 field order), gd_reqflags=0x18, gd_cnt=0xa8, gd_intr_nesting_level=0xa0, gd_common_tssd=0x3a80, gd_tss_gdt=0x3a88, gd_common_tss=0x3a98, gd_savefpu=0x3b00, gd_fpu_lock=0x3f00, gd_fpending=0x3f04, gd_ipending=0x3f08, gd_ss_eflags=0x3f48.

Files

  • run.log / nm_global_pristine.txt / final_binary_baseline.txt β€” the dangle proof (object + final binary)
  • build.log / checker.cmd / checker_build.log β€” regen + independent-TU build
  • offset_check.c / offset_check.out β€” 44 ASSYM values verified; CHECK_*/MISSING_* extras
  • assym.pristine.s / assym.fixed.s β€” 44 β†’ 57 defines
  • vkbuild.log β€” FULL untrimmed 547-object VKERNEL64 build incl. the link failure (__build_id_*)
  • link.log / link2.log β€” baseline/patched final-binary links
  • fixbuild.log / fixbuild2.log / fix_run.log / final_binary_fixed.txt β€” fix validation
  • fix.diff β€” git-apply-clean against pristine sys/ (verified with git apply --check)
  • env.txt β€” guest uname/cc/ncpu + source MD5 identity
VERDICT.md
↓ download raw

DF-5658 VERDICT

Status: reproduced (build-diff evidence class). Impact: none at runtime today β€” no relocation or C consumer references the vanished exports; the defect is a silently-broken C↔assembly contract at 21Γ— the scale of the pc64 twin (DF-5596), on the platform whose link (plain userland cc) has the weakest undefined-symbol gate. Confidence: certain.

The claim under test

Pass-2 task_log lead (2026-10-08): "vkernel64 global.s:65 has the identical GD_CURRENTLDT dangle". Verified β€” and it is not alone: an exhaustive used-vs-defined inventory over ALL assym.s consumers in the VKERNEL64 build (global.s, locore.s, swtch.s, fork_tramp.s β€” plus in_cksum2.s/monitor.s/ bzeront.s which include assym.s but consume no macros) shows:

macro field exists? location
GD_REQFLAGS yes β€” mi.gd_reqflags global.s:54
GD_NPXTHREAD yes (defined) global.s:53
GD_COMMON_TSSD yes β€” globaldata.h:68 global.s:61
GD_TSS_GDT yes β€” globaldata.h:69 global.s:62
GD_CURRENTLDT yes β€” globaldata.h:78 global.s:65 ← the lead
GD_FPU_LOCK yes β€” globaldata.h:73 global.s:68
GD_SAVEFPU yes β€” globaldata.h:72 global.s:69
GD_SS_EFLAGS yes β€” globaldata.h:83 global.s:84
GD_INTR_NESTING_LEVEL yes β€” mi.gd_intr_nesting_level global.s:85
GD_FPENDING/GD_IPENDING/GD_SPENDING yes β€” globaldata.h:74-76 global.s:94-96
GD_CNT yes β€” mi.gd_cnt global.s:97
GD_PRV_CMAP1-3/PMAP1/CADDR1-3/PADDR1 NO β€” absent from vkernel64 struct privatespace global.s:77-78,86-93 β†’ DF-5659

Every live displacement use in the real consumers resolves (swtch.o, fork_tramp.o, locore.o: zero ASSYM-shaped undefined symbols β€” the PCB_CR3/PCB_EXT/MSR_FSBASE/MSR_KGSBASE/PCB_USERLDT uses in swtch.s:348,369, 429,440,464 are all inside #if 0 dead blocks). The entire defect class is concentrated in global.s's .set export block, where gas resolves the missing macro to an undefined external symbol: the alias and its .globl export silently vanish, assembly exits 0, -Werror stays silent.

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

  1. Source identity. All 9 involved files MD5-identical between audit tree and guest /usr/src (env.txt).
  2. Real-rule regen. config VKKERNEL64 β†’ /usr/src/sys/compile/VKERNEL64; make assym.s via kern.post.mk:73-79 emitted exactly 44 defines (assym.pristine.s), regeneration byte-identical (determinism). make global.o (kern.post.mk .s rule, -Werror) β†’ exit 0, and nm global.o (nm_global_pristine.txt): exactly 5 backed exports (gd_curthread=0x8, gd_cpuid=0x48, gd_cpumask=0x50, gd_npxthread=0x3a90, gd_other_cpus=0x70) + U for all 21 missing macros β€” U GD_CURRENTLDT among them, the lead confirmed at the object level β€” while gd_reqflags, gd_common_tss, gd_currentldt, … (the 21 .globl exports) are absent from nm entirely.
  3. End-to-end link. The vkernel is linked as a user program (conf/Makefile:23-25: cc -Wl,--export-dynamic … -pthread, no -T ldscript). Linking all 547 SYSTEM_OBJS + vers.o that way (link.log/final_binary_baseline.txt) β†’ exit 0 with the 21 U GD_* riding into the final binary with zero relocations (objdump -r/readelf -r β†’ 0 GD_* entries) β€” precisely why the userland link never fails on them.
  4. Fix. fix.diff adds the 13 restorable ASSYM lines to genassym.c and deletes the 8 GD_PRV_ aliases from global.s (DF-5659). Rebuilt with the same rules (fix_build.log): assym.s 44β†’57 defines (assym.fixed.s); global.o now exports 18 gd_* symbols with zero undefined ASSYM macros; relinked final binary (fix_run.log, final_binary_fixed.txt): 0 U GD_*, 18 exports*, link exit 0. Every restored value matches the independent TU checker exactly, e.g. gd_currentldt=0x3f14 β€” struct-consistent (gd_spending 0x3f0c + gd_sdelayed 0x3f10 + int β†’ 0x3f14, globaldata.h:74-78).

Contract-wide cross-checks (pass-2 checklist)

  • (b) offset-vs-struct correctness: offset_check.c re-evaluates every one of the 44 emitted ASSYM expressions in a separate TU compiled with the exact genassym CFLAGS β€” 44/44 values match assym.s (only the 0x presentation differs). No stale offsets. Negative result, with evidence.
  • (c) used-vs-defined inventory: every CAPS macro token extracted from all 4 platform .s consumers + 3 cpu/x86_64/misc .s consumers; the ONLY unmatched set is the 21 GD_* in global.s. swtch.s's MSR_FSBASE/MSR_KGSBASE uses are inside #if 0 and additionally resolvable from specialreg.h (swtch.s includes / chains).
  • (d) vkernel-specific fields: TD_SAVEFPU=0x628 verified == offsetof(struct thread, td_mach.mtd_savefpu) (thread.h:43,49), consumed live by swtch.s:558,701 β€” correct. The DF-5655 TLS-aliasing chain (td_tls == td_mach.mtd_savevext.vx_tls, thread.h:50) is C-side only: CHECK_td_tls=CHECK_mtd_savevext=0x630, CHECK_vx_tls=0, and NO assembly consumer references it (swtch.s's TLS-restore block at :477-484 is #if 0). No genassym exposure β€” negative result, chain documented.
  • DF-5597 twin (ioapic stride): zero IOAPIC_* tokens in any vkernel64 .s consumer β€” not applicable on this platform.

Incidental discovery (out-of-file lead, task_log'd)

The full VKERNEL64 build fails at link on kern_mib.c:111/132 β†’ undefined reference to '__build_id_start/__build_id_end' (vkbuild.log): kern.pre.mk's default SYSTEM_LD passes -Wl,-T,$S/platform/$P/conf/ldscript.$M (which PROVIDEs both symbols, vkernel64/conf/ldscript.x86_64:75), but vkernel64's conf/Makefile SYSTEM_LD override drops -T, so the vkernel64 kernel has been unbuildable since the build-id sysctl landed. This failure is also what empirically delimits the DF-5596 mechanism on the vkernel link: the link DOES fail hard on relocation-referenced undefined symbols (__build_id_ has relocations) while tolerating the 21 zero-relocation GD_ dangles silently.

Why it matters / impact ceiling

  • Today: zero runtime effect β€” nothing references the vanished symbols by relocation; C code uses mdcpu-> (and vkernel64's set_user_ldt panics, userldt.c:44 β€” no LDT support). The .globl exports that ddb/kgdb scripts and future assembly would rely on simply do not exist, on the platform whose debugger (in-process ddb) reads the vkernel's own symtab.
  • Systemically: the genassym contract is the ONLY build-time guard for the C↔asm offset agreement (the silent memory-corruption class this audit family tracks β€” DF-5586/5335 lineage). This file shows the guard failing silently 21 ways at once on the platform with the weakest link gate, and 8 of the 21 cannot even be "restored" (fields absent) β€” a maintainer attempting the obvious fix hits a compile error and is likely to abandon all 21.

Classification

Reproduced: yes β€” object level (nm global.o), final-binary level (21 U, 0 relocations, link exit 0), and controlled fix (0 U, 18 exports, values independent-TU verified). Exploit chain: none β€” build-time contract defect, no attacker-reachable primitive. Recommended fix validated: fix.diff (git-apply-clean against pristine sys/, never applied to the audit tree).

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

Baseline: 21 undefined GD_ symbols in global.o and the final VKERNEL64 binary, only 5 backed gd_ exports, zero build diagnostics. After fix.diff (guest /usr/src copy, same kern.post.mk rules): assym.s 44->57 defines, global.o exports all 18 field-backed gd_ symbols with zero undefined ASSYM macros, final relinked binary has 0 U GD_ and 18 exports, every value matching the independent-TU checker (gd_currentldt=0x3f14). Previously-observed bad behavior gone; link green.

fix.diff (git-apply-clean vs pristine sys/), fixbuild.log, fixbuild2.log, fix_run.log, final_binary_fixed.txt, assym.fixed.s vs assym.pristine.s
↓ fix.diffper-fix-DF-5658

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log + nm_global_pristine.txt (U GD_CURRENTLDT + 20 siblings at object level); final_binary_baseline.txt (21 U GD_*, 0 relocations, link exit 0); assym.pristine.s (44 defines) vs assym.fixed.s (57); fix_run.log + final_binary_fixed.txt (0 U, 18 exports after fix); offset_check.out (44/44 emitted values independently verified + MISSING_* targets); vkbuild.log (full untrimmed build incl. the unrelated __build_id link buster that delimits the tolerance mechanism)

PoC changes

No seed PoC existed (pass-2 lead). Authored offset_check.c (independent-TU re-evaluation of all 44 ASSYM expressions + CHECK_/MISSING_ extras) and the build-tree regeneration/probe procedure; had to relax -Werror/-Wnested-externs for the checker harness only (production flags untouched).

Verified recommended fix

Add the 13 ASSYM(GD_, offsetof(struct mdglobaldata, ...)) lines to vkernel64 genassym.c and delete the 8 GD_PRV_ aliases from global.s (fix.diff, validated)

Verdict

Verified on the guest's VKERNEL64 build tree (source MD5-identical): global.s consumes 26 GD_ macros but genassym.c defines only 5, so 21 .set exports silently vanish β€” nm on the real-rule-built global.o shows U for all 21 missing macros (incl. the task_log lead GD_CURRENTLDT at global.s:65) with the .globl'd gd_ aliases absent entirely, assembly exit 0 under -Werror; the exact SYSTEM_LD userland-cc link completes exit 0 with all 21 U GD_ riding into the final vkernel64 binary carrying ZERO relocations, which is precisely why nothing ever fails. 13 of the 21 are restorable (fields exist in struct mdglobaldata); the other 8 (GD_PRV_) alias fields that do not exist on this platform and are split to DF-5659. fix.diff (13 ASSYM additions + 8 alias deletions) validated: assym.s 44->57 defines, global.o exports 18 gd_ with zero undefined ASSYM macros, relinked final binary 0 U GD_ / 18 exports, every restored value matching an independent-TU checker (gd_currentldt=0x3f14 struct-consistent).