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

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 with size being size_t and the destination field uint16_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 computes 1+size in size_t and, when size == (size_t)-1 (or any size >= SIZE_MAX-1), 1+size wraps to 0 and kmalloc(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 writes size (~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 invokes drm_scdc_write/drm_scdc_read with a size derived from untrusted sink data, or a deliberately huge size.
  • Privileges gained or impact: required privilege: kldload (root) β€” these symbols are EXPORT_SYMBOL and 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.c via set_scrambling/set_high_tmds_clock_ratio, and the inline readb/writeb wrappers drm_scdc_helper.h:111,129) pass size = sizeof(u8) = 1.
  • Required config or capabilities: kldload (root) + a synthetic i2c_adapter, OR a future in-tree caller that derives size from 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 copies size bytes 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; kldload required.
  • Blast radius: if triggered by a future/victim caller: kernel heap corruption + panic (write) or silent partial-read info leak (read).

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

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2117 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md file
↓ download 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

sys/dev/drm/drm_scdc_helper.c

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

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined 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