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

generic_reg_wait prototype in dm_services.h transposes (mask, shift) vs the definition in dc_helper.c (shift, mask) β€” latent API-contract trap in the DC register hub; harmless today only because REG_WAIT's FN() expansion happens to match the definition

Summary

Declaration and definition disagree on the (mask,shift) order - both u32 so arguments bind silently; the only call family (REG_WAIT/FN()) matches the definition so behavior is correct today, but any header-trusting caller transposes and polls garbage for the full budget. Fix the declaration to match. The hub otherwise clean: every *_SET_N/_UPDATE_N pair-count census found no n exceeding supplied triplets (no stack-garbage-into-MMIO; DF-4435 remains the only mismatch, drop-direction).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4999 Β· 3 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
fix.diff β€” 685 B view raw

DF-4999 β€” dc_helper.c: generic_reg_wait prototype in dm_services.h declares (mask, shift) but the definition is (shift, mask) β€” latent API-contract trap for every DC consumer

Where

Declaration: sys/dev/drm/amd/display/dc/dm_services.h:152-156 Definition: sys/dev/drm/amd/display/dc/dc_helper.c:254-257

/* dm_services.h:152 */
unsigned int generic_reg_wait(const struct dc_context *ctx,
    uint32_t addr, uint32_t mask, uint32_t shift, uint32_t condition_value, ...)

/* dc_helper.c:254 */
uint32_t generic_reg_wait(const struct dc_context *ctx,
    uint32_t addr, uint32_t shift, uint32_t mask, uint32_t condition_value, ...)

What

The shared-header prototype swaps mask/shift relative to the actual definition. Today this is harmless at the ABI level (both parameters are uint32_t, so the compiler binds arguments positionally with no diagnostic) and harmless at the behavior level because the only call family, REG_WAIT() (inc/reg_helper.h), expands FN(reg,field) to reg__field__SHIFT, reg__field__MASK β€” i.e. it passes shift first, matching the definition, not the header. (There are no direct generic_reg_wait() call sites outside reg_helper.h/dm_services.h.)

The trap: any future caller that reads the header (as one does) and writes generic_reg_wait(ctx, addr, mask, shift, cond, delay, tries, __func__, __LINE__) silently transposes the pair β€” get_reg_field_value_ex() (dm_services.h:112) then computes (shift & reg_val) >> mask (garbage), the poll condition never matches, and every such wait burns its full budget (in-tree budgets reach 100 ms) and lands in the error path (dm_error + BREAK_TO_DEBUGGER). The same mis-ordering applied to the varargs generic_reg_update_ex family would be memory-unsafe (stack garbage merged into MMIO writes); for generic_reg_wait it is functional-only.

The function's own comment block at dm_services.h:146-148 ("return number of poll before condition is met / return 0 if condition is not meet") further documents that the API is meant to be consumed directly.

Impact

No in-tree defect today (audited every call path); API-contract hazard / documentation defect in the single most-used helper hub of the DC subsystem. Info severity, hardening.

Pass-2 companion audit of the hub (negative results recorded in the finding entry)

  • generic_reg_update_ex/generic_indirect_reg_update_ex varargs: every direct *_REG_SET_N/UPDATE_N call site in the tree was pair-counted β€” all match (n never exceeds the supplied triplets), so there is no stack-garbage-into-MMIO primitive; the only mismatch in-tree is DF-4435 (n smaller, writes silently dropped β€” filed pass 1, not re-reported).
  • generic_reg_wait loop is bounded (time_out_num_tries+1 iterations); all in-tree budgets are compile-time constants with products ≀ 100 ms (largest seen: dcn10_optc.c:496/762, 1 Β΅s Γ— 100000); no user-controlled argument ever reaches this file.
VERDICT.md
↓ download raw

DF-4999 β€” VERDICT

Finding: prototype/definition parameter-order mismatch for generic_reg_wait between dm_services.h:152-156 and dc_helper.c:254-257.

Status: untested (static; the audit guest has no DRM/DC device β€” /dev/dri absent, same as the DF-4435 verification)

Proof

  1. Declaration (header every DC translation unit includes):

