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

TUNABLE_bool stores a 4-byte int through (int*)&_Bool_param: setting any i915 bool tunable silently corrupts the following parameters (and the registered names are mangled to KBUILD_MODNAME.<param>)

Summary

The generic shim casts a 1-byte _Bool to int* and kgetenv_int writes 4 bytes - the following struct members get the upper bytes. i915's 13 bools sit at the struct tail (contained; silent misconfig of unrelated params, e.g. enable_dp_mst flipped live on the guest) but the shim is a landmine for any non-tailed bool. Byte-safe TUNABLE_bool fix validated on rebuilt i915.ko.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5135 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
layout.c β€” 2.5 KB view raw
i915_probe.c β€” 3.2 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 210 B view raw
build.log β€” 776 B view raw
run.log β€” 1.3 KB view raw
env.txt β€” 480 B view raw
fix.diff β€” 892 B view raw
fix_build.log β€” 436 B view raw
fix_run.log β€” 433 B view raw
verdict.json β€” 4.2 KB view raw

DF-5135 β€” TUNABLE_bool stores 4 bytes into 1-byte _Bool i915 params,

silently corrupting the neighbouring parameters

sys/dev/drm/include/linux/moduleparam.h:41:

#define TUNABLE_bool(name, var)     TUNABLE_INT( _TUNABLE_VAR(.name), (int *)&(var))

bool is _Bool β€” 1 byte (sys/sys/stdint.h:58). TUNABLE_INT + tunable_int_init() β†’ kgetenv_int() performs *data = (int)tmp β€” a 4-byte store through that cast (kern_environment.c:379-389, 527-533). Every i915_param_named(..., bool, ...) in sys/dev/drm/i915/i915_params.c instantiates this macro on a member of struct i915_params, so setting one bool tunable overwrites the three following struct members (all further bools; the layout keeps bools at the tail, and the last bool's write ends in the struct's padding, so the corruption stays inside the object β€” a correctness/security-configuration bug and a latent landmine, not a past-the- object overflow on this struct).

Bonus discovery (guest-verified): the registered tunable names are mangled. The shipped i915.ko registers bool tunables as KBUILD_MODNAME.<name> (and the module_param_named_unsafe ones as KBUILD_MODNAME.(<name>) β€” with parentheses), because -DKBUILD_MODNAME never reaches i915_params.c. The int params dodge this because i915_params.c:43-127 adds explicit TUNABLE_INT("drm.i915.*") lines; the bools (and the charp paths, whose TUNABLE_charp shim is a no-op) have no working name at all except the mangled ones.

Live reproduction (audit guest, stock i915.ko)

kenv KBUILD_MODNAME.verbose_state_checks=1
kldload i915                    # tunable fetch happens at module SYSINIT
kldload ./i915probe.ko          # dumps the real i915_modparams tail bytes
sysctl hw.i915probe.params

verbose_state_checks sits at offset 112; enable_dp_mst (default true) at 114. The 4-byte store of 1 writes bytes 112..115 β†’ enable_dp_mst is silently flipped 1β†’0:

dmabug:   error_capture=1  disable_display=0  verbose_state_checks=1  nuclear_pageflip=0
dmabug:   enable_dp_mst=0 enable_dpcd_backlight=0 enable_gvt=0   <-- dp_mst corrupted (default is 1)

Baseline (no kenv var): enable_dp_mst=1.

Layout proof (cc -o layout layout.c; ./layout):

sizeof(bool)=1 sizeof(struct i915_params)=120
offsets: error_capture=110 disable_display=111 verbose_state_checks=112
         nuclear_pageflip=113 enable_dp_mst=114 enable_gvt=116(last)
after 'i915.error_capture=1': error_capture=1 disable_display=0
         verbose_state_checks=0 nuclear_pageflip=0  (0xAA bytes overwritten to 0)

Fix validation

fix.diff replaces TUNABLE_bool with a SYSINIT helper that fetches into an int and narrows the store ((var) = !!__tv). Rebuilt i915.ko on the guest, rebooted, re-ran the exact kenv sequence:

dmabug:   verbose_state_checks=1  ...
dmabug:   enable_dp_mst=1 enable_dpcd_backlight=0 enable_gvt=0   <-- intact
VERDICT.md
↓ download raw

DF-5135 β€” VERDICT: reproduced (adjacent-param corruption from bool tunables), fix validated

Mechanism

  • module_param_named(<bool params>) in i915_params.c expands through moduleparam.h:52-56 to TUNABLE_bool(name, var) β†’ moduleparam.h:41 β†’ TUNABLE_INT(..., (int *)&i915_modparams.<bool>).
  • bool = _Bool = 1 byte (sys/sys/stdint.h:58; verified on guest: sizeof(bool)=1). tunable_int_init β†’ kgetenv_int stores an int (4 bytes) at that address when the tunable exists in the kernel env.
  • struct i915_params (i915_params.h:73-77) keeps all 13 bools at the tail; layout verified live: error_capture@110 … enable_dp_mst@114, enable_dpcd_backlight@115, enable_gvt@116 (last), sizeof=120.
  • Setting any bool tunable therefore rewrites the following up-to-3 bool members with the upper bytes of the value (little-endian: value 1 zeroes them). The write stays inside the padded struct β€” contained corruption of other params (e.g. silently disabling verbose_state_checks, MST, or hangcheck-related bools), plus a latent landmine for any future bool param whose neighbours are not β‰₯3 pad bytes.

