[Bug]: ALSA backend: input request narrower than the device minimum makes getActiveInputChannels() disagree with the callback array; first callback dereferences null
Detailed steps on how to reproduce the bug
When an application asks an ALSA device for fewer input channels than its capture stream can run (1 on a codec fixed at 2, 2 on an interface fixed at 8), the backend widens the mask it reports through getActiveInputChannels() to the device minimum but builds the callback's pointer array from the original request. AudioDeviceManager sizes its buffers from the reported count, gets fewer pointers, pads with null, and the first callback dereferences one: segfault at address 0. Debug builds hit the assertion at juce_AudioDeviceManager.cpp:111 first.
Reproduced on JUCE 9.0.1 and 9.0.2 (the file is identical on develop) with an onboard Realtek ALC233 codec and with an XMOS UAC2 interface. Requesting all channels, or output only, never crashes. The output path already handles this case correctly; the attached 8-line patch treats the input mask the same way, and is verified to remove the crash on pristine 9.0.2. It is independent of the duplex-start report filed separately as #1741.
The zip contains BUG_REPORT_channel_mask.md (mechanism with line numbers, evidence, reproduction, fix and its alternative), juce-alsa-input-channel-mask.patch, and ALSADuplexProbe.h, a PIP that reproduces it from the command line:
ALSADuplexProbe --device="<card>; Direct hardware" --inputs=1 --outputs=2 --rate=48000 --buffer=256 --seconds=5
juce-alsa-input-channel-mask-report.zip
ALSA backend: requesting fewer input channels than the device minimum makes getActiveInputChannels() disagree with the callback's pointer array, and the first callback dereferences null
Attachments: juce-alsa-input-channel-mask.patch (the proposed fix, 8 lines) and
ALSADuplexProbe.h (a PIP that reproduces it from the command line).
Summary
When an application opens an ALSA device asking for fewer input channels than the hardware
can run, for example 1 channel on a codec whose capture stream is fixed at 2, or 2 channels
on an interface fixed at 8, the backend widens the channel mask it reports through
getActiveInputChannels() to the device minimum, but builds the array of pointers it passes
to the audio callback from the original, narrower request. AudioDeviceManager sizes its
per-channel buffers from the reported count, receives fewer pointers than that, and pads the
rest with null. The first callback reads through one of them and the process dies with a
segmentation fault at address 0. In a Debug build the mismatch is caught first by the
assertion at juce_AudioDeviceManager.cpp:111.
Any device with a fixed capture channel count is affected as soon as an application asks for less than all of them, which is the natural request for a stereo application on a multichannel interface. The output path already handles the same situation correctly, so the fix is to treat the input mask the same way.
Environment
- JUCE 9.0.1 (e18f7f5) and 9.0.2 (7278278),
modules/juce_audio_devices/native/juce_ALSA_linux.cppandmodules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp. The ALSA file is byte-identical in both releases and on thedevelopandmasterbranches as of 2026-09-10 (md5 b9669055425ce2dd34f99e2fcbc23741), so the line numbers below apply to all of them. - Debian 13, kernel 6.12, x86_64, ALSA
hw:devices opened directly. - Reproduced on an onboard Intel HDA codec (Realtek ALC233, capture fixed at 2 channels, request 1) and on an XMOS xCORE.ai UAC2 interface (capture fixed at 8 channels, request 2).
Mechanism
All in ALSAThread::open():
- Lines 523-524:
maxInputsRequestedis raised tominChansIn, so the input buffer is allocated with the device's channel count. Correct. - Lines 531-539:
inputChannelDataForCallbackandcurrentInputChansare filled by iterating the requested mask, so they end up with only the requested channels: 1 pointer, 1 bit. - Line 568, just before
setParameters:ensureMinimumNumBitsSet (currentInputChans, minChansIn)widens the mask to the device minimum. From here ongetActiveInputChannels()reports 2 (or 8) channels, whileinputChannelDataForCallbackstill holds 1 (or 2) pointers. - Line 543 shows the output path doing it the other way round:
ensureMinimumNumBitsSet (outputChannels, minChansOut)runs before the output pointer array is built, so for outputs the mask and the pointers always agree.
The consequence lands in AudioDeviceManager's CallbackMaxSizeEnforcer
(juce_AudioDeviceManager.cpp around lines 95-125):
audioDeviceAboutToStart()resizesstoredInputChannelstodevice->getActiveInputChannels().countNumberOfSetBits(), the widened count.audioDeviceIOCallbackWithContext()asserts that this equalsnumInputChannels(line 111), then copies thenumInputChannelspointers it was given into the front of the larger array and passes the whole array on. The tail entries are null.- The inner callback path reads input samples through the array and dereferences a null entry.
Evidence
Stock JUCE 9.0.2, Debug build of the probe, onboard ALC233 codec, request 1 of 2 input channels:
JUCE v9.0.2
JUCE Assertion failure in juce_AudioDeviceManager.cpp:111 (9 times, one per callback)
Segmentation fault, exit status 139
kernel: ALSADuplexProbe[10640]: segfault at 0 ip ... error 4 (error 4 = user-mode read)Same on JUCE 9.0.1, Release build, XMOS interface, request 2 of 8 input channels: no assertion (Release), segfault at address 0 on the first callback, five times out of five launches.
Requesting all of the device's input channels (2 of 2, or 8 of 8) never crashes, and neither does output-only, which is consistent with the mechanism: the mask and the pointer array only disagree when the input request is narrower than the device minimum.
Reproduction
ALSADuplexProbe.h is a PIP; build it and run from the command line against any device whose
capture stream has a fixed channel count. An onboard codec is enough:
ALSADuplexProbe --device="<card>; Direct hardware" --inputs=1 --outputs=2 --rate=48000 --buffer=256 --seconds=5Stock: Debug prints the assertion per callback and then segfaults; Release segfaults directly.
With --inputs=2 (the device's count) it runs. The probe's own report line shows the count the
device ends up with, e.g. 2 in / 2 out.
In the GUI, the same happens when a user unticks one input channel of such a device in an
AudioDeviceSelectorComponent: the checkbox does not stick, because the widened mask is read
back, and each click reopens the device with a mismatched pointer array. Whether that crashes
depends on whether the padded entries happen to be null or stale pointers from a previous open.
Proposed fix
Widen the requested input mask to the device minimum before the callback pointer array is
built from it, exactly as the output path does at line 543. Eight lines at the top of
ALSAThread::open(), guarded so that a request for no inputs stays a request for no inputs:
if (inputChannels.getHighestBit() >= 0)
ensureMinimumNumBitsSet (inputChannels, (int) minChansIn);The later widening at line 568 then becomes a no-op and can stay or go. The application
receives all of the device's input channels in the callback, which is what already happens
for outputs on such devices, and getActiveInputChannels() describes exactly the array the
callback gets.
An alternative with the opposite semantics would be to drop the widening at line 568 instead, so the application sees only the channels it asked for while the device runs all of them. Both restore the invariant; we chose the one that mirrors the existing output behaviour. Either way, the reported mask and the pointer array have to be built from the same set of bits.
Verified with pristine 9.0.2 plus this patch, same request as above:
exit 0, no assertion
Setup : 48000 Hz, block 256, 2 in / 2 out, ...
Capture : card2/pcm0c/sub0 RUNNING period 256 buffer 1024and --inputs=2 unchanged. The patch applies cleanly with git apply to pristine 9.0.1,
9.0.2 and develop (kept CRLF to match the upstream file), and compiles without warnings.
Relation to the duplex start report
This is independent of the linked-duplex-start defect reported separately (pre-roll patch): different lines, different mechanism, and the two patches apply together in either order. We found this one while measuring that one, because the probe with a partial input request crashed before it could measure anything. The workaround our own application has carried for some time, forcing every input channel on before opening the device, turns out to have been avoiding exactly this mismatch.
What is the expected behaviour?
Requesting fewer input channels than the device's minimum should open the device without crashing, and getActiveInputChannels() should describe exactly the pointer array the audio callback receives. Either the backend widens the request to the device minimum before building the callback array (as it already does for outputs, so the application receives all of the device's input channels), or it keeps the requested channels and reports only those; in both cases the reported mask and the callback's pointers must be built from the same set of channels, so AudioDeviceManager never has to pad the array with null pointers.
Operating systems
Linux
What versions of the operating systems?
Debian 13, kernel 6.12
Architectures
x86_64
Stacktrace
JUCE v9.0.2
JUCE Assertion failure in juce_AudioDeviceManager.cpp:111
JUCE Assertion failure in juce_AudioDeviceManager.cpp:111
(repeats once per audio callback, 9 times in 5 s, Debug build)
Segmentation fault, exit status 139
kernel: ALSADuplexProbe[10640]: segfault at 0 ip 00005592b835e1f3 sp 00007fd64dcd48a0 error 4
(Release builds segfault directly, without the assertion)Plug-in formats (if applicable)
No response
Plug-in host applications (DAWs) (if applicable)
No response
Testing on the develop branch
The bug is present on the develop branch
Code of Conduct
- I agree to follow the Code of Conduct
Source: juce-framework/JUCE