Skip to content

fix: 2 nullderef / size_t-wrap bugs found by static analysis#217

Open
blackden wants to merge 2 commits into
wolfetplayer:mainfrom
blackden:fix/com-stringcontains-snd-mix-nullderef
Open

fix: 2 nullderef / size_t-wrap bugs found by static analysis#217
blackden wants to merge 2 commits into
wolfetplayer:mainfrom
blackden:fix/com-stringcontains-snd-mix-nullderef

Conversation

@blackden

@blackden blackden commented Jun 6, 2026

Copy link
Copy Markdown

Summary

Two narrow, well-isolated bugs surfaced by cppcheck + clang scan-build while auditing the engine. Both pre-date RealRTCW (inherited from id Tech 3 / RTCW SDK), affect all platforms, and are reproducible on x86_64 and arm64.

Commit 1 — qcommon: guard Com_StringContains against str2 longer than str1

Com_StringContains computes len = strlen(str1) - strlen(str2) as int. On LP64 the subtraction is done in size_t; when str2 is longer than str1 the result wraps to a huge positive number, which is then truncated to a large positive int. The function then runs for (i = 0; i <= len; i++) reading str1[i + j] far past the buffer end.

Fix: compute as size_t, early-return NULL when str2 is longer.

Detected as: scan-build reported 5 downstream OOB-read warnings in Com_Filter (which calls Com_StringContains); after the fix → 2 unrelated warnings remain.

Commit 2 — client: wrap chunk->next NULL to soundData in SDL mixer paint paths

In S_PaintChannelFrom16_scalar, SetVoiceAmplitudeFrom16, and S_PaintChannelFromMuLaw, when the mix advances past the end of the current sndBuffer chunk, the code does chunk = chunk->next; samples = chunk->sndChunk; unconditionally. For non-looping sounds at the very end, chunk->next is NULL — null-deref.

The doppler-shift path (just below in the same file) already handles this with if (!chunk) chunk = sc->soundData;. Mirror that pattern in the three non-doppler sites.

Detected as: scan-build reported 3 nullderef warnings on these exact lines; after the fix → 0.

Test plan

  • Local build passes with make ARCH=arm64 USE_INTERNAL_LIBS=0
  • cppcheck clean on touched files
  • scan-build: 5→2 warnings around Com_Filter, 3→0 in snd_mix.c
  • ASAN+UBSAN smoke test (-fsanitize=address,undefined): playthrough of campaign level 1 with \find <very-long-string> from console + 60s of legacy-mixer sound (s_useOpenAL 0; snd_restart) — no new sanitizer hits in either modified file
  • Manual: \find ... over a long needle returns cleanly, no hang
  • Manual: legacy SDL mixer plays footsteps/voice/gunfire without artefacts or crashes

Notes

These are upstream-clean fixes — they touch only code/qcommon/common.c and code/client/snd_mix.c, no platform-specific code. ioquake3 and other Q3-family forks carry identical code and would benefit from the same patches.

🤖 Generated with Claude Code

blackden added 2 commits June 6, 2026 15:51
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.
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).
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.

1 participant