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_typeare never wired into any engine vtable (rg dal_i2c_generic_hw_engine_submit_request sys/dev/drm/amd/displayreturns only the definition and prototype)._constructis never called.i2caux->i2c_generic_hw_engineis only set toNULL(i2caux.c:447).- Both in-tree I2C HW engines wire
.submit_requestto the guarded siblingdal_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).
Recommended fix
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
sys/dev/drm/amd/display/dc/i2caux/i2c_hw_engine.c:91-96β sibling with the proper guard.sys/dev/drm/amd/display/dc/i2caux/dce110/i2c_hw_engine_dce110.c:441-446βget_hw_buffer_available_sizesource that can return 0.
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-2090 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix | 364 B | view raw |
| VERDICT.md | verdict | source-trace confirmation | 663 B | β 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
fixedEarly return if size==0; batch build rc=0.
Early return if size==0; batch 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.
No comments yet.