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.cpp
and modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp. The ALSA file is
byte-identical in both releases and on the develop and master branches 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:
maxInputsRequested is raised to minChansIn, so the input buffer is
allocated with the device's channel count. Correct.
- Lines 531-539:
inputChannelDataForCallback and currentInputChans are 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 on getActiveInputChannels() reports 2
(or 8) channels, while inputChannelDataForCallback still 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() resizes storedInputChannels to
device->getActiveInputChannels().countNumberOfSetBits(), the widened count.
audioDeviceIOCallbackWithContext() asserts that this equals numInputChannels (line 111),
then copies the numInputChannels pointers 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=5
Stock: 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 1024
and --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
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=5juce-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 nullAttachments:
juce-alsa-input-channel-mask.patch(the proposed fix, 8 lines) andALSADuplexProbe.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 passesto the audio callback from the original, narrower request.
AudioDeviceManagersizes itsper-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
modules/juce_audio_devices/native/juce_ALSA_linux.cppand
modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp. The ALSA file isbyte-identical in both releases and on the
developandmasterbranches as of 2026-09-10(md5 b9669055425ce2dd34f99e2fcbc23741), so the line numbers below apply to all of them.
hw:devices opened directly.request 1) and on an XMOS xCORE.ai UAC2 interface (capture fixed at 8 channels, request 2).
Mechanism
All in
ALSAThread::open():maxInputsRequestedis raised tominChansIn, so the input buffer isallocated with the device's channel count. Correct.
inputChannelDataForCallbackandcurrentInputChansare filled by iteratingthe requested mask, so they end up with only the requested channels: 1 pointer, 1 bit.
setParameters:ensureMinimumNumBitsSet (currentInputChans, minChansIn)widens the mask to the device minimum. From here on
getActiveInputChannels()reports 2(or 8) channels, while
inputChannelDataForCallbackstill holds 1 (or 2) pointers.ensureMinimumNumBitsSet (outputChannels, minChansOut)runs before the output pointer array is built, so foroutputs the mask and the pointers always agree.
The consequence lands in
AudioDeviceManager'sCallbackMaxSizeEnforcer(
juce_AudioDeviceManager.cpparound lines 95-125):audioDeviceAboutToStart()resizesstoredInputChannelstodevice->getActiveInputChannels().countNumberOfSetBits(), the widened count.audioDeviceIOCallbackWithContext()asserts that this equalsnumInputChannels(line 111),then copies the
numInputChannelspointers it was given into the front of the larger arrayand passes the whole array on. The tail entries are null.
Evidence
Stock JUCE 9.0.2, Debug build of the probe, onboard ALC233 codec, request 1 of 2 input channels:
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.his a PIP; build it and run from the command line against any device whosecapture stream has a fixed channel count. An onboard codec is enough:
Stock: 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 thedevice 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 readback, 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: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 thecallback 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:
and
--inputs=2unchanged. The patch applies cleanly withgit applyto 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
Plug-in formats (if applicable)
No response
Plug-in host applications (DAWs) (if applicable)
No response
Testing on the
developbranchThe bug is present on the
developbranchCode of Conduct