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

Unsigned underflow in max_payload_size-1 bypasses HW buffer chunking when HW buffer is empty (dead code today)

Field Value
ID DF-2090
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L
CWE CWE-191 Integer Underflow (Wrap or Wraparound)
File sys/dev/drm/amd/display/dc/i2caux/i2c_generic_hw_engine.c
Lines 96-186
Area drm/amd
Confidence speculative
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match dfly_specific

Summary

dal_i2c_generic_hw_engine_submit_request computes (max_payload_size - 1) on a uint32_t without first checking that max_payload_size != 0. When the HW-specific get_hw_buffer_available_size() returns 0 (buffer exhausted), the subtraction wraps to UINT32_MAX, making the size comparison always-false, so the entire user payload is processed in a single un-chunked transaction that bypasses the HW circular-buffer limit and overfeeds write_data/read_data/execute_transaction. The sibling dal_i2c_hw_engine_submit_request guards this exact case; this function omits the guard. All five exported functions in this file are dead code in the current DragonFlyBSD tree (see Threat model), so impact is latent.

Root cause

At i2c_generic_hw_engine.c:96-97 max_payload_size is obtained from base->funcs->get_hw_buffer_available_size(base) with no validation. The DCE110 implementation returns I2C_HW_BUFFER_SIZE - buffer_used_bytes (i2c_hw_engine_dce110.c:441-446) which is 0 when buffer_used_bytes reaches 538.

In the first-iteration branch (i2c_generic_hw_engine.c:150-158):

current_transaction_size =
    (remaining_payload_size > max_payload_size - 1) ?   /* line 154 */
        max_payload_size :
        remaining_payload_size + 1;                       /* line 156 */
current_payload_size = current_transaction_size - 1;      /* line 158 */

When max_payload_size == 0, (max_payload_size - 1) == 0xFFFFFFFF (unsigned wrap). For any remaining_payload_size < UINT32_MAX the ternary is false, so current_transaction_size = remaining_payload_size + 1 and current_payload_size = remaining_payload_size. The loop then calls hw_engine->funcs->write_data(hw_engine, current_payload, remaining_payload_size) (i2c_generic_hw_engine.c:183-186) / read_data (line 214-215) with the FULL un-chunked payload, and attributes.transaction_size = remaining_payload_size + 1 (line 193) is programmed into HW.

Contrast with the sibling dal_i2c_hw_engine_submit_request in i2c_hw_engine.c:91-96 which explicitly checks if (payload.length >= get_hw_buffer_available_size()) return BUFFER_OVERFLOW β€” this generic version has no such guard. The loop also never recovers: after the single iteration remaining_payload_size reaches 0 and the loop exits, so no chunking ever occurs.

Threat model & preconditions

  • Attacker position: would-be local privileged user β€” but see below.
  • Privileges gained or impact: HW data-buffer overflow past the register-file aperture, corrupting adjacent MMIO-mapped register state.
  • Required config or capabilities: none in the current kernel build.
  • Reachability: NOT reachable today. All five exported functions in this file are dead code:
  • dal_i2c_generic_hw_engine_submit_request, _get_transaction_timeout, _get_engine_type are never wired into any engine vtable (rg dal_i2c_generic_hw_engine_submit_request sys/dev/drm/amd/display returns only the definition and prototype).
  • _construct is never called.
  • i2caux->i2c_generic_hw_engine is only set to NULL (i2caux.c:447).
  • Both in-tree I2C HW engines wire .submit_request to the guarded sibling dal_i2c_hw_engine_submit_request (i2c_hw_engine_dce80.c:815, i2c_hw_engine_dce110.c:498).

Filed Low/speculative so the fix is documented if the generic engine is ever wired up.

Proof of concept

Not reproducible against the current DragonFlyBSD build. To demonstrate the latent bug in isolation one would: (1) construct a mock i2c_generic_hw_engine whose get_hw_buffer_available_size returns 0, (2) call dal_i2c_generic_hw_engine_submit_request with payload.length=N>0, (3) observe that write_data is called once with length=N rather than rejecting with BUFFER_OVERFLOW. A kernel PoC would require first wiring the vtable, which does not exist today.

Impact

Latent only. If the generic engine were ever wired into a vtable, an attacker who can issue I2C transactions to a DDC line whose HW buffer is momentarily exhausted (e.g., a concurrent transaction left buffer_used_bytes == I2C_HW_BUFFER_SIZE, or a hot-unplug/reset race zeroes the available-size arithmetic) would overflow the DCE110 538-entry DC_I2C_DATA circular buffer (process_transaction, i2c_hw_engine_dce110.c:323-330 writes one MMIO REG_SET_2 per byte).

Mirror the sibling guard in i2c_hw_engine.c. Add an explicit zero-check on max_payload_size before the loop so the unsigned subtraction at line 154 can never underflow:

--- a/sys/dev/drm/amd/display/dc/i2caux/i2c_generic_hw_engine.c
+++ b/sys/dev/drm/amd/display/dc/i2caux/i2c_generic_hw_engine.c
@@ -112,6 +112,17 @@ bool dal_i2c_generic_hw_engine_submit_request(

    bool first_iteration = true;

+   /* Guard against a zero-length (exhausted) HW buffer.
+    * Without this, (max_payload_size - 1) in the first-iteration
+    * branch below underflows to UINT32_MAX, making the size
+    * comparison always false and submitting the entire payload in
+    * a single un-chunked transaction that bypasses the HW buffer
+    * limit.  The sibling dal_i2c_hw_engine_submit_request guards
+    * this same condition (i2c_hw_engine.c:91-96). */
+   if (max_payload_size == 0) {
+       i2caux_request->status =
+           I2CAUX_TRANSACTION_STATUS_FAILED_BUFFER_OVERFLOW;
+       return false;
+   }
+
    if (i2caux_request->operation == I2CAUX_TRANSACTION_READ)
        attributes.action = I2CAUX_TRANSACTION_ACTION_I2C_READ;
    else if (i2caux_request->operation == I2CAUX_TRANSACTION_WRITE)

Additionally, the timeout function (lines 269-270) should clamp length before (length << 3) to prevent shift/multiply overflow, though this is a secondary hardening concern given length is normally bounded by max_payload_size.

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-2090 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix 364 B view raw
VERDICT.md verdict source-trace confirmation 663 B ↓ raw
VERDICT.md verdict source-trace confirmation
↓ download raw

DF-2090 β€” i2c_generic_hw_engine max_payload_size-1 underflow

Verdict

REPRODUCED (source-only confirmation). Bug confirmed by source tracing.

Mechanism

dal_i2c_generic_hw_engine_submit_request (i2c_generic_hw_engine.c:154) computes (max_payload_size - 1) on uint32_t without checking max_payload_size!=0. If get_hw_buffer_available_size() returns 0, underflow to 0xFFFFFFFF bypasses chunking. Currently dead code (DCE110 returns fixed non-zero) but latent.

Fix

Add early return: if (max_payload_size == 0) return false; before the loop.

Batch-build status

Applied with all 24 other fixes; kernel + modules compiled rc=0, 0 errors, -Werror.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Early return if size==0; batch build rc=0.

Early return if size==0; batch build rc=0.
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

i2c_generic_hw_engine max_payload_size-1 underflow. Dead code.

Verified recommended fix

i2c_generic_hw_engine max_payload_size-1 underflow. Dead code.

Verdict

i2c_generic_hw_engine max_payload_size-1 underflow. Dead code.