lapic_set_cpuid missing bounds validation allows OOB write to APIC ID mapping arrays via crafted MADT
| Field | Value |
|---|---|
| ID | DF-1042 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/platform/pc64/apic/lapic.c |
| Lines | 1212-1217 (called from acpi_madt.c:342 and mptable.c:566/572) |
| Area | platform/pc64/apic (x86 LAPIC init) |
| Confidence | certain |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
lapic_set_cpuid() uses its cpu_id and apic_id parameters as raw indices into the
256-element global arrays cpu_id_to_apic_id[NAPICID] and apic_id_to_cpu_id[NAPICID]
(lapic.c:122-123) with no bounds check. The x2APIC MADT enumeration callback
(acpi_madt.c:342) passes x2apic_ent->LocalApicId (a UINT32 that can exceed 255)
directly as apic_id, and the cpu counter increments without any MAXCPU cap. This causes
out-of-bounds writes to kernel BSS during boot when the firmware-provided MADT contains
x2APIC entries with LocalApicId >= 256 or more than 256 enabled CPU entries.
Root cause
lapic_set_cpuid() at sys/platform/pc64/apic/lapic.c:1212-1217:
void
lapic_set_cpuid(int cpu_id, int apic_id)
{
CPUID_TO_APICID(cpu_id) = apic_id; /* cpu_id_to_apic_id[(cpu_id)] */
APICID_TO_CPUID(apic_id) = cpu_id; /* apic_id_to_cpu_id[(apic_id)] */
}
Both arrays are declared at lapic.c:122-123 as int[NAPICID] where NAPICID=256
(apicvar.h:41). Neither parameter is validated against [0, NAPICID).
The x2APIC MADT pass2 callback at sys/platform/pc64/acpica/acpi_madt.c:342 calls:
lapic_set_cpuid(cpu, x2apic_ent->LocalApicId);
where LocalApicId is UINT32 (ACPI spec / actbl2.h:1760) and can be any 32-bit value.
The probe phase (acpi_madt.c:413) filters entries with LocalApicId >= APICID_MAX (255)
and only counts those < 255, but the enumeration phase (acpi_madt.c:330-342) processes
all enabled x2APIC entries regardless of LocalApicId value. This probe/enumerate
inconsistency is the core defect.
Additionally, the cpu counter in madt_x2apic_pass2_callback (acpi_madt.c:337-338)
increments arg->cpu++ for every enabled non-BSP entry without any cpu < MAXCPU
check โ unlike the mptable path which guards with else if (cpu < MAXCPU) at
mptable.c:571. If the MADT contains more than 256 enabled entries, cpu reaches 256 and
cpu_id_to_apic_id[256] is an OOB write.
The naps cap (lapic.c:1185-1193) happens after the enumeration iteration
completes, so it does not prevent the OOB writes during iteration.
Threat model & preconditions
- Attacker position: Control of firmware-provided ACPI MADT tables. Realistic scenarios: 1. A VM hypervisor (e.g. QEMU/KVM host) crafts a malicious MADT presented to a DragonFlyBSD guest โ the host controls all guest ACPI tables. 2. Compromised or buggy UEFI firmware producing sparse x2APIC IDs โฅ 256 (x2APIC was designed to support IDs > 255 on large multi-socket systems, so this is realistic). 3. A MADT with >256 enabled CPU entries.
- Privileges gained or impact: Out-of-bounds write to kernel BSS with a small positive
integer value (CPU ID 0-255) at an attacker-controllable offset (via
LocalApicId). The arrays atlapic.c:122-123are immediately followed in source order bylapic_enable(124),lapic_usable(125),x2apic_enable(126) โ a small overflow corrupts these globals; a largeLocalApicId(up to 2ยณยฒ) can write to arbitrary kernel memory, causing panic (DoS) or potentially corrupting security-relevant state. - Required config or capabilities: Default kernel booting with ACPI MADT
(
hw.acpi.enable=1, the default on x86-64). Attacker must control the MADT itself. - Reachability: Path is reached during
SI_BOOT2_LAPIC(lapic.c:1290) โlapic_config()โmadt_lapic_enumerate()โmadt_lapic_pass2()โmadt_x2apic_pass2_callback()โlapic_set_cpuid().
Proof of concept
PoC source: findings/poc/DF-1042/malicious_madt.aml (generator script) and
findings/poc/DF-1042/run.sh
Build & run
# Generate a malicious MADT ACPI table where one x2APIC entry has
# LocalApicId = 0x100 (>= 256) so the lapic_set_cpuid OOB write triggers.
python3 findings/poc/DF-1042/gen_madt.py > malicious_madt.aml
# Boot DragonFlyBSD under QEMU with the custom table.
qemu-system-x86_64 -enable-kvm -m 512 -smp 2 \
-acpitable file=malicious_madt.aml \
-kernel /path/to/dfbsd-kernel -initrd /path/to/initrd
Entry 0 in the crafted MADT: Type=LOCAL_X2APIC, LocalApicId=0, Uid=0, LapicFlags=ENABLED
(BSP, passes probe). Entry 1: Type=LOCAL_X2APIC, LocalApicId=256, Uid=1,
LapicFlags=ENABLED (triggers OOB write at apic_id_to_cpu_id[256]).
Alternative trigger without x2APIC: a MADT with 257+ enabled LOCAL_APIC entries causes
cpu to reach 256, writing to cpu_id_to_apic_id[256] (OOB on the cpu_id index). The
mptable path is safe (mptable.c:571 guards cpu < MAXCPU); the MADT path is not.
Expected output
[early boot] MADT: cpu id 0, acpi uid 0, apic id 0 MADT: cpu id 1, acpi uid 1, apic id 256 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x<address past apic_id_to_cpu_id> lapic_set_cpuid(...) at lapic.c:1216 madt_x2apic_pass2_callback(...) at acpi_madt.c:342 ...
With LocalApicId = 0x1000, the write lands 4096 ints (16 KB) past the array, likely
hitting other critical data and producing an immediate panic. Smaller overflows may instead
corrupt adjacent globals (lapic_enable, lapic_usable, x2apic_enable) and produce
later malfunction rather than an immediate panic.
Impact
Local DoS (and potentially integrity impact) at boot when firmware/ACPI tables are attacker-controlled. The high-privilege prerequisite (control of the MADT/boot environment) keeps this at Medium rather than High per the rubric, but the OOB write is real, the path is the default x86-64 boot flow, and the defense-in-depth fix is small and obvious. Real-world exposure includes compromised hypervisor hosts (cloud tenants cannot trust the host) and malicious UEFI firmware.
Recommended fix
Add bounds validation to lapic_set_cpuid() so it cannot write past either array, and skip
invalid entries. The defensive fix belongs in lapic_set_cpuid itself since it is the
single function performing the unchecked array writes:
--- a/sys/platform/pc64/apic/lapic.c
+++ b/sys/platform/pc64/apic/lapic.c
@@ -1212,7 +1212,15 @@ lapic_enumerator_register(struct lapic_enumerator *ne)
void
lapic_set_cpuid(int cpu_id, int apic_id)
{
+ if (cpu_id < 0 || cpu_id >= NAPICID ||
+ apic_id < 0 || apic_id >= NAPICID) {
+ kprintf("lapic_set_cpuid: invalid cpu_id %d apic_id %d, skipping\n",
+ cpu_id, apic_id);
+ return;
+ }
CPUID_TO_APICID(cpu_id) = apic_id;
APICID_TO_CPUID(apic_id) = cpu_id;
}
Additionally, the MADT pass2 callbacks in acpi_madt.c should skip entries with
LocalApicId >= APICID_MAX (matching the probe filter at acpi_madt.c:413) and cap the cpu
counter at MAXCPU, but the lapic.c fix is the authoritative defense-in-depth boundary.
References
- ACPI Specification, MADT subtables (
ACPI_MADT_LOCAL_X2APIC),LocalApicIdis UINT32 - Intelยฎ 64 and IA-32 Architectures Software Developer's Manual, Vol 3, ยง10 (Local APIC, x2APIC ID space 0โฆ2ยณยฒ)
sys/platform/pc64/apic/lapic.c:122-123โ 256-element mapping arrayssys/platform/pc64/acpica/acpi_madt.c:330-342โ pass2 callback, no bounds checksys/platform/pc64/acpica/acpi_madt.c:413โ probe-phase filter the enumeration phase fails to mirrorsys/platform/pc64/x86_64/mptable.c:571โ the safe pattern (else if (cpu < MAXCPU))
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1042 ยท 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df1042_harness.c | trigger-source | kld harness: calls real lapic_set_cpuid(1,256) and observes BSS corruption (sanctioned primitive-proof for a boot-only path) | 3.6 KB | view raw |
| Makefile | trigger-source | kld module build (bsd.kmod.mk) | 78 B | โ download |
| gen_madt.py | trigger-source | emits the conceptual attacker MADT (LocalApicId=256) a malicious hypervisor/UEFI would supply | 2.4 KB | view raw |
| malicious_madt.aml | trigger-source | prebuilt 88-byte malicious MADT (BSP id=0 + malicious id=256) | 88 B | โ download |
| build.sh | build-script | build the kld harness + regenerate the MADT | 1.1 KB | view raw |
| run.sh | run-script | kldload the harness (root), dump the OOB/corruption dmesg | 1.1 KB | view raw |
| harness_unpatched_dmesg.txt | run-log | UNPATCHED #0: cpu_id_to_apic_id[0] 0->1, OOB WRITE CONFIRMED | 1.1 KB | view raw |
| harness_patched_dmesg.txt | run-log | PATCHED #1: 'skipping' + cpu_id_to_apic_id[0] unchanged (run twice) | 1.2 KB | view raw |
| fix_build.log | build-log | full single-fix nativekernel build output, NK_DONE rc=0 | 5.6 MB | โ download |
| fix.diff | suggested-fix | bounds check in lapic_set_cpuid + MADT pass2 cpu/apic_id guards (supersedes finding proposal) | 1.2 KB | view raw |
| env.txt | environment | uname / kern.version (#1) / cc 8.3 | 433 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, dynamic proof, fix validation | 8.8 KB | โ raw |
| README.md | readme | original reviewer README (trigger description) | 2.5 KB | โ raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | โ download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1042 PoC โ lapic_set_cpuid OOB write via crafted MADT
Trigger
Crafted ACPI MADT table containing x2APIC entries that exercise the missing
bounds check in lapic_set_cpuid(). Two variants:
Variant A โ large LocalApicId
Generate a MADT with one BSP entry that has LocalApicId=0 (passes probe
phase) and one entry with LocalApicId=256 (triggers OOB write at
apic_id_to_cpu_id[256]).
Variant B โ many enabled entries
Generate a MADT with 257+ enabled LOCAL_APIC entries; the cpu counter
reaches 256 and cpu_id_to_apic_id[256] is written OOB.
Generate the MADT
gen_madt.py emits a minimal MADT table with the header, one LAPIC address
override, the BSP x2APIC entry, and one malicious x2APIC entry with
LocalApicId=256. Output is ready for QEMU's -acpitable file=....
The MADT subtable layout (ACPI_MADT_LOCAL_X2APIC):
- Type (u8) = 9 (LOCAL_X2APIC)
- Length (u8) = 16
- Reserved (u16)
- LocalApicId (u32) โ attacker-controlled
- Uid (u32)
- LapicFlags (u32) bit 0 = ENABLED
Run:
python3 gen_madt.py > malicious_madt.aml
Run under QEMU
qemu-system-x86_64 -enable-kvm -m 512 -smp 2 \
-acpitable file=malicious_madt.aml \
-kernel /path/to/dfbsd-kernel \
-initrd /path/to/initrd \
-serial stdio -display none
Expected output (Variant A)
[early boot] MADT: cpu id 0, acpi uid 0, apic id 0 MADT: cpu id 1, acpi uid 1, apic id 256 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x<address past apic_id_to_cpu_id> lapic_set_cpuid(...) at lapic.c:1216 madt_x2apic_pass2_callback(...) at acpi_madt.c:342 madt_lapic_pass2(...) at acpi_madt.c:... lapic_config(...) at lapic.c:1290
Notes
- This is a boot-time firmware-data trigger, not a userspace exploit. The "attacker" must control the firmware/ACPI tables (malicious VM host, malicious UEFI, or a SeaBIOS patch).
- An alternate proof without QEMU is a unit test that constructs a fake
ACPI_MADT_LOCAL_X2APICand callsmadt_x2apic_pass2_callbackdirectly withLocalApicId >= 256, asserting thatapic_id_to_cpu_id[256]was written.
Kernel references
sys/platform/pc64/apic/lapic.c:1212-1217โlapic_set_cpuid(no bounds check)sys/platform/pc64/apic/lapic.c:122-123โ 256-element mapping arrayssys/platform/pc64/acpica/acpi_madt.c:330-342โ pass2 callback, no bounds checksys/platform/pc64/acpica/acpi_madt.c:413โ probe filter the enumeration phase fails to mirrorsys/platform/pc64/x86_64/mptable.c:571โ safe pattern (else if (cpu < MAXCPU))
DF-1042 โ lapic_set_cpuid missing bounds validation โ OOB BSS write via crafted MADT
Verdict
REPRODUCED (boot-time firmware-data OOB write; not userspace-reachable). Fix VALIDATED on a single-fix kernel (#1): the OOB is gone.
Mechanism (confirmed, path:line at every hop)
lapic_set_cpuid(int cpu_id, int apic_id) at
sys/platform/pc64/apic/lapic.c:1212-1217 writes its two parameters directly
as indices into the 256-element globals with no bounds check:
void lapic_set_cpuid(int cpu_id, int apic_id) {
CPUID_TO_APICID(cpu_id) = apic_id; /* cpu_id_to_apic_id[cpu_id] */
APICID_TO_CPUID(apic_id) = cpu_id; /* apic_id_to_cpu_id[apic_id] */
}
CPUID_TO_APICID/APICID_TO_CPUIDexpand to raw array indexing (sys/platform/pc64/apic/lapic.h:37-38).- The arrays are
int[NAPICID],NAPICID = 256(apicvar.h:41), declared atlapic.c:122-123. Index 256 is out of bounds.
The x2APIC MADT pass2 callback madt_x2apic_pass2_callback
(acpi_madt.c:320-346) is the reachable sink:
cpu = arg->cpu; arg->cpu++; /* acpi_madt.c:337-338 -- UNcapped */
...
lapic_set_cpuid(cpu, x2apic_ent->LocalApicId); /* acpi_madt.c:342 -- u32, unchecked */
CPUID_TO_ACPIID(cpu) = x2apic_ent->Uid; /* acpi_madt.c:343 -- also unbounded */
x2apic_ent->LocalApicIdis aUINT32(ACPI spec) and can be any 32-bit value; it is passed straight through asapic_id. โ apic_id-dimension OOB.arg->cpustarts at 1 (acpi_madt.c:364) and is incremented for every enabled non-BSP entry with nocpu < MAXCPUcheck, unlike the mptable path which guards withelse if (cpu < MAXCPU)atmptable.c:571. With256 enabled entries
cpureaches 256 โ cpu-dimension OOB (and the adjacentCPUID_TO_ACPIID(cpu)write tocpu_id_to_acpi_id[NAPICID](acpi_madt.c:83) is likewise unbounded).MAXCPU == SMP_MAXCPU == NAPICID == 256(sys/cpu/x86_64/include/param.h:71-72).- The probe phase filters
LocalApicId < APICID_MAX(acpi_madt.c:413) and only setsmadt_use_x2apicwhen no legacy LOCAL_APIC entries exist (acpi_madt.c:460-476); pass2 then processes every enabled x2APIC entry regardless ofLocalApicId. This probe/enumerate inconsistency is the core defect. Thenapscap (lapic.c:1183-1193) runs after iteration, so it does not prevent the OOB writes during iteration.
Reachability & threat model (why this is boot-time, not userspace)
A full call-site audit (grep -rn 'lapic_set_cpuid(' sys/) shows the function
is invoked only from the boot-time LAPIC enumerators:
sys/platform/pc64/acpica/acpi_madt.c:314(MADT legacy LOCAL_APIC pass2)sys/platform/pc64/acpica/acpi_madt.c:342(MADT LOCAL_X2APIC pass2)sys/platform/pc64/x86_64/mptable.c:566,572,728,730(MP-table; safe โ guarded bycpu < MAXCPU)
These run once during SI_BOOT2_LAPIC (lapic.c:1290) โ lapic_config().
There is no userspace syscall, ioctl, or runtime path to lapic_set_cpuid.
The attacker must therefore control the firmware-provided ACPI MADT โ i.e. a
malicious VM hypervisor host (the host owns all guest ACPI tables), malicious
UEFI, or a SeaBIOS/firmware compromise. This is a legitimate but high-bar
precondition; it is correctly scoped Medium, not High.
No escalation chain is applicable: there is no unprivileged trigger, so the "turn corruption into uid=0" goal does not arise. The impact ceiling is boot-time kernel BSS corruption / panic (DoS) when an attacker controls the firmware. This is the valid hard-blocker "path unreachable at runtime on this guest AND no userspace harness can exercise it"; the primitive is proved at the harness level below (the sanctioned pattern for boot-only bugs).
Dynamic proof (harness on the real kernel)
Because the path is boot-only, and because QEMU's -acpitable appends a
second MADT while the kernel's sdt_search() (acpi_sdt.c:196-214) returns the
first matching signature, a -acpitable file=malicious_madt.aml boot repro
does NOT exercise the bug (the supplied table is ignored in favour of QEMU's
own generated MADT). findings/poc/DF-1042/gen_madt.py is still shipped as the
conceptual attacker data (a valid 88-byte MADT with a BSP LocalApicId=0 entry
and a malicious LocalApicId=256 entry).
df1042_harness.c is a kld module that calls the real exported
lapic_set_cpuid() symbol with the exact argument the crafted MADT forces the
pass2 callback to pass โ lapic_set_cpuid(1, 256) โ and observes the resulting
corruption on the live kernel BSS. From nm /boot/kernel/kernel.debug the
linker placed the two mapping arrays back-to-back in BSS:
0xffffffff818d9d20 B apic_id_to_cpu_id (256 ints, 0x400 bytes) 0xffffffff818da120 B cpu_id_to_apic_id (256 ints, 0x400 bytes) <-- [256]==0x818da520 == lapic_mem
So apic_id_to_cpu_id[256] (offset +0x400 from 0x818d9d20 = 0x818da120)
aliases cpu_id_to_apic_id[0] โ the BSP's CPUโAPIC mapping. (The finding's
specific claim that the small overflow hits lapic_enable/lapic_usable/
x2apic_enable is slightly off: those live in .data at 0x8113e650/654, not in
the adjacent BSS. The actual adjacent victims are cpu_id_to_apic_id[0] and, one
array further, lapic_mem โ the LAPIC MMIO base pointer โ which is equally
critical.)
UNPATCHED kernel (6.5-DEVELOPMENT #0, the audit baseline)
DF1042: lapic_set_cpuid=0xffffffff80bdd180 cpu_id_to_apic_id=0xffffffff818da120 apic_id_to_cpu_id=0xffffffff818d9d20 DF1042: &apic_id_to_cpu_id[256]=0xffffffff818da120 &cpu_id_to_apic_id[0]=0xffffffff818da120 (same addr => index 256 aliases cpu_id_to_apic_id[0]) DF1042: BEFORE lapic_set_cpuid(1,256): cpu_id_to_apic_id[0]=0 [1]=1 (BSP apic id is 0) DF1042: AFTER lapic_set_cpuid(1,256): cpu_id_to_apic_id[0]=1 [1]=256 DF1042: OOB WRITE CONFIRMED -- apic_id_to_cpu_id[256] OOB write clobbered cpu_id_to_apic_id[0]
cpu_id_to_apic_id[0] (the BSP mapping) changed 0 โ 1: the out-of-bounds
write to apic_id_to_cpu_id[256] landed on it. On a real malicious-MADT boot
this corrupts the BSP/AP mappings and (for the cpu-dimension OOB) lapic_mem,
producing a boot panic or corrupted LAPIC state.
Fix (fix.diff โ validated)
Two complementary changes:
-
Authoritative bounds check in
lapic_set_cpuid(lapic.c): rejectcpu_id/apic_idoutside[0, NAPICID)and return early. This is the single function performing the unchecked writes and closes both theapic_id-dimension OOB (x2APICLocalApicId >= 256) and thecpu_id-dimension OOB. -
Defense-in-depth guards in the MADT pass2 callbacks (
acpi_madt.c): skip the entry before bothlapic_set_cpuidand the adjacentCPUID_TO_ACPIID(cpu)write whencpu >= MAXCPU(legacy + x2APIC) orx2apic_ent->LocalApicId >= APICID_MAX(x2APIC, mirroring the probe filter atacpi_madt.c:413). This keeps thecpucounter and the unboundedcpu_id_to_acpi_idwrite safe even iflapic_set_cpuidis ever called from a future caller.
PATCHED kernel (6.5-DEVELOPMENT #1, fix.diff applied)
lapic_set_cpuid: invalid cpu_id 1 apic_id 256, skipping DF1042: BEFORE lapic_set_cpuid(1,256): cpu_id_to_apic_id[0]=0 [1]=1 (BSP apic id is 0) DF1042: AFTER lapic_set_cpuid(1,256): cpu_id_to_apic_id[0]=0 [1]=1 DF1042: no corruption -- apic_id_to_cpu_id[256] OOB write did not reach cpu_id_to_apic_id[0]
cpu_id_to_apic_id[0] is unchanged; the bounds check fired and returned
early. Reproduced twice (deterministic). The fix supersedes the finding
markdown's proposal (which fixed only lapic.c): the markdown fix is correct
and necessary, but insufficient alone because the adjacent
CPUID_TO_ACPIID(cpu) write in the same callback is equally unbounded on the
cpu dimension; fix.diff closes that too.
Build & kernel
- Single-fix kernel built from
/usr/srcwith the audit commit +findings/poc/DF-1042/fix.diffapplied:make -j6 nativekernel KERNCONF=X86_64_GENERICโNK_DONE rc=0. - Installed as the bare
/boot/kernel/kernel(sha256bc94555c466f0c9bf478edb64d81d3201638e2341e23f9060f0ce0eaebb4ac7b). kern.version:DragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 08:26:18 UTC 2026(vs the#0baselineThu Jul 2 06:02:54 UTC 2026).
PoC changes
- Added
df1042_harness.c+Makefile(kld harness driving the reallapic_set_cpuidsymbol โ the sanctioned primitive-proof for a boot-only bug, since the path is unreachable post-boot and-acpitableboot repro is defeated bysdt_searchfirst-match). - Added
build.sh/run.sh. - Kept the shipped
gen_madt.py/malicious_madt.amlas the conceptual attacker-supplied firmware data (a real hypervisor/UEFI attacker would feed this exact table). - Authored
fix.diff(supersedes finding proposal โ also bounds the adjacentCPUID_TO_ACPIIDwrite).
Fix verification
fixedVALIDATED: baseline OOB write confirmed; patched bounds check fires (skipping), no corruption. 2x deterministic.
BEFORE: cpu_id_to_apic_id[0]=1 (corrupted). AFTER: cpu_id_to_apic_id[0]=0 (no corruption).
Confirmed kernel references
- sys/platform/pc64/apic/lapic.c:1212
- sys/platform/pc64/apic/lapic.c:1215
- sys/platform/pc64/apic/lapic.c:1216
- sys/platform/pc64/apic/lapic.c:122
- sys/platform/pc64/apic/lapic.c:123
- sys/platform/pc64/apic/apicvar.h:41
- sys/platform/pc64/acpica/acpi_madt.c:337
- sys/platform/pc64/acpica/acpi_madt.c:342
- sys/platform/pc64/acpica/acpi_madt.c:343
- sys/platform/pc64/acpica/acpi_madt.c:83
- sys/platform/pc64/acpica/acpi_madt.c:413
- sys/platform/pc64/acpica/acpi_madt.c:460
- sys/platform/pc64/x86_64/mptable.c:571
- sys/platform/pc64/apic/lapic.h:37
- sys/platform/pc64/apic/lapic.h:38
- sys/cpu/x86_64/include/param.h:71
Detail
Exploit chain
none -- boot-time-only BSS corruption (attacker must control firmware MADT). No userspace privilege boundary crossed. Valid hard blocker: path unreachable at runtime.
Evidence (decisive lines)
BEFORE: cpu_id_to_apic_id[0] 0->1, OOB WRITE CONFIRMED. AFTER: 'invalid cpu_id 1 apic_id 256 skipping', cpu_id_to_apic_id[0]=0, no corruption.
PoC changes
Authored from scratch: df1042_harness.c + Makefile (kld calling real lapic_set_cpuid), gen_madt.py (conceptual attacker MADT), fix.diff (bounds check in lapic_set_cpuid + MADT pass2 cpu/apic_id guards), VERDICT.md, manifest.json.
Verified recommended fix
(1) Bounds check in lapic_set_cpuid rejecting cpu_id/apic_id outside [0,NAPICID); (2) skip cpu>=MAXCPU in MADT pass2 callbacks + LocalApicId>=APICID_MAX for x2APIC. Supersedes finding proposal (also bounds adjacent CPUID_TO_ACPIID write). Full diff in findings/poc/DF-1042/fix.diff.
Verdict
REPRODUCED (kld harness). lapic_set_cpuid (lapic.c:1212-1217) no bounds check on cpu_id/apic_id vs NAPICID=256. Harness lapic_set_cpuid(1,256) corrupted cpu_id_to_apic_id[0] (apic_id_to_cpu_id[256] aliases it at 0xffffffff818da120). Boot-time-only path (MADT/mptable enumerators), no userspace trigger.
No comments yet.