Name mangling (discovered during verification)

The shipped /boot/kernel/i915.ko registers the macro-generated tunables under mangled names β€” strings shows KBUILD_MODNAME.enable_gvt, KBUILD_MODNAME.verbose_state_checks, KBUILD_MODNAME.(enable_guc) (unsafe params even get parentheses) β€” because -DKBUILD_MODNAME="i915" does not reach i915_params.c. The explicit TUNABLE_INT("drm.i915.<name>") lines in i915_params.c:43-127 exist precisely to give the INT params sane names; the bools and the charp paths (no-op TUNABLE_charp, moduleparam.h:45 β€” the firmware-path params are dead on this port) have none.

Live run (stock modules)

$ strings /boot/kernel/i915.ko | grep '^KBUILD_MODNAME' | head
KBUILD_MODNAME.enable_gvt
KBUILD_MODNAME.verbose_state_checks
...
$ kenv KBUILD_MODNAME.verbose_state_checks=1
KBUILD_MODNAME.verbose_state_checks="1"
$ kldload i915 && kldload ./i915probe.ko && sysctl hw.i915probe.params
(dmesg)
dmabug:   error_capture=1  disable_display=0  verbose_state_checks=1  nuclear_pageflip=0
dmabug:   enable_dp_mst=0 enable_dpcd_backlight=0 enable_gvt=0

enable_dp_mst default is true (i915_params.h:69) β€” it came back 0 solely because setting the unrelated verbose_state_checks param stored 4 bytes across offsets 112..115. Baseline without the kenv var: dp_mst=1.

Fix validation

fix.diff (moduleparam.h: byte-safe TUNABLE_bool), applied to guest /usr/src, i915.ko force-rebuilt (rm i915_params.o; make; make install), rebooted, identical kenv+load sequence:

dmabug:   verbose_state_checks=1  ...
dmabug:   enable_dp_mst=1 enable_dpcd_backlight=0 enable_gvt=0   <-- intact

fix_baseline_reproduced=1 (dp_mst 1β†’0 corruption on stock), fix_patched_reproduced=0 (no corruption after fix). fix_status=fixed.

Impact honesty

Trigger requires root (kenv/loader.conf) β€” this is a param-integrity / defense-in-depth defect, not an unprivileged primitive: impact=none beyond silent misconfiguration of unrelated driver params. Severity Low.

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

fix.diff replaces TUNABLE_bool with a SYSINIT that does TUNABLE_INT_FETCH into an int then (var) = !!__tv (preserving the compiled-in default when unset). Force-rebuilt i915.ko on the guest, rebooted, re-ran the identical kenv trigger: enable_dp_mst stayed 1 (no neighbour corruption); stock module flipped it to 0 minutes earlier on the same guest.

fix_run.log ('enable_dp_mst=1 ... INTACT'); fix_build.log (header patch + i915_params.o forced recompile + install).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (same base kernel; /boot/kernel/i915.ko rebuilt from patched moduleparam.h)

Confirmed kernel references

Detail

Exploit chain

root sets boot/loader kenv tunable for one bool param (names are mangled to KBUILD_MODNAME. in the shipped module because -DKBUILD_MODNAME never reaches i915_params.c) -> tunable_int_init at module load -> kgetenv_int 4-byte store into 1-byte _Bool -> up to 3 neighbouring bool params silently zeroed/corrupted.

Evidence (decisive lines)

run.log: baseline enable_dp_mst=1, then after kenv KBUILD_MODNAME.verbose_state_checks=1 + reload: 'enable_dp_mst=0' (corrupted); build.log: layout.c output (sizeof(bool)=1, struct=120, offsets 110-116); fix_run.log: patched i915.ko keeps enable_dp_mst=1 under the identical trigger.

PoC changes

PoC written from scratch. First attempt used the guessed tunable name 'i915.error_capture' β€” no-op because (a) CONFIG_DRM_I915_CAPTURE_ERROR compiles that param's registration out and (b) macro-generated names are mangled. strings(1) on the shipped i915.ko revealed the real names (KBUILD_MODNAME., unsafe params as 'KBUILD_MODNAME.()'); switched the trigger to KBUILD_MODNAME.verbose_state_checks whose corruption target (enable_dp_mst, default true) is observably non-default.

Verified recommended fix

Make TUNABLE_bool fetch into an int and narrow the store to the _Bool (see fix.diff); ideally also give bool params sane registered names and pass the mode through.

Verdict

TUNABLE_bool (sys/dev/drm/include/linux/moduleparam.h:41) casts 1-byte _Bool module params to (int *) and kgetenv_int() stores 4 bytes, so setting any i915 bool tunable rewrites the following params of struct i915_params. Reproduced live on the stock guest i915.ko: kenv KBUILD_MODNAME.verbose_state_checks=1 before kldload silently flipped enable_dp_mst (offset 114, default true) to 0 while verbose_state_checks (@112) itself was being set β€” the 4-byte store spanned bytes 112..115. Contained corruption (bools are the tail members; last bool's write ends in padding β€” sizeof=120, verified), so impact is silent misconfiguration of unrelated params plus a latent landmine for future bool params with non-padded neighbours. Root/boot-tunable trigger only. Fix (byte-safe TUNABLE_bool via SYSINIT narrowing) validated on a rebuilt i915.ko: enable_dp_mst stayed 1.