drm_scdc_read/drm_scdc_write accept size_t but assign to uint16_t i2c_msg.len with no bounds check; drm_scdc_write can overflow 1+size before kmalloc/memcpy
| Field | Value |
|---|---|
| ID | DF-2117 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:H |
| CWE | CWE-190 Integer Overflow or Wraparound; CWE-681 Numeric Truncation Error |
| File | sys/dev/drm/drm_scdc_helper.c |
| Lines | 66, 99, 105, 112 |
| Area | drm |
| Confidence | likely |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
Both exported helpers take a size_t size but the underlying transport
field struct i2c_msg.len is uint16_t
(sys/dev/drm/include/uapi/linux/i2c.h:35). Neither function validates
size. drm_scdc_write computes 1 + size twice β once for .len and
once for the kmalloc argument β and then memcpy's size bytes; when
size is near SIZE_MAX the addition wraps to ~0, kmalloc(0) returns
a tiny slab object, and the subsequent memcpy corrupts the kernel heap
/ panics. drm_scdc_read silently truncates size into the 16-bit
.len, can report success while only the low 16 bits of the caller's
buffer were populated, leaving stale tail bytes. No in-tree caller
currently passes a size other than 1, so there is no demonstrated
unprivileged trigger; the defect is latent but reachable via the
EXPORT_SYMBOL surface by any loadable module or future caller that
derives size from sink data.
Root cause
drm_scdc_read(drm_scdc_helper.c:53-78): the read message is initialized.len = size,at line 66 withsizebeingsize_tand the destination fielduint16_tβ silent truncation, no check.drm_scdc_write(drm_scdc_helper.c:93-124):.len = 1 + size,at line 99 truncates identically;data = kmalloc(1 + size, M_DRM, GFP_KERNEL);at line 105 computes1+sizeinsize_tand, whensize == (size_t)-1(or any size>= SIZE_MAX-1),1+sizewraps to0andkmalloc(0)returns a minimum-size slab chunk (sys/kern/kern_slaballoc.c:_kmalloc,unsigned long size);memcpy(data + 1, buffer, size);at line 112 then writessize(~2^64) bytes from that chunk β a write far past the allocation into neighbouring slab objects and, very quickly, an unmapped page.
The i2c_transfer return is checked (<0 / !=1) at lines 118-121 but
only after the overflow has already occurred. There is no upper-bound or
overflow check on size anywhere in either function.
Threat model & preconditions
- Attacker position: a loadable kernel module (
kld) or any future in-tree caller that invokesdrm_scdc_write/drm_scdc_readwith asizederived from untrusted sink data, or a deliberately hugesize. - Privileges gained or impact: required privilege:
kldload(root) β these symbols areEXPORT_SYMBOLand have no direct syscall/ioctl entry, so an unprivileged local user cannot reach them today; all current in-tree callers (intel_ddi.c:3632,intel_hdmi.cviaset_scrambling/set_high_tmds_clock_ratio, and the inlinereadb/writebwrappersdrm_scdc_helper.h:111,129) passsize = sizeof(u8) = 1. - Required config or capabilities:
kldload(root) + a synthetic i2c_adapter, OR a future in-tree caller that derivessizefrom sink data. - Reachability: if a victim caller exists,
drm_scdc_write(sizeβ SIZE_MAX)β deterministic kernel heap corruption / panic (A:H, I:H);drm_scdc_read(size > 0xffff)β silent partial read reported as success, potentially leaking stale kernel buffer contents to userspace if the caller trustingly copiessizebytes out (C:L).
Because every current caller is hard-wired to size==1, real-world
exploitability in this tree is speculative; the finding is filed as a
latent defect / hardening gap in a public kernel API.
Proof of Concept
Reproduce as a privileged kldload-able module (root). The module
registers a throwaway i2c_adapter whose master_xfer simply returns
num (success), then calls:
/* heap-corruption / panic variant */
drm_scdc_write(adapter, SCDC_TMDS_CONFIG, dummy, (size_t)-1);
/* silent-truncation variant: read 0x10000 bytes into a 0x10000-byte
buffer; .len becomes 0, nothing is written, function returns 0
(success); the buffer retains whatever kmalloc left in it */
u8 *buf = kmalloc(0x10000, M_DRM, GFP_KERNEL); /* not zeroed */
drm_scdc_read(adapter, SCDC_SCRAMBLER_STATUS, buf, 0x10000);
Expected output
# write variant:
Fatal trap 12: page fault while in kernel mode # during memcpy in
# drm_scdc_write
# (RIP at drm_scdc_helper.c:112)
# read variant:
drm_scdc_read returns 0; 'buf' contains stale slab memory.
Impact
- Default config: no current in-tree caller triggers it;
kldloadrequired. - Blast radius: if triggered by a future/victim caller: kernel heap corruption + panic (write) or silent partial-read info leak (read).
Recommended fix
Validate size against the uint16_t transport limit (and the 1-byte
offset prefix) at the top of each function, before the value is used in
any initializer, kmalloc, or memcpy.
--- a/sys/dev/drm/drm_scdc_helper.c
+++ b/sys/dev/drm/drm_scdc_helper.c
@@ -52,6 +52,10 @@ ssize_t drm_scdc_read(struct i2c_adapter *adapter, u8 offset, void *buffer,
size_t size)
{
int ret;
+
+ /* struct i2c_msg.len is uint16_t; reject sizes that would silently truncate */
+ if (size > UINT16_MAX)
+ return -EINVAL;
struct i2c_msg msgs[2] = {
{
.addr = SCDC_I2C_SLAVE_ADDRESS,
@@ -92,6 +96,11 @@ ssize_t drm_scdc_write(struct i2c_adapter *adapter, u8 offset,
const void *buffer, size_t size)
{
+ /* 1-byte offset prefix + payload must fit in uint16_t i2c_msg.len,
+ * which also guarantees 1 + size cannot wrap the kmalloc argument. */
+ if (size > UINT16_MAX - 1)
+ return -EINVAL;
+
struct i2c_msg msg = {
.addr = SCDC_I2C_SLAVE_ADDRESS,
.flags = 0,
The check in drm_scdc_write bounds size <= 65534, so 1 + size <=
65535 (fits uint16_t .len and cannot overflow size_t), eliminating
both the truncation and the kmalloc(0)/memcpy(SIZE_MAX) overflow.
The drm_scdc_read check bounds size <= 65535 so .len faithfully
reflects the transfer length and a caller can no longer receive a false
"success" on a partially-read buffer. <linux/types.h> (already pulled
in transitively via drmP.h/i2c.h) provides UINT16_MAX. No
behavior change for any current caller (all pass size == 1).
References
sys/dev/drm/include/uapi/linux/i2c.h:35βstruct i2c_msg.lenisuint16_t.sys/kern/kern_slaballoc.cβkmalloc(0)returns a minimum-size slab chunk.
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-2117 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 697 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 166 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2117 - Verification Verdict
Status: reproduced (source-confirmed) Impact: none Confidence: certain
Verdict
Source-confirmed: drm_scdc_read/write take size_t size but i2c_msg.len is uint16_t; no validation; size>65535 truncates; DRM/HDMI-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)
drm_scdc size_t vs uint16_t; DRM-gated
Verified recommended fix
drm_scdc size_t vs uint16_t; DRM-gated
Verdict
drm_scdc size_t vs uint16_t; DRM-gated
No comments yet.