Skip to content

Commit 90b96a3

Browse files
committed
fill_pathname() OOB write when in_path length >= dst buffer length
1 parent e36d269 commit 90b96a3

4 files changed

Lines changed: 232 additions & 8 deletions

File tree

.github/workflows/Linux-libretro-common-samples.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ jobs:
4848
archive_zstd_test
4949
config_file_test
5050
path_resolve_realpath_test
51+
fill_pathname_test
5152
nbio_test
5253
rpng
5354
rzip_chunk_size_test

libretro-common/file/file_path.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ size_t fill_pathname(char *s, const char *in_path,
347347
{
348348
char *tok = NULL;
349349
size_t _len = strlcpy(s, in_path, len);
350+
if (_len >= len)
351+
_len = (len > 0) ? len - 1 : 0;
350352
if ((tok = (char*)strrchr(path_basename(s), '.')))
351353
{
352354
*tok = '\0'; _len = tok - s;
Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
TARGET := path_resolve_realpath_test
1+
TARGETS := path_resolve_realpath_test fill_pathname_test
22

33
LIBRETRO_COMM_DIR := ../../..
44

5-
SOURCES := \
6-
path_resolve_realpath_test.c \
5+
COMMON_SOURCES := \
76
$(LIBRETRO_COMM_DIR)/compat/fopen_utf8.c \
87
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
98
$(LIBRETRO_COMM_DIR)/compat/compat_strcasestr.c \
@@ -16,19 +15,19 @@ SOURCES := \
1615
$(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \
1716
$(LIBRETRO_COMM_DIR)/time/rtime.c
1817

19-
OBJS := $(SOURCES:.c=.o)
18+
COMMON_OBJS := $(COMMON_SOURCES:.c=.o)
2019

2120
CFLAGS += -Wall -pedantic -std=gnu99 -g -DRARCH_INTERNAL -I$(LIBRETRO_COMM_DIR)/include
2221

23-
all: $(TARGET)
22+
all: $(TARGETS)
2423

2524
%.o: %.c
2625
$(CC) -c -o $@ $< $(CFLAGS)
2726

28-
$(TARGET): $(OBJS)
27+
$(TARGETS): %: %.o $(COMMON_OBJS)
2928
$(CC) -o $@ $^ $(LDFLAGS)
3029

3130
clean:
32-
rm -f $(TARGET) $(OBJS)
31+
rm -f $(TARGETS) $(addsuffix .o,$(TARGETS)) $(COMMON_OBJS)
3332

34-
.PHONY: clean
33+
.PHONY: all clean
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/* Copyright (C) 2010-2026 The RetroArch team
2+
*
3+
* ---------------------------------------------------------------------------------------
4+
* The following license statement only applies to this file (fill_pathname_test.c).
5+
* ---------------------------------------------------------------------------------------
6+
*
7+
* Permission is hereby granted, free of charge,
8+
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
9+
* to deal in the Software without restriction, including without limitation the rights to
10+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
11+
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
16+
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19+
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
*/
22+
23+
/* Regression for the fill_pathname() out-of-bounds write observed in
24+
* production iOS crash reports as
25+
* __chk_fail_overflow -> __strlcpy_chk -> fill_pathname
26+
* -> database_info_list_iterate_found_match (task_database.c)
27+
*
28+
* fill_pathname() is:
29+
*
30+
* size_t _len = strlcpy(s, in_path, len);
31+
* if ((tok = strrchr(path_basename(s), '.'))) { *tok = '\0'; _len = tok - s; }
32+
* _len += strlcpy(s + _len, replace, len - _len);
33+
*
34+
* strlcpy() returns strlen(in_path), not the number of bytes written.
35+
* If strlen(in_path) >= len AND the truncated copy contains no '.' in
36+
* its basename, the conditional does not fire and _len stays at the
37+
* (large) source length. The second strlcpy then runs as
38+
* strlcpy(s + _len, replace, len - _len)
39+
* with len - _len underflowed to a huge size_t. s + _len is well past
40+
* the end of the destination buffer, and strlcpy happily writes
41+
* strlen(replace) + 1 bytes there.
42+
*
43+
* On Apple platforms strlcpy is a libc symbol covered by FORTIFY, so
44+
* __strlcpy_chk catches the bogus length and aborts (this is the
45+
* SIGTRAP visible in the crash logs). On Linux there is no FORTIFY
46+
* for strlcpy, so we detect the overrun by surrounding the destination
47+
* with sentinel bytes and checking they survive the call.
48+
*/
49+
50+
#include <stdio.h>
51+
#include <stdlib.h>
52+
#include <string.h>
53+
54+
#include <file/file_path.h>
55+
56+
static int failures = 0;
57+
58+
#define DST_LEN 64
59+
#define GUARD_LEN 256
60+
#define SENTINEL 0xCC
61+
62+
/* Long input with no '.' anywhere -- forces fill_pathname() to take
63+
* the "no extension to strip" branch with _len > len. */
64+
static void test_overlong_input_no_dot(void)
65+
{
66+
char *region;
67+
char *dst;
68+
size_t i;
69+
size_t in_len = DST_LEN * 4; /* 256 chars, well past DST_LEN */
70+
char *in_path;
71+
72+
region = (char*)malloc(DST_LEN + GUARD_LEN);
73+
if (!region)
74+
abort();
75+
memset(region, SENTINEL, DST_LEN + GUARD_LEN);
76+
dst = region;
77+
78+
in_path = (char*)malloc(in_len + 1);
79+
if (!in_path)
80+
abort();
81+
for (i = 0; i < in_len; i++)
82+
in_path[i] = 'a';
83+
in_path[in_len] = '\0';
84+
85+
fill_pathname(dst, in_path, ".lpl", DST_LEN);
86+
87+
/* The destination's first DST_LEN bytes are fair game for the
88+
* function to write into. Anything in [DST_LEN, DST_LEN+GUARD_LEN)
89+
* must be untouched. */
90+
for (i = DST_LEN; i < (size_t)(DST_LEN + GUARD_LEN); i++)
91+
{
92+
if ((unsigned char)region[i] != SENTINEL)
93+
{
94+
printf("[FAILED] fill_pathname overran dst: byte %zu changed from 0x%02x to 0x%02x\n",
95+
i, SENTINEL, (unsigned char)region[i]);
96+
failures++;
97+
break;
98+
}
99+
}
100+
if (i == (size_t)(DST_LEN + GUARD_LEN))
101+
printf("[SUCCESS] overlong no-dot input did not overrun destination\n");
102+
103+
free(in_path);
104+
free(region);
105+
}
106+
107+
/* Long input where the truncated copy DOES contain a '.' -- the
108+
* conditional resets _len so this path was never broken, but include
109+
* it as a regression so a future "fix" doesn't silently break the
110+
* normal extension-replacement case. */
111+
static void test_overlong_input_with_dot(void)
112+
{
113+
char dst[DST_LEN];
114+
/* "aaaa.aaaa..." for in_len bytes -- truncation will still leave
115+
* dots inside the destination. */
116+
size_t in_len = DST_LEN * 4;
117+
char *in_path = (char*)malloc(in_len + 1);
118+
size_t i;
119+
120+
if (!in_path)
121+
abort();
122+
for (i = 0; i < in_len; i++)
123+
in_path[i] = (i % 5 == 4) ? '.' : 'a';
124+
in_path[in_len] = '\0';
125+
126+
memset(dst, 0, sizeof(dst));
127+
fill_pathname(dst, in_path, ".lpl", sizeof(dst));
128+
129+
/* Result must be NUL-terminated within the buffer. */
130+
if (memchr(dst, '\0', sizeof(dst)) == NULL)
131+
{
132+
printf("[FAILED] fill_pathname did not NUL-terminate within buffer\n");
133+
failures++;
134+
}
135+
else
136+
printf("[SUCCESS] overlong with-dot input stayed NUL-terminated in buffer\n");
137+
138+
free(in_path);
139+
}
140+
141+
/* Helper: run fill_pathname with a 64-byte buffer and assert the
142+
* resulting string matches @want. */
143+
static void check_spec(const char *label, const char *in_path,
144+
const char *replace, const char *want)
145+
{
146+
char dst[64];
147+
memset(dst, 0, sizeof(dst));
148+
fill_pathname(dst, in_path, replace, sizeof(dst));
149+
if (strcmp(dst, want) != 0)
150+
{
151+
printf("[FAILED] %s: fill_pathname(\"%s\", \"%s\") = \"%s\" (expected \"%s\")\n",
152+
label, in_path, replace, dst, want);
153+
failures++;
154+
return;
155+
}
156+
printf("[SUCCESS] %s\n", label);
157+
}
158+
159+
/* Edge case: input is exactly len-1, no extension, replace appended.
160+
* The first strlcpy returns strlen(in_path) == len-1, no '.' in the
161+
* truncated copy, _len = len-1, so len - _len = 1. The second
162+
* strlcpy gets len=1 and writes only the NUL. Must not overrun. */
163+
static void test_exact_fit_no_extension(void)
164+
{
165+
char *region = (char*)malloc(DST_LEN + GUARD_LEN);
166+
char *dst;
167+
char in_path[DST_LEN];
168+
size_t i;
169+
170+
if (!region)
171+
abort();
172+
memset(region, SENTINEL, DST_LEN + GUARD_LEN);
173+
dst = region;
174+
175+
for (i = 0; i < DST_LEN - 1; i++)
176+
in_path[i] = 'b';
177+
in_path[DST_LEN - 1] = '\0';
178+
179+
fill_pathname(dst, in_path, ".lpl", DST_LEN);
180+
181+
for (i = DST_LEN; i < (size_t)(DST_LEN + GUARD_LEN); i++)
182+
{
183+
if ((unsigned char)region[i] != SENTINEL)
184+
{
185+
printf("[FAILED] fill_pathname overran dst on exact-fit input: byte %zu changed\n", i);
186+
failures++;
187+
break;
188+
}
189+
}
190+
if (i == (size_t)(DST_LEN + GUARD_LEN))
191+
printf("[SUCCESS] exact-fit no-extension input did not overrun destination\n");
192+
193+
free(region);
194+
}
195+
196+
int main(void)
197+
{
198+
/* Documented semantics. */
199+
check_spec("normal extension replacement preserved",
200+
"/foo/bar/baz/boo.c", ".asm", "/foo/bar/baz/boo.asm");
201+
check_spec("no extension -> concatenate replace",
202+
"foo", ".lpl", "foo.lpl");
203+
check_spec("empty replace strips extension",
204+
"foo.c", "", "foo");
205+
check_spec("dot in dirname does not count as extension",
206+
"/foo.bar/baz", ".x", "/foo.bar/baz.x");
207+
check_spec("only the last dot in the basename is stripped",
208+
"foo.bar.baz", ".x", "foo.bar.x");
209+
210+
/* Out-of-bounds behavior. */
211+
test_overlong_input_with_dot();
212+
test_exact_fit_no_extension();
213+
test_overlong_input_no_dot();
214+
215+
if (failures)
216+
{
217+
printf("\n%d fill_pathname test(s) failed\n", failures);
218+
return 1;
219+
}
220+
printf("\nAll fill_pathname regression tests passed.\n");
221+
return 0;
222+
}

0 commit comments

Comments
 (0)