AmAudio/AmAudioFile: bound read size against the sample buffer - #568
Conversation
|
It dies in Debian 11 (bullseye) reached end of security support, so its There is no fix for it in the tree yet to carry into this PR. Unblocking it needs a packaging-side change (bump the base image off bullseye, or pass The compile itself is fine: a full Generated by Claude Code |
There was a problem hiding this comment.
🟡 Changes recommended
Two critical buffer-overrun paths remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request adds bounds checks for audio reads to protect fixed-size buffers from malformed sample-rate ratios.
Changes:
- Bounds computed reads in
AmAudio::get(). - Bounds file reads before
fread().
Review findings:
core/AmAudio.cpp:312— Critical (3 votes): validate the scaled sample count before conversion tointand passing it tocalcBytesToRead.core/AmAudioFile.cpp:381— Critical (1 vote): account for decoder output expansion after reading.
File summaries
| File | Summary |
|---|---|
core/AmAudioFile.cpp |
Adds file-read bounds; decoder output can still exceed the buffer. |
core/AmAudio.cpp |
Adds computed-read validation; validation occurs too late. |
Review details
Suppressed comments (3)
core/AmAudio.cpp:312
- This only bounds the pre-read size; the later
decode()andresampleOutput()can expand that data before line 338 copies it. In particular, when the input rate is lower thanoutput_sample_rate, the resampler writes/returns more thanAUDIO_BUFFER_SIZEwithout receiving a destination capacity, sosamplesand the caller's fixed buffer can still be overrun. Reject or otherwise capacity-check the final PCM size before invoking the resampler.
if(size < 0 || (unsigned int)size > AUDIO_BUFFER_SIZE){
core/AmAudio.cpp:312
AmRtpAudio::get()overrides the base method, so it never reaches this guard. It performs the same SDP-controlled rate scaling and passes the resulting sample count toplayout_buffer->read()before copying tobuffer; the B2B path calls this with the destination leg's rate, so a high-rate source/low-rate output can still write past thesamplesDblBuffer. Add an equivalent bound to the override or centralize the bounded read path.
if(size < 0 || (unsigned int)size > AUDIO_BUFFER_SIZE){
core/AmAudio.cpp:318
- Checking the encoded
read()size here does not bound whatdecode()writes into the back half ofsamples. For example, the PCMU/PCMA decoders writesize * 2PCM bytes, so an input size of exactly 8192 passes this check butAmAudio::decode()writes 16384 bytes into an 8192-byte half-buffer. The limit must account for decoder expansion before callingdecode().
if(size < 0 || (unsigned int)size > AUDIO_BUFFER_SIZE){
ERROR("AmAudio::get: refusing read of %i bytes (max %i): nb_samples=%u, "
"input_rate=%i, output_rate=%i\n",
size, AUDIO_BUFFER_SIZE, nb_samples, getSampleRate(),
output_sample_rate);
return -1;
}
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
AmAudio::get() computes how much to read as
calcBytesToRead((int)((float)nb_samples * (float)getSampleRate()
/ (float)output_sample_rate))
and passes the result straight to read(), which fills one half of the
'samples' DblBuffer. decode() then expands that into the other half as
PCM16, and the resampled result is memcpy()'d into the caller's
'buffer'. All three are AUDIO_BUFFER_SIZE bytes - callers size 'buffer'
that way as well, see AmMediaProcessorThread - and nothing bounded the
request against any of them.
Both getSampleRate() (the negotiated remote codec rate) and
output_sample_rate (the other leg's rate) come from SDP, so a skewed
ratio between them scales the request up without bound. The narrowing
to int is undefined for an out-of-range value, and the huge unsigned
sample count it yields can wrap inside samples2bytes() into a byte
count that looks valid, so the scaled sample count is validated before
it is narrowed and handed to the codec. The bound is expressed in
samples against PCM16_B2S(AUDIO_BUFFER_SIZE), which is what makes the
*decoded* frame fit; the encoded size is then bounded separately
against the half it is read into. A zero or negative rate on either
side is rejected up front instead of producing an inf/NaN ratio.
AmAudioFile::read() has the same exposure one level down: it fread()s
'size' bytes into the DblBuffer with no bound check of its own. Bound
it by the encoded size and by what that decodes to - for PCMU/PCMA the
decoded frame is twice the encoded one, so an encoded bound of
AUDIO_BUFFER_SIZE on its own would still let decode() write past the
half it targets.
AUDIO_BUFFER_SIZE is 8192 bytes, i.e. 4096 PCM16 samples, several
hundred milliseconds at any rate SEMS negotiates, so no legitimate
frame is affected: a 20 ms G.711 frame is 160 samples / 160 bytes, and
even a 48 kHz stream feeding an 8 kHz leg asks for 960 samples.
c78e593 to
5358db7
Compare
Add a test_amaudio suite for the read size checks added in #568. It registers a test-only L16-like codec (2 bytes per sample, no decode step), a G.711-like codec (1 byte per sample, decode doubles the size) and a header-less file format, and covers: - AmAudio::get() rejecting a zero or negative rate on either side, including a rate ratio that truncates to zero samples; - the bound on the scaled sample count: 4096 samples accepted and 4097 rejected for both codecs and for a 2:1 rate ratio, and a skewed 48 kHz to 8 kHz ratio up to nb_samples = UINT_MAX; - the bound on the encoded size, reached with stereo L16 (2048 samples accepted, 2049 rejected); - AmAudioFile::read() rejecting more than AUDIO_BUFFER_SIZE bytes, and G.711 reads whose decoded frame would not fit, mono and stereo; - regular 10-60 ms frames at 8-48 kHz, mono and stereo, through get() on both an AmAudio and an AmAudioFile, with unchanged read and returned sizes. A rejected request has to return -1 without reaching read() or fread(): the AmAudio used for get() counts its read() calls, and the file tests check the stream position and the EOF indicator. The stand-in read() refuses anything that would not fit instead of writing it, and the file tests only issue oversized reads where no payload is left, so without the checks the suite fails on assertions rather than by corrupting memory. That matters because ASan is not a gating CI job, and UBSan does not see these overflows. Against the tree before #568, 6 of the 8 cases fail. Removing any one of the checks from the current tree fails at least one case, except for the size < 0 half of the encoded size check, which the unsigned size > AUDIO_BUFFER_SIZE half already covers. Resampling is switched off in these tests: the internal resampler reads sinc[i][256] in core/resample/resample.cpp, which aborts sems_tests in the UBSan job. That is a separate, pre-existing issue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bug
AmAudio::get()computes how much to read as:sizeis never checked against anything before it is used:read()fills thesamplesDblBuffer, which only ever exposesAUDIO_BUFFER_SIZE(8192) bytes at a time —unsigned char samples[AUDIO_BUFFER_SIZE * 2]with half live.memcpy()'d into the caller'sbuffer, which is alsoAUDIO_BUFFER_SIZE(AmMediaProcessorThread::buffer[AUDIO_BUFFER_SIZE], passed down throughreadStreams()).Both
getSampleRate()(the negotiated remote codec rate) andoutput_sample_rate(the other leg's rate) come from SDP. A skewed ratio between them — a high-rate codec feeding a low-rate output leg — scales the request up without bound, and a negative intermediate from thefloat→intconversion is passed straight through. The overrun lands inAmAudio's own object memory and in the media processor thread's buffer.AmAudioFile::read()has the same exposure one level down: itfread()ssizebytes into the sameDblBufferwith no bound check of its own.The fix
Reject oversized (and negative) requests in both places rather than overrunning.
AUDIO_BUFFER_SIZEis 8192 bytes — several hundred milliseconds of PCM16 at any rate SEMS negotiates — so no legitimate frame is affected. A typical 20 ms G.711 frame is 320 bytes.This complements the existing zero-clock-rate rejections (#541, #542, #495) by bounding the upper end of the same computation.
Validation
Full
cmakebuild of core and all apps: clean, no new warnings.Credit
Backported from sipwise/sems commit
c6a33f41("AmAudio/AmAudioFile: add buffer guards (overflow)", MT#65557). Thanks to the Sipwise team for the original fix; adapted to this tree (which still uses the plainintreturn path inAmAudio::get()).Generated by Claude Code