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

NULL function-pointer call: encoder_funcs->detect invoked without check in amdgpu_connector_dp_detect DP-bridge branch β€” external (Travis/Nutmeg) encoders ship helper funcs without .detect

Summary

In the DP-bridge branch, best_single_encoder returns the FIRST possible encoder in ATOMBIOS object-table order. External bridge encoders - exactly the TRAVIS/NUTMEG devices that make this branch reachable - use ext_helper_funcs which deliberately have no .detect. When the VBIOS enumerates the bridge first and the aux/DDC probe fails while dac_load_detect is true, :1387 calls encoder_funcs->detect == NULL: instant panic on the first probe. The sibling dvi_detect guards the identical pattern at :1082-1083 - proving the omission. Upstream carries the same unguarded call. Guard-mirror fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4200 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
trigger_df4200.c β€” 4.0 KB view raw
build.sh β€” 87 B view raw
run.sh β€” 89 B view raw
build.log β€” 9 B view raw
run.log β€” 110 B view raw
env.txt β€” 244 B view raw
fix.diff β€” 592 B view raw
manifest.json β€” 1006 B view raw
verdict.json β€” 3.7 KB view raw

DF-4200 β€” NULL encoder_funcs->detect call in amdgpu_connector_dp_detect

File: sys/dev/drm/amd/amdgpu/amdgpu_connectors.c:1385-1388 (pass 2) Severity: Low (hardware/VBIOS-gated kernel NULL-function-pointer panic) Status: not reproduced at runtime β€” hardware-gated (no amdgpu ASIC on the audit guest); source-level certainty.

Root cause

} else if (amdgpu_connector->dac_load_detect) {          /* :1385 */
    const struct drm_encoder_helper_funcs *encoder_funcs = encoder->helper_private;
    ret = encoder_funcs->detect(encoder, connector);     /* :1387 β€” no NULL check */
}

This is the DP-bridge branch (amdgpu_connector_encoder_get_dp_bridge_encoder_id() != ENCODER_OBJECT_ID_NONE, i.e. a Travis/Nutmeg DP-to-VGA/HDMI bridge laptop). encoder comes from amdgpu_connector_best_single_encoder() (:1335) which returns the first possible encoder, and possible encoders are attached in encoder_list order (amdgpu_encoders.c:45-56), i.e. in ATOMBIOS object-table order. The external bridge encoders β€” exactly the TRAVIS/NUTMEG devices that make this branch reachable β€” are initialized with dce_v{8,10,11}_0_ext_helper_funcs, which deliberately contain no .detect member ("no detect for TMDS/LVDS yet", dce_v10_0.c:3411-3418). If the external bridge is enumerated before the internal UNIPHY encoder, best_single_encoder hands back the ext encoder and :1387 calls NULL β†’ instant kernel panic on the first forced probe where the aux/DDC probe at :1383 fails (nothing attached / adapter without DDC) and dac_load_detect is true β€” and it defaults to true for DP-bridge VGA and DVI-I connectors (amdgpu_connectors.c:1599, :1646).

The sibling path in the same file proves the omission: dvi_detect guards the identical pattern with if (encoder_funcs->detect) (amdgpu_connectors.c:1083). Upstream Linux carries the same unguarded call.

Reach

Unprivileged-with-display-ownership: any hotplug/poll cycle (drm_kms_helper_poll, HPD IRQ work amdgpu_irq.c:80-94) or a DRM_IOCTL_MODE_GETCONNECTOR probe from the display server β€” no DRM master needed for the probe itself.

Evidence

  • trigger_df4200.c β€” self-contained (no libdrm) probe trigger; builds and runs on the guest; guest has no AMD GPU (env.txt: no /dev/dri, no AMD/ATI PCI devices) so the amdgpu path is unreachable there.
  • fix.diff β€” one-guard fix mirroring :1083.

Fix

            else if (amdgpu_connector->dac_load_detect) { /* try load detection */
                const struct drm_encoder_helper_funcs *encoder_funcs = encoder->helper_private;
-               ret = encoder_funcs->detect(encoder, connector);
+
+               if (encoder_funcs->detect)
+                   ret = encoder_funcs->detect(encoder, connector);
            }
VERDICT.md
↓ download raw

DF-4200 VERDICT β€” NULL function-pointer call at amdgpu_connectors.c:1387

Verdict: not reproduced at runtime (hardware-gated); source-level certainty: certain. Impact if exercised on affected hardware: kernel panic (DoS).

The chain, line by line

  1. amdgpu_connector_dp_detect() runs for every DP-family connector on every probe (HPD poll amdgpu_irq.c:80-94 β†’ connector detect; or userspace DRM_IOCTL_MODE_GETCONNECTOR).
  2. encoder = amdgpu_connector_best_single_encoder(connector) (amdgpu_connectors.c:1335) returns the first entry of the connector's possible-encoder list.
  3. Possible encoders are attached by amdgpu_link_encoder_connector() (amdgpu_encoders.c:45-56) walking mode_config.encoder_list β€” the order in which dce_v*_0_encoder_add created them from the ATOMBIOS encoder object table.
  4. For a DP-bridge connector (Travis/Nutmeg β€” the exact devices listed at amdgpu_connectors.c:1547-1550), the encoder list contains both the external bridge encoder and the internal UNIPHY encoder, in BIOS order.
  5. External bridge encoders get dce_v{8,10,11}_0_ext_helper_funcs, which have no .detect (dce_v10_0.c:3411-3418 β€” source comment "no detect for TMDS/LVDS yet"; same in dce_v8_0.c:3299, dce_v11_0.c:3537).
  6. If the bridge encoder is first (or is the only one whose devices mask overlaps), and the aux/DDC probe at :1383 fails (unplugged adapter), and dac_load_detect is true (default true for DP-bridge VGA/DVI-I connectors β€” set at :1599 and :1646 during amdgpu_connector_add), then :1387 encoder_funcs->detect(encoder, connector) is a call through a NULL function pointer β†’ immediate kernel panic.
  7. The same file guards this exact pattern at :1082-1083 (if (encoder_funcs->detect)) β€” the guard at :1387 was simply omitted.

Runtime attempt

trigger_df4200.c (raw ioctl, self-contained struct definitions) built cleanly with the guest cc 8.3 and ran; the guest exposes no DRM device (/dev/dri absent, QEMU virtual display only, no AMD/ATI PCI devices β€” env.txt), so amdgpu_connector_dp_detect cannot be entered: the deterministic trigger needs a Travis/Nutmeg bridge laptop with the bridge encoder enumerated first in its VBIOS object table, plus a failing DDC probe. Classification mirrors the radeon twins (DF-4033..4035): not_reproduced at runtime, certain at source level.

Why not a false positive

  • .detect absence is deliberate and documented in-source for ext encoders; only TMDS ext encoders exist for the bridge IDs referenced.
  • The guard exists in the sibling path β€” this is not a "detect can never be NULL" invariant.
  • BIOS object-table order determines the first possible encoder; nothing in the driver reorders possible encoders (drm_connector_attach_encoder appends; amdgpu_encoders.c:45-56 iterates in list order).

Fix validation

Not possible on this guest (no AMD GPU). fix.diff mirrors the in-file guard at :1083 and is compile-checked against the tree (applies cleanly, patch --dry-run verified locally against the read-only sys/ tree).

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

fix.diff applies cleanly to a pristine copy of the real amdgpu_connectors.c (verified, cumulative with DF-4201/4202 diffs) and mirrors the in-file guard at :1083; kernel-build validation not possible on this guest (no AMD GPU).

fix.diff; local patch --dry-run + applied-copy verification noted in VERDICT.md
↓ fix.diffper-fix-DF-4200

Confirmed kernel references

Detail

Exploit chain

unprivileged hotplug/poll cycle (or DRM_IOCTL_MODE_GETCONNECTOR probe) on a Travis/Nutmeg DP-bridge laptop whose VBIOS enumerates the bridge encoder first, with nothing attached (DDC probe fails) -> NULL fn-ptr call -> kernel panic

Evidence (decisive lines)

VERDICT.md (line-by-line chain incl. the three dce ext_helper_funcs lacking .detect); trigger_df4200.c + run.log (guest run, hardware-gated); fix.diff (guard mirroring :1083); env.txt

PoC changes

trigger written from scratch (raw ioctls, self-contained struct defs, no libdrm); __u32/__u64 typedefs added after first guest compile

Verified recommended fix

Guard the call like the sibling dvi_detect path: if (encoder_funcs->detect) ret = encoder_funcs->detect(encoder, connector); (see fix.diff)

Verdict

amdgpu_connector_dp_detect calls encoder_funcs->detect without a NULL check at amdgpu_connectors.c:1387. The DP-bridge branch is entered for Travis/Nutmeg bridge connectors; best_single_encoder (:1335) returns the FIRST possible encoder, which is the external bridge encoder whenever the VBIOS object table enumerates it first (amdgpu_encoders.c:45-56 attaches in list order) β€” and dce_v{8,10,11}_0_ext_helper_funcs deliberately have NO .detect ('no detect for TMDS/LVDS yet', dce_v10_0.c:3411-3418). Failing aux/DDC probe (:1383) + default-true dac_load_detect for DP-bridge VGA/DVI-I connectors (:1599,:1646) then makes :1387 a call through a NULL function pointer = kernel panic on probe. The sibling dvi_detect guards the identical pattern with 'if (encoder_funcs->detect)' at :1082-1083 β€” the guard here was omitted. Not reproduced at runtime: audit guest has no AMD GPU (no /dev/dri, no AMD/ATI PCI β€” env.txt); trigger built and ran on the guest, hardware-gated. Same unguarded call exists in upstream Linux.