AudioModule: validate codec2 header and bound the RX decode reads (#11036)

* AudioModule: validate codec2 header and bound the RX decode reads

Two issues reachable from a crafted AUDIO_APP payload:

The RX path built a temp codec2 from rx_encode_frame[3] whenever the frame header
did not match ours. codec2_create returns NULL for an invalid mode byte, and the
next call dereferenced it. Only decode frames that carry our own header (magic +
mode) and drop the rest, so the untrusted mode byte never reaches codec2_create.

The decode loop advanced by the frame size while testing only i < rx_encode_frame_index,
so a payload length that was not a multiple of the frame size read past the received
data and could read past rx_encode_frame. Bound each read to i + frameSize <= the
received length clamped to the buffer, and clamp the receive memcpy to the buffer.

Behavior change: audio frames whose codec2 mode differs from this node's configured
mode are dropped instead of decoded with a temporary codec.

* AudioModule: pass byte count (not sample count) to i2s_write

* AudioModule: fix the mirror sample/byte bug on the i2s_read capture path
This commit is contained in:
Thomas Göttgens
2026-07-16 19:18:31 -05:00
committed by GitHub
co-authored by GitHub
parent 166c808cdf
commit bdf92e1183
+25 -20
View File
@@ -68,23 +68,23 @@ void run_codec2(void *parameter)
}
if (audioModule->radio_state == RadioState::rx) {
size_t bytesOut = 0;
if (memcmp(audioModule->rx_encode_frame, &audioModule->tx_header, sizeof(audioModule->tx_header)) == 0) {
for (int i = 4; i < audioModule->rx_encode_frame_index; i += audioModule->encode_codec_size) {
// Reject frames not carrying our own header (magic + mode); an invalid mode byte
// would else NULL-deref via codec2_create. The bound keeps reads inside the buffer.
const int headerLen = sizeof(audioModule->tx_header);
const int frameSize = audioModule->encode_codec_size;
int limit = audioModule->rx_encode_frame_index;
if (limit > (int)sizeof(audioModule->rx_encode_frame))
limit = sizeof(audioModule->rx_encode_frame);
if (frameSize > 0 && limit >= headerLen &&
memcmp(audioModule->rx_encode_frame, &audioModule->tx_header, headerLen) == 0) {
for (int i = headerLen; i + frameSize <= limit; i += frameSize) {
codec2_decode(audioModule->codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i);
i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size, &bytesOut,
pdMS_TO_TICKS(500));
// adc_buffer_size is a sample count; i2s_write wants bytes (int16_t samples).
i2s_write(I2S_PORT, &audioModule->output_buffer, audioModule->adc_buffer_size * sizeof(int16_t),
&bytesOut, pdMS_TO_TICKS(500));
}
} else {
// if the buffer header does not match our own codec, make a temp decoding setup.
CODEC2 *tmp_codec2 = codec2_create(audioModule->rx_encode_frame[3]);
codec2_set_lpc_post_filter(tmp_codec2, 1, 0, 0.8, 0.2);
int tmp_encode_codec_size = (codec2_bits_per_frame(tmp_codec2) + 7) / 8;
int tmp_adc_buffer_size = codec2_samples_per_frame(tmp_codec2);
for (int i = 4; i < audioModule->rx_encode_frame_index; i += tmp_encode_codec_size) {
codec2_decode(tmp_codec2, audioModule->output_buffer, audioModule->rx_encode_frame + i);
i2s_write(I2S_PORT, &audioModule->output_buffer, tmp_adc_buffer_size, &bytesOut, pdMS_TO_TICKS(500));
}
codec2_destroy(tmp_codec2);
LOG_WARN("Audio: dropping frame with mismatched or short codec2 header");
}
}
}
@@ -213,16 +213,18 @@ int32_t AudioModule::runOnce()
}
}
if (radio_state == RadioState::tx) {
// Get I2S data from the microphone and place in data buffer
// Get I2S data from the microphone and place in data buffer. adc_buffer_size is a
// sample count; i2s_read and adc_buffer_index are in bytes, so scale by int16_t.
size_t bytesIn = 0;
res = i2s_read(I2S_PORT, adc_buffer + adc_buffer_index, adc_buffer_size - adc_buffer_index, &bytesIn,
const size_t frameBytes = adc_buffer_size * sizeof(uint16_t);
res = i2s_read(I2S_PORT, (uint8_t *)adc_buffer + adc_buffer_index, frameBytes - adc_buffer_index, &bytesIn,
pdMS_TO_TICKS(40)); // wait 40ms for audio to arrive.
if (res == ESP_OK) {
adc_buffer_index += bytesIn;
if (adc_buffer_index == adc_buffer_size) {
if (adc_buffer_index >= frameBytes) {
adc_buffer_index = 0;
memcpy((void *)speech, (void *)adc_buffer, 2 * adc_buffer_size);
memcpy((void *)speech, (void *)adc_buffer, frameBytes);
// Notify run_codec2 task that the buffer is ready.
radio_state = RadioState::tx;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
@@ -273,9 +275,12 @@ ProcessMessage AudioModule::handleReceived(const meshtastic_MeshPacket &mp)
if ((moduleConfig.audio.codec2_enabled) && (myRegion->profile->audioPermitted)) {
auto &p = mp.decoded;
if (!isFromUs(&mp)) {
memcpy(rx_encode_frame, p.payload.bytes, p.payload.size);
size_t n = p.payload.size;
if (n > sizeof(rx_encode_frame)) // bound a crafted payload to the RX buffer
n = sizeof(rx_encode_frame);
memcpy(rx_encode_frame, p.payload.bytes, n);
radio_state = RadioState::rx;
rx_encode_frame_index = p.payload.size;
rx_encode_frame_index = n;
// Notify run_codec2 task that the buffer is ready.
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(codec2HandlerTask, &xHigherPriorityTaskWoken);