Skip to content

record: parseAVDevices mistakes a device row for a section header when the device name contains "AVFoundation audio/video devices:", silently dropping and misclassifying inputs #68

Description

@REPPL

Summary

parseAVDevices splits ffmpeg's avfoundation -list_devices true stderr into the video and audio device lists by detecting the two section-header lines with a whole-line strings.Contains, run before the device-row regex and short-circuited with continue:

for _, line := range strings.Split(stderr, "\n") {
    switch {
    case strings.Contains(line, "AVFoundation video devices:"):
        section = 1
        continue
    case strings.Contains(line, "AVFoundation audio devices:"):
        section = 2
        continue
    }
    m := deviceLine.FindStringSubmatch(line) // never reached for a row that hit the switch
    ...
}

A device row is [AVFoundation indev @ 0x..] [N] <name>, and <name> (the regex's trailing .*) is attacker-influenceable OS metadata. If a device's name contains the substring AVFoundation audio devices: or AVFoundation video devices:, its own row matches a header case: the row is dropped and section flips, misclassifying every device listed after it.

Wrong assumption

"Only ffmpeg's own section-header lines contain the substring AVFoundation <video|audio> devices:." A device row can legitimately contain it, because the device name is arbitrary. The reliable discriminator — a header line has no [N] index, every device row does — already exists (deviceLine) but is not used to gate the section switch.

Trigger (input → observed vs. correct)

ffmpeg stderr from a session where one audio device carries the magic substring in its name (a crafted USB audio device or a virtual-audio driver, whose name the author chooses):

[AVFoundation indev @ 0x1] AVFoundation video devices:
[AVFoundation indev @ 0x1] [0] Studio Camera
[AVFoundation indev @ 0x1] [1] Capture screen 0
[AVFoundation indev @ 0x1] AVFoundation audio devices:
[AVFoundation indev @ 0x1] [0] Evil AVFoundation video devices:
[AVFoundation indev @ 0x1] [1] USB audio CODEC
  • Observed: line [0] Evil AVFoundation video devices: matches the video devices: header case → row dropped, section flips to video → the next row [1] USB audio CODEC is appended to video. Result: audio = []. selectDevices then returns no avfoundation audio input devices found and record aborts — a false "no microphone" on a machine that has one.
  • Correct: both [0] and [1] rows parse as devices under the audio section → audio = [Evil…, USB audio CODEC], selectDevices succeeds.

Second, silent variant (defeats the anti-shadowing safeguard): a virtual-audio driver that has made itself the system default, named e.g. Loopback AVFoundation video devices:, has its own row dropped by the continue. The mic roster (formatAudioRoster, internal/record/recorders.go:151), whose documented purpose (internal/record/record.go:475-478) is to make "a virtual audio driver shadowing the real mic visible before a session is recorded to silence", then never lists the shadowing device — the safeguard is silently defeated, with record exiting 0.

Scope note: the mic capture itself is unaffected — it records avfoundation :default (micArgs, internal/record/recorders.go:43-52), resolved by ffmpeg at capture time, not by this parse. Only the advisory roster, the audio-present check, and the screen-index resolution consume the parsed lists, which is why this is rated minor.

Location

  • Defect: internal/record/recorders.go:82-110 (parseAVDevices), specifically the header switch at internal/record/recorders.go:85-92 running before the device regex at :93.
  • Unused discriminator: deviceLine at internal/record/recorders.go:78 (requires a [N] index a header never has).
  • Consumers of the misparse: selectDevices internal/record/recorders.go:120-141 (empty-audio abort; screen-index by name), formatAudioRoster :151, and internal/record/record.go:471-481.

CWE anchor

CWE-436 (Interpretation Conflict) — a data line is interpreted as a control/structural marker — with an in-band-signaling flavour (the section delimiter is matched anywhere on the line rather than anchored to the delimiter's own shape). CWE-20 (Improper Input Validation) also fits.

Sibling sweep (whole-line-substring structure detection over subprocess output)

  • parseAVDevices header switch — this defect (the only place that detects a structural section from a whole-line substring over untrusted subprocess output).
  • tail() / outputTail / lockedBuffer.tail — route subprocess output through session.SafeText(Lines) for terminal safety only; they do no structure detection, so the pattern does not recur there.
  • looksLikeAVFailure (internal/record/tcc.go) — Contains on lowercased signatures, but it only picks an error-hint string; a misclassification there is benign, not a dropped/misrouted record.
  • Related but distinct (not this bug, and not validated here): selectDevices picks the screen by strings.Contains(d.name, "Capture screen") among video devices, so a crafted video device named to contain "Capture screen" could be mis-selected. Different mechanism (name-based selection, not section detection); noting it for a maintainer, not asserting it as a finding.

Validator confirmations (both lenses, independent)

  • Reachability lens: "A crafted USB/virtual-audio device name — the repo's own acknowledged untrusted boundary (recorders.go:145 and its tests sanitise device names because 'a crafted USB/virtual-audio device can set' them) — containing the literal substring AVFoundation video/audio devices: is producible with no macOS naming constraint blocking it, reaches parseAVDevices via ffmpeg stderr, and silently drops/misclassifies devices, defeating the documented anti-shadowing purpose of the advisory roster."
  • Correctness lens: "The header switch runs a whole-line strings.Contains before the device regex, so a [N]-indexed row whose name contains the substring is dropped and section flips, with no guard or downstream normalization correcting it; placing the header check only on lines the index regex does not match yields the correct audio list — a demonstrable output divergence the existing tests do not cover."

Fix direction (not a patch)

Detect a section header only on a line the [N]-indexed deviceLine regex does not match (or otherwise anchor the header test to a line with no device index), so any indexed device row is always parsed as a device.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions