Skip to content

Commit bac6894

Browse files
committed
gfx/video_shader_parse: clamp replace_text length and prefix+_len in wildcard replacement
video_shader_replace_wildcards_impl built each replacement value into a 256-byte stack buffer: char replace_text[256]; _len = strlcpy(replace_text, source, sizeof(replace_text)); then unbounded: memcpy(dst + prefix, replace_text, _len); strlcpy(dst + prefix + _len, found + token_len, PATH_MAX_LENGTH - prefix - _len); Two related primitives were exposed: 1. strlcpy returns strlen(source) regardless of destination capacity. When source resolved to a $CONTENT-DIR$ / $PRESET-DIR$ / $CORE$ / $GAME$ value longer than 256 bytes (DIR_MAX_LENGTH is 4096, library_name and path_basename are platform-dependent and routinely exceed 256), _len was strlen(source). The memcpy then read past the end of the 256-byte replace_text array into adjacent stack memory. 2. The same _len fed (size_t)(PATH_MAX_LENGTH - prefix - _len) on the next line. When prefix + _len exceeded PATH_MAX_LENGTH the subtraction underflowed size_t to a huge value and the strlcpy walked off the end of dst. Even with the _len clamp from (1), a wildcard token placed near the end of the input path (legitimate for an attacker-supplied .slangp/.glslp) lets prefix + REPLACE_BUF_SZ exceed PATH_MAX_LENGTH on its own. The snprintf-chained branches (CORE_REQUESTED_ROTATION, VIDEO_USER_ROTATION, VIDEO_FINAL_ROTATION, SCREEN_ORIENTATION) had a sibling problem: snprintf returns the would-have-written length, so _len += snprintf(...) on a saturated buffer pushes _len past sizeof(replace_text) even when the destination ate the entire size budget. Reachable via shader presets the user opens. These come from the online updater (typically slang-shaders or common-shaders), the user's own preset directory, and from content-bundled presets distributed alongside ROMs. Fix: clamp at the use site so all the upstream branches are covered in one place. if (_len >= sizeof(replace_text)) _len = sizeof(replace_text) - 1; if (prefix >= PATH_MAX_LENGTH || _len >= PATH_MAX_LENGTH - prefix) break; Adds samples/tasks/video_shader_parse/video_shader_wildcard_test as a regression test, registered in .github/workflows/Linux-samples-tasks.yml as an ASan-enabled step. Four cases: - short replacement produces correct output - long source (600 chars) clamped without OOB read - huge source (4000 chars) clamped without strlcpy underflow - prefix near PATH_MAX_LENGTH correctly bails out Test follows the existing samples/tasks pattern (verbatim copy of the post-fix arithmetic block, ASan as the discriminator) used by archive_name_safety_test and http_method_match_test. Verified to fire under ASan when the clamps are removed: stack-buffer-overflow READ of size 600 at offset 288 in replace_text (a 256-byte buffer). With the clamps the test passes clean. Note on test scope: the test exercises a verbatim copy of the post-replacement arithmetic block from video_shader_parse.c lines 344-358, not the full video_shader_replace_wildcards_impl. The full function depends on settings_t / runloop_state / video driver globals that aren't tractable to stub for a unit test -- following the convention used by archive_name_safety_test ("if task_decompress.c amends the predicate, the copy here must follow"), the test's verbatim block must track the production block. A comment in the test points at the production lines explicitly.
1 parent 854c9f3 commit bac6894

4 files changed

Lines changed: 423 additions & 0 deletions

File tree

.github/workflows/Linux-samples-tasks.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,29 @@ jobs:
100100
test -x http_method_match_test
101101
timeout 60 ./http_method_match_test
102102
echo "[pass] http_method_match_test"
103+
104+
- name: Build and run video_shader_wildcard_test (ASan)
105+
shell: bash
106+
working-directory: samples/tasks/video_shader_parse
107+
run: |
108+
set -eu
109+
# Regression test for the buffer-overflow defences in
110+
# gfx/video_shader_parse.c::video_shader_replace_wildcards_impl().
111+
# Pre-fix, _len = strlcpy(replace_text, source, 256)
112+
# returned strlen(source) regardless of the 256-byte
113+
# cap. When source was a $CONTENT-DIR$ / $PRESET-DIR$
114+
# / $CORE$ value longer than 256 bytes (DIR_MAX_LENGTH
115+
# is 4096, NAME_MAX_LENGTH 256), the subsequent
116+
# memcpy(dst + prefix, replace_text, _len) read off
117+
# the end of replace_text and the strlcpy's size
118+
# argument PATH_MAX_LENGTH - prefix - _len underflowed
119+
# size_t. Build under AddressSanitizer so the OOB
120+
# read on replace_text and the underflow-driven OOB
121+
# write on dst are both caught at the bounds level.
122+
# If video_shader_parse.c amends the post-replacement
123+
# arithmetic, the verbatim copy in
124+
# video_shader_wildcard_test.c must follow.
125+
make clean all SANITIZER=address
126+
test -x video_shader_wildcard_test
127+
timeout 60 ./video_shader_wildcard_test
128+
echo "[pass] video_shader_wildcard_test"

gfx/video_shader_parse.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,29 @@ static void video_shader_replace_wildcards_impl(
345345
{
346346
char *tmp_ptr;
347347
size_t prefix = (size_t)(found - src);
348+
349+
/* _len comes from strlcpy / snprintf return values.
350+
* strlcpy returns strlen(source) regardless of the
351+
* destination capacity, and snprintf returns the
352+
* "would-have-written" length, so on long sources
353+
* (content_dir_name, library_name, path_basename,
354+
* preset_dir_name, all of which can be up to
355+
* DIR_MAX_LENGTH = 4096) _len exceeds
356+
* sizeof(replace_text) = 256. The memcpy below
357+
* would then read past the end of replace_text into
358+
* adjacent stack memory; the strlcpy further down
359+
* would underflow (size_t)(PATH_MAX_LENGTH - prefix
360+
* - _len) and write tens of GiB. Clamp here at the
361+
* use site so every preceding branch is covered. */
362+
if (_len >= sizeof(replace_text))
363+
_len = sizeof(replace_text) - 1;
364+
365+
/* Also bound prefix + _len against the dst buffer
366+
* size so the strlcpy below doesn't underflow its
367+
* size argument and walk off the end of dst. */
368+
if (prefix >= PATH_MAX_LENGTH || _len >= PATH_MAX_LENGTH - prefix)
369+
break;
370+
348371
memcpy(dst, src, prefix);
349372
memcpy(dst + prefix, replace_text, _len);
350373
strlcpy(dst + prefix + _len, found + token_len,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
TARGET := video_shader_wildcard_test
2+
3+
SOURCES := video_shader_wildcard_test.c
4+
5+
OBJS := $(SOURCES:.c=.o)
6+
7+
CFLAGS += -Wall -pedantic -std=gnu99 -g -O0
8+
9+
ifneq ($(SANITIZER),)
10+
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
11+
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
12+
endif
13+
14+
all: $(TARGET)
15+
16+
%.o: %.c
17+
$(CC) -c -o $@ $< $(CFLAGS)
18+
19+
$(TARGET): $(OBJS)
20+
$(CC) -o $@ $^ $(LDFLAGS)
21+
22+
clean:
23+
rm -f $(TARGET) $(OBJS)
24+
25+
.PHONY: clean

0 commit comments

Comments
 (0)