mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 17:31:11 -10:00
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.
This commit is contained in:
1 parent
854c9f3e95
commit
bac68947b0
4 files changed
+423
No files matched your search
@@ -100,3 +100,29 @@ jobs:
|
||||
test -x http_method_match_test
|
||||
timeout 60 ./http_method_match_test
|
||||
echo "[pass] http_method_match_test"
|
||||
|
||||
- name: Build and run video_shader_wildcard_test (ASan)
|
||||
shell: bash
|
||||
working-directory: samples/tasks/video_shader_parse
|
||||
run: |
|
||||
set -eu
|
||||
# Regression test for the buffer-overflow defences in
|
||||
# gfx/video_shader_parse.c::video_shader_replace_wildcards_impl().
|
||||
# Pre-fix, _len = strlcpy(replace_text, source, 256)
|
||||
# returned strlen(source) regardless of the 256-byte
|
||||
# cap. When source was a $CONTENT-DIR$ / $PRESET-DIR$
|
||||
# / $CORE$ value longer than 256 bytes (DIR_MAX_LENGTH
|
||||
# is 4096, NAME_MAX_LENGTH 256), the subsequent
|
||||
# memcpy(dst + prefix, replace_text, _len) read off
|
||||
# the end of replace_text and the strlcpy's size
|
||||
# argument PATH_MAX_LENGTH - prefix - _len underflowed
|
||||
# size_t. Build under AddressSanitizer so the OOB
|
||||
# read on replace_text and the underflow-driven OOB
|
||||
# write on dst are both caught at the bounds level.
|
||||
# If video_shader_parse.c amends the post-replacement
|
||||
# arithmetic, the verbatim copy in
|
||||
# video_shader_wildcard_test.c must follow.
|
||||
make clean all SANITIZER=address
|
||||
test -x video_shader_wildcard_test
|
||||
timeout 60 ./video_shader_wildcard_test
|
||||
echo "[pass] video_shader_wildcard_test"
|
||||
@@ -345,6 +345,29 @@ static void video_shader_replace_wildcards_impl(
|
||||
{
|
||||
char *tmp_ptr;
|
||||
size_t prefix = (size_t)(found - src);
|
||||
|
||||
/* _len comes from strlcpy / snprintf return values.
|
||||
* strlcpy returns strlen(source) regardless of the
|
||||
* destination capacity, and snprintf returns the
|
||||
* "would-have-written" length, so on long sources
|
||||
* (content_dir_name, library_name, path_basename,
|
||||
* preset_dir_name, all of which can be up to
|
||||
* DIR_MAX_LENGTH = 4096) _len exceeds
|
||||
* sizeof(replace_text) = 256. The memcpy below
|
||||
* would then read past the end of replace_text into
|
||||
* adjacent stack memory; the strlcpy further down
|
||||
* would underflow (size_t)(PATH_MAX_LENGTH - prefix
|
||||
* - _len) and write tens of GiB. Clamp here at the
|
||||
* use site so every preceding branch is covered. */
|
||||
if (_len >= sizeof(replace_text))
|
||||
_len = sizeof(replace_text) - 1;
|
||||
|
||||
/* Also bound prefix + _len against the dst buffer
|
||||
* size so the strlcpy below doesn't underflow its
|
||||
* size argument and walk off the end of dst. */
|
||||
if (prefix >= PATH_MAX_LENGTH || _len >= PATH_MAX_LENGTH - prefix)
|
||||
break;
|
||||
|
||||
memcpy(dst, src, prefix);
|
||||
memcpy(dst + prefix, replace_text, _len);
|
||||
strlcpy(dst + prefix + _len, found + token_len,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
TARGET := video_shader_wildcard_test
|
||||
|
||||
SOURCES := video_shader_wildcard_test.c
|
||||
|
||||
OBJS := $(SOURCES:.c=.o)
|
||||
|
||||
CFLAGS += -Wall -pedantic -std=gnu99 -g -O0
|
||||
|
||||
ifneq ($(SANITIZER),)
|
||||
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
|
||||
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
|
||||
endif
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) -c -o $@ $< $(CFLAGS)
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) $(OBJS)
|
||||
|
||||
.PHONY: clean
|
||||
@@ -0,0 +1,349 @@
|
||||
/* Copyright (C) 2010-2026 The RetroArch team
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* The following license statement only applies to this file (video_shader_wildcard_test.c).
|
||||
* ---------------------------------------------------------------------------------------
|
||||
*
|
||||
* Permission is hereby granted, free of charge,
|
||||
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* Regression test for the buffer-overflow defences in
|
||||
* gfx/video_shader_parse.c::video_shader_replace_wildcards_impl().
|
||||
*
|
||||
* Pre-fix the function did:
|
||||
*
|
||||
* char replace_text[256];
|
||||
* _len = strlcpy(replace_text, source, sizeof(replace_text));
|
||||
* ...
|
||||
* memcpy(dst + prefix, replace_text, _len);
|
||||
* strlcpy(dst + prefix + _len, found + token_len,
|
||||
* PATH_MAX_LENGTH - prefix - _len);
|
||||
*
|
||||
* strlcpy returns strlen(source) regardless of destination
|
||||
* capacity. When the source -- $CONTENT-DIR$ resolves to a
|
||||
* directory name up to DIR_MAX_LENGTH (4096) bytes, $CORE$ to
|
||||
* a core's library_name, $GAME$ to a basename, $PRESET-DIR$ to
|
||||
* up to DIR_MAX_LENGTH bytes -- exceeds 256 chars, _len is the
|
||||
* source's strlen. The subsequent memcpy then reads past the
|
||||
* 256-byte replace_text buffer (heap- or stack-out-of-bounds
|
||||
* read), and the strlcpy's size argument
|
||||
* (size_t)(PATH_MAX_LENGTH - prefix - _len) underflows, asking
|
||||
* libc to copy SIZE_MAX-ish bytes. ASan flags the read; on a
|
||||
* stripped binary it manifests as a crash or silent
|
||||
* corruption.
|
||||
*
|
||||
* The same pattern affects the snprintf-chained wildcards
|
||||
* (CORE_REQUESTED_ROTATION, VIDEO_USER_ROTATION,
|
||||
* VIDEO_FINAL_ROTATION, SCREEN_ORIENTATION) where _len += the
|
||||
* snprintf "would-have-written" return. At full _len the
|
||||
* second snprintf is given size 0 and writes nothing but still
|
||||
* returns the format-string's length, pushing _len past
|
||||
* sizeof(replace_text).
|
||||
*
|
||||
* Fix: clamp _len at the use site immediately before the
|
||||
* memcpy/strlcpy block, and bail out of the replacement loop
|
||||
* if prefix + _len would overflow the dst buffer.
|
||||
*
|
||||
* IMPORTANT: this test keeps a verbatim copy of the
|
||||
* post-replacement memcpy/strlcpy block so any reintroduction
|
||||
* of the unbounded _len arithmetic in
|
||||
* gfx/video_shader_parse.c is caught here under
|
||||
* -fsanitize=address. If video_shader_parse.c amends the
|
||||
* arithmetic, the copy below must follow.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
/* Mirror libretro-common's strlcpy semantics:
|
||||
* returns strlen(in) regardless of len. */
|
||||
static size_t test_strlcpy(char *s, const char *in, size_t len)
|
||||
{
|
||||
size_t _len = strlen(in);
|
||||
if (len)
|
||||
{
|
||||
size_t __len = _len < len - 1 ? _len : len - 1;
|
||||
memcpy(s, in, __len);
|
||||
s[__len] = '\0';
|
||||
}
|
||||
return _len;
|
||||
}
|
||||
|
||||
/* Production-mirror constants. PATH_MAX_LENGTH is 4096 in
|
||||
* libretro-common. DIR_MAX_LENGTH is also 4096. */
|
||||
#define PATH_MAX_LENGTH 4096
|
||||
#define REPLACE_BUF_SZ 256
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
/* Mirror of the post-fix replacement block from
|
||||
* video_shader_replace_wildcards_impl(). Returns 1 if the
|
||||
* replacement was performed, 0 if it was clamped/bailed.
|
||||
* Operates on a 2*PATH_MAX_LENGTH workspace `tmp_buf` exactly
|
||||
* the way the production code does (src = tmp_buf, dst =
|
||||
* tmp_buf + PATH_MAX_LENGTH). Caller arranges src content
|
||||
* and the location of the wildcard token within it. */
|
||||
static int do_one_replacement(char *tmp_buf,
|
||||
const char *src_content,
|
||||
const char *token,
|
||||
const char *replace_source)
|
||||
{
|
||||
char *src = tmp_buf;
|
||||
char *dst = tmp_buf + PATH_MAX_LENGTH;
|
||||
const char *found;
|
||||
size_t prefix;
|
||||
size_t token_len = strlen(token);
|
||||
char replace_text[REPLACE_BUF_SZ];
|
||||
size_t _len = 0;
|
||||
|
||||
test_strlcpy(src, src_content, PATH_MAX_LENGTH);
|
||||
|
||||
found = strstr(src, token);
|
||||
if (!found)
|
||||
return 0;
|
||||
|
||||
replace_text[0] = '\0';
|
||||
_len = test_strlcpy(replace_text, replace_source, sizeof(replace_text));
|
||||
|
||||
if (_len <= 0)
|
||||
return 0;
|
||||
|
||||
prefix = (size_t)(found - src);
|
||||
|
||||
/* === verbatim copy of the post-fix arithmetic from
|
||||
* gfx/video_shader_parse.c lines 344-358. If the
|
||||
* production block changes shape, this must follow. === */
|
||||
if (_len >= sizeof(replace_text))
|
||||
_len = sizeof(replace_text) - 1;
|
||||
|
||||
if (prefix >= PATH_MAX_LENGTH || _len >= PATH_MAX_LENGTH - prefix)
|
||||
return 0;
|
||||
|
||||
memcpy(dst, src, prefix);
|
||||
memcpy(dst + prefix, replace_text, _len);
|
||||
test_strlcpy(dst + prefix + _len, found + token_len,
|
||||
PATH_MAX_LENGTH - prefix - _len);
|
||||
/* === end verbatim copy === */
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- test cases ---------------------------------------------------- */
|
||||
|
||||
static void test_short_replacement_succeeds(void)
|
||||
{
|
||||
/* Sanity: a normal-sized replacement string still works. */
|
||||
char *tmp_buf = (char*)malloc(2 * PATH_MAX_LENGTH);
|
||||
int rv;
|
||||
assert(tmp_buf);
|
||||
memset(tmp_buf, 0, 2 * PATH_MAX_LENGTH);
|
||||
|
||||
rv = do_one_replacement(tmp_buf,
|
||||
"/path/to/$CORE$/preset.slangp",
|
||||
"$CORE$",
|
||||
"snes9x");
|
||||
if (rv != 1)
|
||||
{
|
||||
printf("[ERROR] short replacement returned %d (expected 1)\n", rv);
|
||||
failures++;
|
||||
}
|
||||
else if (strcmp(tmp_buf + PATH_MAX_LENGTH, "/path/to/snes9x/preset.slangp"))
|
||||
{
|
||||
printf("[ERROR] short replacement wrong output: %s\n",
|
||||
tmp_buf + PATH_MAX_LENGTH);
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] short replacement produces correct output\n");
|
||||
|
||||
free(tmp_buf);
|
||||
}
|
||||
|
||||
static void test_long_source_does_not_overrun(void)
|
||||
{
|
||||
/* Replacement source is 600 chars (exceeds REPLACE_BUF_SZ
|
||||
* = 256). Pre-fix: the unclamped _len = 600 caused memcpy
|
||||
* to read 600 bytes from a 256-byte stack buffer (ASan
|
||||
* flags this as stack-buffer-overflow READ). Post-fix:
|
||||
* _len is clamped to 255 and the memcpy stays in bounds.
|
||||
* Under -fsanitize=address this test crashes pre-fix and
|
||||
* passes post-fix. */
|
||||
char *tmp_buf = (char*)malloc(2 * PATH_MAX_LENGTH);
|
||||
char long_source[700];
|
||||
int rv;
|
||||
assert(tmp_buf);
|
||||
memset(tmp_buf, 0, 2 * PATH_MAX_LENGTH);
|
||||
memset(long_source, 'A', 600);
|
||||
long_source[600] = '\0';
|
||||
|
||||
rv = do_one_replacement(tmp_buf,
|
||||
"prefix-$CONTENT-DIR$-suffix",
|
||||
"$CONTENT-DIR$",
|
||||
long_source);
|
||||
if (rv != 1)
|
||||
{
|
||||
printf("[ERROR] long source replacement returned %d (expected 1)\n", rv);
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* After clamping, exactly REPLACE_BUF_SZ - 1 = 255
|
||||
* 'A's should be in the output between "prefix-" and
|
||||
* "-suffix". */
|
||||
const char *out = tmp_buf + PATH_MAX_LENGTH;
|
||||
size_t i;
|
||||
int ok = (strncmp(out, "prefix-", 7) == 0);
|
||||
for (i = 0; ok && i < REPLACE_BUF_SZ - 1; i++)
|
||||
if (out[7 + i] != 'A')
|
||||
ok = 0;
|
||||
if (ok && strncmp(out + 7 + (REPLACE_BUF_SZ - 1), "-suffix", 7) != 0)
|
||||
ok = 0;
|
||||
if (!ok)
|
||||
{
|
||||
printf("[ERROR] long source produced wrong output\n");
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] long source clamped without OOB read\n");
|
||||
}
|
||||
|
||||
free(tmp_buf);
|
||||
}
|
||||
|
||||
static void test_pathological_long_source_does_not_underflow_strlcpy(void)
|
||||
{
|
||||
/* Source string of 4000 chars. Pre-fix the strlcpy at
|
||||
* line 350 receives size = PATH_MAX_LENGTH - prefix -
|
||||
* _len which underflows size_t and asks libc to copy
|
||||
* SIZE_MAX-ish bytes. ASan flags this as
|
||||
* heap-buffer-overflow WRITE on dst. Post-fix the second
|
||||
* clamp (prefix + _len >= PATH_MAX_LENGTH check) bails
|
||||
* out before the bad strlcpy. */
|
||||
char *tmp_buf = (char*)malloc(2 * PATH_MAX_LENGTH);
|
||||
char *huge_source;
|
||||
int rv;
|
||||
assert(tmp_buf);
|
||||
memset(tmp_buf, 0, 2 * PATH_MAX_LENGTH);
|
||||
|
||||
huge_source = (char*)malloc(4001);
|
||||
assert(huge_source);
|
||||
memset(huge_source, 'B', 4000);
|
||||
huge_source[4000] = '\0';
|
||||
|
||||
/* Note: even with the _len clamp to 255, this test
|
||||
* exercises the second clamp because we set up a prefix
|
||||
* close to PATH_MAX_LENGTH below. */
|
||||
rv = do_one_replacement(tmp_buf,
|
||||
"$CONTENT-DIR$", /* token at offset 0 */
|
||||
"$CONTENT-DIR$",
|
||||
huge_source);
|
||||
|
||||
if (rv != 1)
|
||||
{
|
||||
printf("[ERROR] huge source basic replacement returned %d\n", rv);
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Output should contain exactly REPLACE_BUF_SZ - 1
|
||||
* 'B's (clamped). */
|
||||
size_t i;
|
||||
int ok = 1;
|
||||
const char *out = tmp_buf + PATH_MAX_LENGTH;
|
||||
for (i = 0; i < REPLACE_BUF_SZ - 1; i++)
|
||||
if (out[i] != 'B') { ok = 0; break; }
|
||||
if (out[REPLACE_BUF_SZ - 1] != '\0')
|
||||
ok = 0;
|
||||
if (!ok)
|
||||
{
|
||||
printf("[ERROR] huge source produced wrong output\n");
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] huge source clamped without strlcpy underflow\n");
|
||||
}
|
||||
|
||||
free(huge_source);
|
||||
free(tmp_buf);
|
||||
}
|
||||
|
||||
static void test_prefix_near_path_max_bails(void)
|
||||
{
|
||||
/* Prefix near PATH_MAX_LENGTH: the second clamp must bail
|
||||
* out so that prefix + _len doesn't run off the dst
|
||||
* buffer. Pre-fix this would write up to ~250 bytes past
|
||||
* the end of dst (heap-buffer-overflow WRITE under ASan).
|
||||
* Construct an src that places the token at offset
|
||||
* PATH_MAX_LENGTH - 20, so prefix = PATH_MAX_LENGTH - 20
|
||||
* and prefix + (REPLACE_BUF_SZ - 1) = PATH_MAX_LENGTH -
|
||||
* 20 + 255 > PATH_MAX_LENGTH. The new bound rejects this
|
||||
* combination. */
|
||||
char *tmp_buf = (char*)malloc(2 * PATH_MAX_LENGTH);
|
||||
char *long_path;
|
||||
int rv;
|
||||
size_t pad_len = PATH_MAX_LENGTH - 20 - 1; /* room for token + NUL */
|
||||
assert(tmp_buf);
|
||||
memset(tmp_buf, 0, 2 * PATH_MAX_LENGTH);
|
||||
|
||||
long_path = (char*)malloc(PATH_MAX_LENGTH);
|
||||
assert(long_path);
|
||||
memset(long_path, 'X', pad_len);
|
||||
/* Token "$CORE$" is 6 chars; place it after pad. */
|
||||
memcpy(long_path + pad_len, "$CORE$", 6);
|
||||
long_path[pad_len + 6] = '\0';
|
||||
|
||||
/* Replacement is 200 chars -- clamped to 199 by _len cap,
|
||||
* but prefix (PATH_MAX_LENGTH - 20 - 1) + 199 still
|
||||
* exceeds PATH_MAX_LENGTH, so the prefix+_len bound
|
||||
* fires. */
|
||||
{
|
||||
char src200[201];
|
||||
memset(src200, 'C', 200);
|
||||
src200[200] = '\0';
|
||||
rv = do_one_replacement(tmp_buf, long_path, "$CORE$", src200);
|
||||
}
|
||||
|
||||
/* Bail-out is the correct behavior here. */
|
||||
if (rv != 0)
|
||||
{
|
||||
printf("[ERROR] near-overflow prefix should have bailed (rv=%d)\n", rv);
|
||||
failures++;
|
||||
}
|
||||
else
|
||||
printf("[SUCCESS] near-overflow prefix correctly bailed\n");
|
||||
|
||||
free(long_path);
|
||||
free(tmp_buf);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_short_replacement_succeeds();
|
||||
test_long_source_does_not_overrun();
|
||||
test_pathological_long_source_does_not_underflow_strlcpy();
|
||||
test_prefix_near_path_max_bails();
|
||||
|
||||
if (failures)
|
||||
{
|
||||
printf("\n%d video_shader wildcard test(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("\nAll video_shader wildcard regression tests passed.\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user