c /* sys/dev/drm/amd/display/dc/dm_services.h:152 */ unsigned int generic_reg_wait(const struct dc_context *ctx, uint32_t addr, uint32_t mask, uint32_t shift, uint32_t condition_value, unsigned int delay_between_poll_us, unsigned int time_out_num_tries, const char *func_name, int line);

  1. Definition (this file):

c /* sys/dev/drm/amd/display/dc/dc_helper.c:254 */ uint32_t generic_reg_wait(const struct dc_context *ctx, uint32_t addr, uint32_t shift, uint32_t mask, uint32_t condition_value, unsigned int delay_between_poll_us, unsigned int time_out_num_tries, const char *func_name, int line)

  1. Harmless today only because C binds arguments positionally over identical uint32_t types and the sole call family is the REG_WAIT macro (inc/reg_helper.h:245-248), whose FN() expansion orders shift, mask β€” matching the definition, contradicting the header. Direct-call census: rg generic_reg_wait\( over sys/dev/drm/amd/display/dc returns only the definition, the header declaration, and the REG_WAIT macro β€” no other call sites.

  2. The trap for future callers is concrete: a header-believing caller transposes the pair; get_reg_field_value_ex(reg_val, mask, shift) (dm_services.h:112-116) computes (mask & reg_val) >> shift with the roles swapped β†’ condition never satisfied β†’ full poll budget burned (in-tree budgets up to 100 ms, e.g. dcn10_optc.c:496) β†’ dm_error timeout + BREAK_TO_DEBUGGER() (dc_helper.c:288-293) on every call β€” and for the sibling varargs family (generic_reg_update_ex) an equivalent declaration/consumer disagreement would feed stack garbage into MMIO writes (va_arg walks past the supplied triplets), which is why the header contract matters even where the runtime effect today is nil. AMD's own code comment (dc_helper.c:223-225) acknowledges the varargs family is "prone to stack corruption type of bugs".

Fix

One-line header correction (fix.diff) making the declaration identical to the definition (also unifying the return type to uint32_t). No behavior change for existing callers.

Fix validation

patch --dry-run OK. No kernel build possible on the guest (DC requires AMD display hardware; guest has no DRM device), but the edit is declaration-only β€” it cannot change codegen of the existing macro callers.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Declaration-only edit; cannot change codegen of existing macro callers (they already pass (shift, mask) positionally). patch --dry-run OK. No hardware to build/run DC on the guest.

findings/poc/DF-4999/fix.diff (patch -p1 --dry-run: OK)
↓ fix.diffper-fix-DF-4999

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (side-by-side declaration/definition, call-site census, hazard analysis); fix.diff (one-hunk header correction, patch --dry-run APPLY-OK)

PoC changes

No PoC authored: no user-reachable surface; the defect is a header contract mismatch provable only statically.

Verified recommended fix

Fix the dm_services.h prototype to (addr, shift, mask, ...) and return type uint32_t, identical to the dc_helper.c definition

Verdict

Declaration/definition parameter-order mismatch, static certainty, no in-tree runtime defect. dm_services.h:152-156 declares generic_reg_wait(ctx, addr, mask, shift, condition_value, ...) while the definition dc_helper.c:254-257 is (ctx, addr, shift, mask, condition_value, ...). Because both parameters are uint32_t, arguments bind positionally with no compiler diagnostic; today the only call family, REG_WAIT (inc/reg_helper.h:245-248), expands FN(reg,field) to (shift, mask) - matching the DEFINITION - so behavior is correct (direct-call census over sys/dev/drm/amd/display/dc found no other call sites). The hazard: any caller trusting the header transposes the pair; get_reg_field_value_ex (dm_services.h:112) then extracts (shift & reg_val) >> mask - garbage - so the poll condition never matches, each wait burns its full budget (in-tree budgets reach 100ms, e.g. dcn10_optc.c:496) and trips dm_error/BREAK_TO_DEBUGGER (dc_helper.c:288-293); an equivalent mismatch on the sibling varargs family generic_reg_update_ex would be memory-unsafe (va_arg past supplied args feeds stack garbage into MMIO writes - the hazard class AMD itself notes at dc_helper.c:223-225). Untested: declaration-only change, no runtime surface without AMD DC hardware (guest /dev/dri absent, same as DF-4435 verification). Info severity hardening finding.