From 06f8229f38e4eb5e33f998fdad0935723535f509 Mon Sep 17 00:00:00 2001 From: ragnar Date: Sat, 6 Jun 2026 02:29:37 +0300 Subject: [PATCH 1/2] fix(qcommon): guard Com_StringContains against str2 longer than str1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Com_StringContains computed: int len = strlen( str1 ) - strlen( str2 ); When str2 is longer than str1 (a normal lookup case — searching for a long pattern in a short cvar / filename / map name), the size_t subtraction underflows to a huge unsigned value before the implicit truncation to int. On LP64 (Apple Silicon) the truncated value is indeterminate and the loop `i <= len` runs with a garbage bound, reading str1[j] past the end of the string and feeding garbage into the equality comparison. The substring search semantically can't match when the needle is longer than the haystack, so add an explicit early-return guard before the subtraction. Both lengths are now also cached so strlen isn't called twice. Clang's static analyzer flagged five downstream sites in this and Com_Filter (common.c:632/636/658/726/730 - core.uninitialized.Assign / Branch / UndefinedBinaryOperatorResult / CallAndMessage). All five stem from this one root cause. The bug is inherited from upstream ioquake3. --- code/qcommon/common.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/code/qcommon/common.c b/code/qcommon/common.c index 04f0e03f0..c341a1eea 100644 --- a/code/qcommon/common.c +++ b/code/qcommon/common.c @@ -624,8 +624,18 @@ Com_StringContains */ char *Com_StringContains( char *str1, char *str2, int casesensitive ) { int len, i, j; - - len = strlen( str1 ) - strlen( str2 ); + size_t l1, l2; + + l1 = strlen( str1 ); + l2 = strlen( str2 ); + if ( l2 > l1 ) { + // substring is longer than haystack; the loop below would + // not execute, but the unsigned subtraction would underflow + // before being truncated into int and produce indeterminate + // loop bounds. Bail explicitly. + return NULL; + } + len = (int)( l1 - l2 ); for ( i = 0; i <= len; i++, str1++ ) { for ( j = 0; str2[j]; j++ ) { if ( casesensitive ) { From 883dd7166e02c7564f193bede8e57b10669518e6 Mon Sep 17 00:00:00 2001 From: ragnar Date: Sat, 6 Jun 2026 02:30:37 +0300 Subject: [PATCH 2/2] fix(client): wrap chunk->next NULL to soundData in SDL mixer paint paths Three non-doppler chunk-advance sites in snd_mix.c read the next chunk and immediately dereferenced it: chunk = chunk->next; samples = chunk->sndChunk; sndBuffer->next is explicitly NULL-terminated at allocation time (snd_mem.c:73), so on end-of-sound boundary crossing the second line dereferences a null pointer. The doppler variants in the same file already guard with the engine's established wrap-on-exhaustion idiom: chunk = chunk->next; if ( !chunk ) { chunk = sc->soundData; } Apply the same idiom to the non-doppler paths: - S_SetVoiceAmplitudeFrom16 (snd_mix.c:286) - S_PaintChannelFrom16_scalar (snd_mix.c:530, non-doppler branch) - S_PaintChannelFromMuLaw (snd_mix.c:705, non-doppler branch) This is a SDL/legacy mixer-only path; the default macOS audio backend is OpenAL (snd_openal.c) where this code does not run. Bug inherited from upstream ioquake3. Found by clang static analyzer (core.NullDereference at snd_mix.c:290, 521, 700). --- code/client/snd_mix.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/code/client/snd_mix.c b/code/client/snd_mix.c index 7dbcfda33..cd8ea49c9 100644 --- a/code/client/snd_mix.c +++ b/code/client/snd_mix.c @@ -284,6 +284,9 @@ void S_SetVoiceAmplitudeFrom16( const sfx_t *sc, int sampleOffset, int count, in for ( i = 0; i < count; i++ ) { if ( sampleOffset >= SND_CHUNK_SIZE ) { chunk = chunk->next; + if ( !chunk ) { + chunk = sc->soundData; + } samples = chunk->sndChunk; sampleOffset = 0; } @@ -528,6 +531,9 @@ static void S_PaintChannelFrom16_scalar( channel_t *ch, const sfx_t *sc, int cou if (sampleOffset == SND_CHUNK_SIZE) { chunk = chunk->next; + if ( !chunk ) { + chunk = sc->soundData; + } samples = chunk->sndChunk; sampleOffset = 0; } @@ -703,6 +709,9 @@ void S_PaintChannelFromMuLaw( channel_t *ch, sfx_t *sc, int count, int sampleOff samples++; if (chunk != NULL && samples == (byte *)chunk->sndChunk+(SND_CHUNK_SIZE*2)) { chunk = chunk->next; + if ( !chunk ) { + chunk = sc->soundData; + } samples = (byte *)chunk->sndChunk; } }