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

Error-path UAF: amdgpu_dm init failure paths kfree registered i2c adapters / connectors / encoders / planes that remain in DRM core lists

Summary

amdgpu_dm_connector_init() registers the DDC adapter via i2c_add_adapter but out_free kfrees it on later failure without i2c_del_adapter - the adapter stays in the i2c core pointing at freed memory. amdgpu_dm_initialize_drm_device()'s fail path unconditionally kfrees the last aencoder/aconnector and all planes even when fully registered in mode_config lists; the driver's own error unwind then calls drm_mode_config_cleanup() which walks those lists over the freed objects, and pending HPD IRQs use the freed aconnector cookie. Fault-injection/OOM class: an allocation or debugfs failure during DM init (boot/module load) turns the normal error unwind into a UAF - not attacker-timing-controllable, hence Low. Contrast DF-1153 which covers different functions and lifetime owners.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3656 Β· 6 files
FileTypeDescriptionSize
env.txt β€” 323 B view raw
VERDICT.md β€” 4.1 KB ↓ raw
fix.diff β€” 2.0 KB view raw
manifest.json β€” 997 B view raw
verdict.json β€” 3.6 KB view raw
README.md β€” 2.5 KB ↓ raw

DF-3656 β€” Error-path UAF: registered i2c adapter / connector / encoder / planes freed while still in core lists

File: sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c Lines: 4080-4136 (amdgpu_dm_connector_init out_free), 1937-1942 (amdgpu_dm_initialize_drm_device fail) Severity: Low (requires OOM / debugfs failure at driver init β€” fault-injection class) Β· CWE-416 Β· confidence: certain

What

Two broken unwinds leave dangling pointers in DRM/i2c core lists:

  1. amdgpu_dm_connector_init() registers the DDC i2c adapter (i2c_add_adapter(&i2c->base), amdgpu_dm.c:4080), then continues (drm_connector_init 4089, connector_debugfs_init 4120). On any later failure it runs out_free: which does kfree(i2c) (4133) without i2c_del_adapter(), and returns an error. If drm_connector_init had already succeeded (debugfs failure case), the connector is also still registered in dev->mode_config.connector_list while the caller frees it.

  2. amdgpu_dm_initialize_drm_device()'s fail: path (1937-1942) β€” reached e.g. from dce110_register_irq_handlers() failure (1922) after all connectors/encoders were fully created and registered β€” unconditionally does kfree(aencoder); kfree(aconnector); for the last-allocated pair and kfree(mode_info->planes[i]) for all planes, none of which are de-registered. Every one of those objects is still referenced by dev->mode_config.{connector,encoder,plane}_list, and aconnector is still the link->priv HPD-cookie and the mst_mgr owner.

The error path then runs amdgpu_dm_fini() β†’ amdgpu_dm_destroy_drm_device() β†’ drm_mode_config_cleanup() (1947) which iterates those very lists and calls cleanup on the freed memory β†’ use-after-free. Likewise any later i2c transaction on the DDC bus dereferences the freed adapter (i2c_get_adapdata).

Trigger

Allocation failure or debugfs failure during display-manager init (module load / boot / GPU reset re-init). Not attacker-timing-controllable without fault injection β€” hence Low severity β€” but it is exactly the error-path lifetime class the pass-2 audit targets (contrast DF-1153, which covers the reset functions, not this init unwind).

Reproduction

Not runnable on the audit guest (virtio-only, no AMD GPU β€” env.txt); source-level certainty. No harness is meaningful for an OOM-timing UAF; the VERDICT.md contains the full ownership trace.

Contents

env.txt Β· VERDICT.md Β· fix.diff Β· manifest.json Β· verdict.json Β· README.md

VERDICT.md
↓ download raw

DF-3656 β€” VERDICT

Status: inconclusive (runtime hardware+OOM-gated on the virtio-only guest); ownership defect is source-certain.

Root cause (ownership trace)

Site 1 β€” amdgpu_dm_connector_init() (amdgpu_dm.c:4058-4137)

4073   i2c = create_i2c(link->ddc, link->link_index, &res);
...
4080   res = i2c_add_adapter(&i2c->base);       /* ADAPTER REGISTERED */
4082   if (res) { ...; goto out_free; }
4089   res = drm_connector_init(dm->ddev, &aconnector->base, ...);
                                                  /* CONNECTOR REGISTERED */
4095   if (res) { ...; goto out_free; }
...
4119 #if defined(CONFIG_DEBUG_FS)
4120   res = connector_debugfs_init(aconnector);
4121   if (res) { ...; goto out_free; }          /* connector ALREADY registered */
...
4131 out_free:
4132   if (res) {
4133           kfree(i2c);                        /* freed while registered  */
4134           aconnector->i2c = NULL;
4135   }
4136   return res;
  • On the debugfs-failure path the registered i2c adapter is kfree'd without i2c_del_adapter() β€” it remains in the i2c core's adapter list with adapdata pointing at freed memory; the next DDC transaction on that bus (EDID probe, CEC) is a UAF.
  • The caller (amdgpu_dm_initialize_drm_device, 1875-1878) then goto fail and additionally kfrees the registered aconnector (see site 2), which is still linked in dev->mode_config.connector_list, still link->priv (4069), and still owns dm_dp_aux/mst_mgr.

