Skip to content

AmAudio/AmAudioFile: bound read size against the sample buffer - #568

Merged
hecko merged 1 commit into
masterfrom
hecko/amaudio-read-size-guards
Sep 13, 2026
Merged

AmAudio/AmAudioFile: bound read size against the sample buffer#568
hecko merged 1 commit into
masterfrom
hecko/amaudio-read-size-guards

Conversation

@hecko

@hecko hecko commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The bug

AmAudio::get() computes how much to read as:

int size = calcBytesToRead((int)((float)nb_samples * (float)getSampleRate()
                                 / (float)output_sample_rate));
size = read(rd_ts, size);
...
memcpy(buffer, (unsigned char*)samples, size);

size is never checked against anything before it is used:

  • read() fills the samples DblBuffer, which only ever exposes AUDIO_BUFFER_SIZE (8192) bytes at a time — unsigned char samples[AUDIO_BUFFER_SIZE * 2] with half live.
  • the result is memcpy()'d into the caller's buffer, which is also AUDIO_BUFFER_SIZE (AmMediaProcessorThread::buffer[AUDIO_BUFFER_SIZE], passed down through readStreams()).

Both getSampleRate() (the negotiated remote codec rate) and output_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 the floatint conversion is passed straight through. The overrun lands in AmAudio's own object memory and in the media processor thread's buffer.

AmAudioFile::read() has the same exposure one level down: it fread()s size bytes into the same DblBuffer with no bound check of its own.

The fix

Reject oversized (and negative) requests in both places rather than overrunning.

AUDIO_BUFFER_SIZE is 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 cmake build 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 plain int return path in AmAudio::get()).


Generated by Claude Code

Copilot AI lite review requested due to automatic review settings September 12, 2026 03:23

hecko commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

deb_test_build is failing here, but not because of this change.

It dies in Dockerfile-debian11 at the very first RUN apt update, before any source is compiled:

E: Release file for http://deb.debian.org/debian-security/dists/bullseye-security/InRelease
   is expired (invalid since 4d 6h 8min 32s)
ERROR: failed to solve: process "/bin/sh -c apt update" did not complete successfully: exit code: 100

Debian 11 (bullseye) reached end of security support, so its InRelease file is no longer being refreshed and apt now rejects it as expired. The same job is already red on master at e5e7e61 for the same reason (run 34616001093), and this PR touches only core/AmAudio.cpp and core/AmAudioFile.cpp.

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 -o Acquire::Check-Valid-Until=false in that Dockerfile), which belongs in its own PR rather than here.

The compile itself is fine: a full cmake build of core plus all apps is clean with no new warnings, and the remaining checks (hardened/asan/ubsan/tsan, FreeBSD 13.5/14.3/15.0, macOS, rpm) are running.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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:312Critical (3 votes): validate the scaled sample count before conversion to int and passing it to calcBytesToRead.
  • core/AmAudioFile.cpp:381Critical (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() and resampleOutput() can expand that data before line 338 copies it. In particular, when the input rate is lower than output_sample_rate, the resampler writes/returns more than AUDIO_BUFFER_SIZE without receiving a destination capacity, so samples and 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 to playout_buffer->read() before copying to buffer; the B2B path calls this with the destination leg's rate, so a high-rate source/low-rate output can still write past the samples DblBuffer. 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 what decode() writes into the back half of samples. For example, the PCMU/PCMA decoders write size * 2 PCM bytes, so an input size of exactly 8192 passes this check but AmAudio::decode() writes 16384 bytes into an 8192-byte half-buffer. The limit must account for decoder expansion before calling decode().
  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.

Comment thread core/AmAudio.cpp
Comment thread core/AmAudioFile.cpp
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.
@hecko
hecko force-pushed the hecko/amaudio-read-size-guards branch from c78e593 to 5358db7 Compare September 12, 2026 03:39
@hecko
hecko merged commit e0ca53e into master Sep 13, 2026
24 of 26 checks passed
@hecko
hecko deleted the hecko/amaudio-read-size-guards branch September 13, 2026 09:32
hecko added a commit that referenced this pull request Sep 13, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants