Undefined behavior in radix_max when tree height reaches RADIX_TREE_MAX_HEIGHT (shift >= word width)
| Field | Value |
|---|---|
| ID | DF-2109 |
| Status | new |
| Severity | Info |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:N |
| CWE | CWE-758 Reliance on Undefined Behavior |
| File | sys/dev/drm/linux_radix.c |
| Lines | 42-46 |
| Area | drm/linuxkpi |
| Confidence | certain |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
radix_max() computes (1UL << (root->height * RADIX_TREE_MAP_SHIFT)) - 1.
On 64-bit platforms RADIX_TREE_MAX_HEIGHT = howmany(64, 6) = 11, so
when height == 11 the expression is 1UL << 66 β a shift by more than
the width of unsigned long, which is undefined behavior per
ISO C11 Β§6.5.7. The expand loop in radix_tree_insert can drive height
to exactly 11 (it checks == RADIX_TREE_MAX_HEIGHT only before
incrementing at line 196, allowing the increment to 11 at line 211),
after which every subsequent radix_max() call on that root is UB. On
x86-64 the hardware masks the shift count to 6 bits (66 & 63 = 2),
yielding radix_max == 3, which causes the loop to re-enter and hit the
E2BIG check β benign but non-portable. No memory corruption occurs, but
the tree is left in an inconsistent state (height=11) after a failed
insert of an impossibly large index.
Root cause
radix-tree.h:39-40definesRADIX_TREE_MAX_HEIGHTashowmany(sizeof(long)*NBBY, RADIX_TREE_MAP_SHIFT) = howmany(64, 6) = 11.linux_radix.c:45computes1UL << (root->height * 6). Atheight == 11this is1UL << 66.- The guard in
radix_tree_insertatlinux_radix.c:196(if (root->height == RADIX_TREE_MAX_HEIGHT) return -E2BIG) fires only after height has already been incremented to 11 at line 211 in the prior iteration β meaning the while-condition at line 193 (radix_max(root) < index) is re-evaluated withheight == 11, invoking the UB. - The same UB then affects any subsequent lookup/insert/delete on this
root via their
radix_max()calls at lines 64, 88, 126, 193.
Threat model & preconditions
- Attacker position: N/A β not exploitable through any reachable DRM attack surface.
- Privileges gained or impact: none. ISO C undefined behavior
(correctness/portability risk on non-x86 architectures where large
shifts may trap or yield 0); a leaked/inconsistent tree node when
E2BIGis returned after the expand loop already allocated a new root level (minor memory leak). - Required config or capabilities: the height-11 state requires
inserting an
unsigned longindex> (1UL<<60)-1(~1.15e18). All DRM callers use bounded indices:i915_gem_execbuffer.c:776passesu32handle(max2^32, needing height 6);i915_gem.c:6569iter->radixuses page-offset indices bounded by GEM object size (max ~2^20). A malicious kernel module (already root-privileged) could theoretically trigger it, but that is not a privilege-escalation path. - Reachability: kernel-internal API misuse only.
Proof of Concept
Not practically exploitable for memory corruption or privilege escalation.
To demonstrate the UB on x86-64 (informational only, not a security PoC):
write a kernel module that calls
radix_tree_insert(root, 1UL<<63, &dummy) on a fresh tree β the expand
loop drives height 1ββ¦β11, the 1UL<<66 UB manifests as radix_max
returning 3 (masked shift), and -E2BIG is returned leaving
root->height == 11 in an inconsistent state. No panic, no corruption.
Reproducing on a non-x86 architecture (e.g., older ARM where large shifts
may yield 0) could cause the expand loop to fail to terminate until the
height check trips, still resulting only in -E2BIG.
Impact
No memory corruption, no info leak, no privilege escalation. Portability / correctness hardening defect only.
Recommended fix
Make radix_max() robust against shift overflow by clamping the shift to
the word width. When height*RADIX_TREE_MAP_SHIFT >= BITS_PER_LONG,
return ULONG_MAX (all indices representable), which is the
mathematically correct maximum for a tree tall enough to index the full
unsigned long range. This also makes the tree correctly accept all
64-bit indices without ever hitting the E2BIG path for valid
pointer-keyed indices.
--- a/sys/dev/drm/linux_radix.c
+++ b/sys/dev/drm/linux_radix.c
@@ -42,7 +42,11 @@
static inline unsigned long
radix_max(struct radix_tree_root *root)
{
- return ((1UL << (root->height * RADIX_TREE_MAP_SHIFT)) - 1UL);
+ unsigned int shift = root->height * RADIX_TREE_MAP_SHIFT;
+
+ if (shift >= sizeof(unsigned long) * NBBY)
+ return (~0UL);
+ return ((1UL << shift) - 1UL);
}
static inline int
References
- ISO C11 Β§6.5.7 β "If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined."
sys/dev/drm/radix-tree.h:39-40βRADIX_TREE_MAX_HEIGHT = 11on 64-bit.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2109 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 697 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 162 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2109 - Verification Verdict
Status: reproduced (source-confirmed) Impact: none Confidence: likely
Verdict
Source-confirmed: radix_max (:45) computes 1UL<<(height*6)-1; when height==RADIX_TREE_MAX_HEIGHT(11), shift=66 >= 64 is UB; DRM-module-gated
Fix Status
Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)
Source File
Fix Validation
All 87 fix.diffs compiled together in a single batch kernel build
(make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors).
The combined patch is at findings/poc/batch_build/all_fixes.patch.
Fix verification
fixedbatch build rc=0
batch build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
radix_max UB shift height==11; DRM-gated
Verified recommended fix
radix_max UB shift height==11; DRM-gated
Verdict
radix_max UB shift height==11; DRM-gated
No comments yet.