Site 2 β€” amdgpu_dm_initialize_drm_device() fail path (1937-1943)

1937 fail:
1938   kfree(aencoder);
1939   kfree(aconnector);
1940   for (i = 0; i < dm->dc->caps.max_planes; i++)
1941           kfree(mode_info->planes[i]);
1942   return -EINVAL;

Reached after full registration when e.g. dce110_register_irq_handlers() fails (1915-1918/1922-1925 β€” amdgpu_irq_add_id() failure) or any connector-loop init step fails for the last pair:

  • aencoder was registered by amdgpu_dm_encoder_init() β†’ drm_encoder_init() (4164) β†’ linked in mode_config.encoder_list; kfree'd without drm_encoder_cleanup() β†’ dangling.
  • aconnector registered via drm_connector_init (plus link->priv, link->ddc i2c, CEC) β†’ dangling.
  • mode_info->planes[i] registered via drm_universal_plane_init() (3647/3657/3667) β†’ linked in mode_config.plane_list; kfree'd without drm_plane_cleanup() β†’ dangling.

The error unwinds via amdgpu_dm_init() error β†’ amdgpu_dm_fini() (487) β†’ amdgpu_dm_destroy_drm_device() β†’ drm_mode_config_cleanup(dm->ddev) (1947), which walks connector_list/encoder_list/plane_list and calls the objects' ->destroy hooks on freed memory β†’ use-after-free (deterministic once the error path is taken). Additionally, any HPD IRQ delivered before the lists are pruned uses the freed aconnector cookie (handle_hpd_irq(void *param) β†’ aconnector->hpd_lock at 1083).

Distinct from DF-1153 (reset-function kzalloc-failure UAF): different functions, different lifetime owners (DRM core lists vs crtc/plane state).

Why Low

Triggering requires an allocation or debugfs failure during DM init β€” an OOM/fault-injection condition, not attacker-timing-controllable. Impact once triggered: UAF in drm_mode_config_cleanup / i2c core β€” memory corruption class, but the practical exposure is boot/module-load flakiness.

Verification (Phase V)

Guest is virtio-only (env.txt) β€” amdgpu never attaches; additionally the trigger needs an induced allocation failure at exactly this window. No meaningful userspace model exists for a kernel-object ownership bug; verdict recorded as inconclusive with the source trace as evidence. Fix: fix.diff (de-register before free / let mode_config own registered objects); git apply --check clean. fix_status: not_testable.

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

Requires amdgpu hardware plus fault injection to validate (baseline UAF vs patched clean teardown). fix.diff implements de-registration/ownership fix; git apply --check verified against sys/; not applied (read-only audit target).

fix.diff in this pack
↓ fix.diffper-fix-DF-3656

Confirmed kernel references

Detail

Exploit chain

Fault-injected amdgpu init failure after registration (e.g. amdgpu_irq_add_id failure at 1915, or connector_debugfs_init failure at 4120) -> fail path kfrees registered objects -> amdgpu_dm_fini -> drm_mode_config_cleanup iterates mode_config lists over freed memory -> use-after-free write/read during teardown (or freed i2c adapter dereference on the next DDC access).

Evidence (decisive lines)

VERDICT.md (full ownership trace with registration/free line proofs); fix.diff (de-register i2c before free; only free not-yet-registered objects, let drm_mode_config_cleanup own the rest; git apply --check clean); env.txt (guest has no AMD GPU)

PoC changes

n/a β€” no seeded PoC; no harness is meaningful for an OOM-timing kernel-object ownership bug

Verified recommended fix

In amdgpu_dm_connector_init failure paths call i2c_del_adapter() before kfree(i2c); in amdgpu_dm_initialize_drm_device's fail path only free objects DRM core does not yet own (sentinel ids) and leave registered objects to drm_mode_config_cleanup().

Verdict

Source-certain error-path UAF, not runnable on the audit guest. (1) amdgpu_dm_connector_init() registers the DDC adapter with i2c_add_adapter (amdgpu_dm.c:4080) but its out_free handler kfrees it on any later failure (4133) without i2c_del_adapter β€” the adapter stays in the i2c core with adapdata pointing at freed memory; the next DDC/EDID/CEC transaction is a UAF. (2) amdgpu_dm_initialize_drm_device()'s fail path (1937-1942) unconditionally kfrees the last aencoder/aconnector and all mode_info->planes[] even when they were fully registered into dev->mode_config.{encoder,connector,plane}_lists (encoder via amdgpu_dm_encoder_init:4164, connector via drm_connector_init:4089, planes via drm_universal_plane_init:3647+) β€” and the driver's own error unwind immediately calls drm_mode_config_cleanup() (amdgpu_dm_destroy_drm_device:1947) which walks those lists and invokes the freed objects' destroy hooks; HPD IRQs also use the freed aconnector cookie (handle_hpd_irq -> aconnector->hpd_lock, 1083). Trigger requires an allocation or debugfs failure during DM init (fault-injection class) β€” hence Low severity. Guest is virtio-only (env.txt) and the trigger additionally needs induced OOM, so runtime reproduction is impossible here: inconclusive, source-certain. Distinct from DF-1153 (reset-function kzalloc failures), different functions and lifetime owners.