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)
PoC verification
Evidence pack
findings/poc/DF-5135 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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 toTUNABLE_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_intstores 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
fixedfix.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).
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.
No comments yet.