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.
Summary
parseAVDevicessplits ffmpeg'savfoundation -list_devices truestderr into the video and audio device lists by detecting the two section-header lines with a whole-linestrings.Contains, run before the device-row regex and short-circuited withcontinue: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 substringAVFoundation audio devices:orAVFoundation video devices:, its own row matches a header case: the row is dropped andsectionflips, 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):
[0] Evil AVFoundation video devices:matches thevideo devices:header case → row dropped,sectionflips to video → the next row[1] USB audio CODECis appended to video. Result:audio = [].selectDevicesthen returnsno avfoundation audio input devices foundandrecordaborts — a false "no microphone" on a machine that has one.[0]and[1]rows parse as devices under the audio section →audio = [Evil…, USB audio CODEC],selectDevicessucceeds.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 thecontinue. 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, withrecordexiting 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
internal/record/recorders.go:82-110(parseAVDevices), specifically the headerswitchatinternal/record/recorders.go:85-92running before the device regex at:93.deviceLineatinternal/record/recorders.go:78(requires a[N]index a header never has).selectDevicesinternal/record/recorders.go:120-141(empty-audio abort; screen-index by name),formatAudioRoster:151, andinternal/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)
parseAVDevicesheader 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 throughsession.SafeText(Lines)for terminal safety only; they do no structure detection, so the pattern does not recur there.looksLikeAVFailure(internal/record/tcc.go) —Containson lowercased signatures, but it only picks an error-hint string; a misclassification there is benign, not a dropped/misrouted record.selectDevicespicks the screen bystrings.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)
recorders.go:145and its tests sanitise device names because 'a crafted USB/virtual-audio device can set' them) — containing the literal substringAVFoundation video/audio devices:is producible with no macOS naming constraint blocking it, reachesparseAVDevicesvia ffmpeg stderr, and silently drops/misclassifies devices, defeating the documented anti-shadowing purpose of the advisory roster."switchruns a whole-linestrings.Containsbefore the device regex, so a[N]-indexed row whose name contains the substring is dropped andsectionflips, with no guard or downstream normalization correcting it; placing the header check only on lines the index regex does not match yields the correctaudiolist — 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]-indexeddeviceLineregex 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.