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)
PoC verification
Evidence pack
findings/poc/DF-4200 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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);
}
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
amdgpu_connector_dp_detect()runs for every DP-family connector on every probe (HPD pollamdgpu_irq.c:80-94β connector detect; or userspaceDRM_IOCTL_MODE_GETCONNECTOR).encoder = amdgpu_connector_best_single_encoder(connector)(amdgpu_connectors.c:1335) returns the first entry of the connector's possible-encoder list.- Possible encoders are attached by
amdgpu_link_encoder_connector()(amdgpu_encoders.c:45-56) walkingmode_config.encoder_listβ the order in whichdce_v*_0_encoder_addcreated them from the ATOMBIOS encoder object table. - 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.
- 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). - 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_detectis true (default true for DP-bridge VGA/DVI-I connectors β set at :1599 and :1646 duringamdgpu_connector_add), then :1387encoder_funcs->detect(encoder, connector)is a call through a NULL function pointer β immediate kernel panic. - 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
.detectabsence 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_testablefix.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
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_connectors.c:1335
- sys/dev/drm/amd/amdgpu/amdgpu_connectors.c:1383
- sys/dev/drm/amd/amdgpu/amdgpu_connectors.c:1385-1388
- sys/dev/drm/amd/amdgpu/amdgpu_connectors.c:1082-1083
- sys/dev/drm/amd/amdgpu/amdgpu_connectors.c:1599
- sys/dev/drm/amd/amdgpu/dce_v10_0.c:3411-3418
- sys/dev/drm/amd/amdgpu/amdgpu_encoders.c:45-56
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.
No comments yet.