The Google Drive backend held a 128-entry static folder-id cache,
each entry a full path buffer - 295 kilobytes of load-resident
state on platforms without demand paging, for a backend most
sessions never configure. The cache now allocates on the first
insert and lookups treat an absent cache as a miss; it lives for
the session once syncing has run, exactly the lifetime the static
had. All eleven touch sites sit in the two cache functions,
audited.
webdav_state_t::cnonce is only ever assigned the string literal
"1a2b3c4f" and read from (strlen, MD5_Update input, strlcpy source);
unlike its sibling digest fields (realm/nonce/algo/opaque) it is never
malloc'd, strdup'd or freed. As a plain char* it triggers, under
CXX_BUILD=1:
warning: ISO C++ forbids converting a string constant to 'char*'
[-Wwrite-strings]
Type it as const char* to match its actual use. All consumers already
take const (MD5_Update's const void*, strlcpy's const char* source,
strlen), it is valid C, and it is not part of the free path, so the C
build and runtime behaviour are unchanged.
Follow-up to the CXX_BUILD fixes in 059ca26 .. 9f1531a.
Five non-loop path-build sequences chained strlcpy with one or two
following strlcat calls. Every strlcat scans its destination from
the start to find the NUL, so an N-call chain pays N strlen scans
on an ever-growing prefix. The mechanical fix is to capture the
strlcpy return value and use strlcpy-at-offset for subsequent
appends — same end result, but each step is O(strlen(src)) instead
of O(strlen(dst)+strlen(src)).
The sites:
configuration.c (input_config_save_keybinds_user)
Three calls to fill_pathname_join_delim built the same
'prefix_base' stem before each suffix variant ('_btn', '_axis',
'_mbtn'). Build the stem ONCE, then overwrite just the suffix
bytes at the known offset via memcpy. Eliminates two redundant
full path builds plus three strlcat scans.
ai/game_ai.c (game_ai_lib_init path)
strlcpy(lib_path) + strlcat('/data/') + strlcat(game_name) +
strlen() truncation check. Replaced with three offset-tracked
strlcpys; truncation detection moves to a comparison of the
accumulated _len against sizeof - 1 (same outcome — bail
before using a truncated path).
network/cloud_sync/smb.c (smb_sync_build_path helper)
The 'add trailing slash if missing' branch used strlen(dest)-1
to peek at the last char; now uses the strlcpy-tracked _len-1.
Two following strlcats become strlcpys at offset.
network/cloud_sync/smb.c (cloud_sync subdir builder)
Same shape, two strlcats off a strlcpy.
libretro-common/vfs/vfs_implementation_smb.c (smb_path_join)
Same shape as smb_sync_build_path — including the strlen(dest)-1
peek that becomes a _len-1 peek.
Correctness verified: 164 cases (4 realistic shapes + 80
truncation boundaries x 2 input shapes) produce byte-identical
output to the prior strlcat-chain form, checked under ASan+UBSan.
Absolute savings per call are modest (each strlcat avoided saves
one O(strlen(dst)) scan, ~3-10 ns for typical path lengths) and
none of these are per-frame paths, so the wall-clock win is small
in any individual call. The value is shape consistency with the
offset-tracking pattern used elsewhere in the codebase, and the
configuration.c change additionally removes two redundant
fill_pathname_join_delim invocations per keybind unset.
Zero new compiler warnings on any of the four files.
Suggested commit message
================================================================
cloud_sync: fix one-byte heap overflow in HTTP failure loggers
The four cloud-sync HTTP failure loggers (webdav, s3 ×2, google_drive)
all wrote a NUL terminator at data->data[data->len] and then printed
data->data with %s. The buffer in data->data is owned by net_http
and is sized exactly to data->len: net_http_update() shrinks the
response buffer with realloc(response->data, response->len) on the
P_DONE transition (net_http.c lines 1441, 1501, 1540), and
net_http_data() returns that pointer plus the same length to the
task layer. The terminator write therefore overflows the heap chunk
by one byte and corrupts the next chunk's metadata.
The corruption is silent at the point of the write. Glibc detects
it later, on an unrelated free() near the same arena -- in practice
the next task_http_transfer_cleanup() during startup, when cloud
sync issues many HTTP requests in quick succession. The result is
malloc_printerr / abort with a backtrace through
task_http_transfer_cleanup that does not point at cloud_sync at all.
Switch all four sites from "NUL-and-print-with-%s" to the
length-bounded form RARCH_WARN("%.*s\n", (int)data->len, ...).
This is local to the broken code, requires no producer-side change,
and removes the implicit "consumer must NUL-terminate, allocator
must oversize" handshake that nothing in the codebase documents.
Other consumers of data->data already pass (data->data, data->len)
together and are unaffected.
Reproducer (ASan, distilled from the call chain above):
char *p = realloc(malloc(64*1024), 57); /* mimic net_http */
memcpy(p, body, 57);
p[57] = 0; /* OOB write */
==116==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1 at 0x506000000059 thread T0
#0 webdav_log_http_failure
0 bytes after 57-byte region
Fixes the abort path in #16834, #17474, #17731 (and likely several
other "[CloudSync] crash on startup" reports that share the
task_http_transfer_cleanup → malloc_printerr → abort signature).
================================================================
PR description (for github.com/libretro/RetroArch)
================================================================
Cloud sync triggers a one-byte heap-buffer-overflow on every non-2xx
HTTP response, in the failure logging path shared by webdav, s3, and
google_drive. Glibc usually catches the resulting heap corruption a
few allocations later, on an unrelated `free()`, with a backtrace
that points at `task_http_transfer_cleanup` and gives no hint that
cloud sync is the source.
```
```
Reproduces reliably at startup with cloud sync enabled; goes away
with cloud sync disabled. The user-visible repro from #17474
("crashes upon fetching any server file other than manifest.server")
is the same bug -- a server returning anything that hits the failure
logger.
`net_http_update()` shrinks the response buffer to **exactly**
`response->len` bytes when the body is fully received:
- `libretro-common/net/net_http.c:1441` `realloc(response->data, response->len)`
- `libretro-common/net/net_http.c:1501` same
- `libretro-common/net/net_http.c:1540` same
`net_http_data()` returns that pointer plus the same length, and
`task_http_transfer_handler` puts both into `data->data` /
`data->len`.
The four cloud-sync failure loggers then do:
```c
data->data[data->len] = 0;
RARCH_WARN("%s\n", data->data);
```
That's a one-byte write past the end of the heap allocation. It
corrupts the malloc chunk header of whatever sits next on the
arena. Glibc only screams when something adjacent gets `free()`d.
At startup with cloud sync, that "something adjacent" is
overwhelmingly `task_http_transfer_cleanup()`'s own
`free(data->data)` or `free(data)`, which is what users see in the
backtrace.
The repeating-substring log spam in #16834
(`"Beetle PCE Fast.opt", PCE Fast/Beetle PCE Fast.opt", config/Beetle..."`)
is the same root cause manifesting as a corrupt-read instead of an
abort -- different chunk geometry, same OOB.
Replace `data->data[data->len] = 0; RARCH_WARN("%s\n", data->data)`
with `RARCH_WARN("%.*s\n", (int)data->len, (const char*)data->data)`
at all four sites:
- `network/cloud_sync/webdav.c` `webdav_log_http_failure`
- `network/cloud_sync/s3.c` `s3_log_http_failure`
- `network/cloud_sync/s3.c` `s3_log_multipart_initiate_failure`
- `network/cloud_sync/google_drive.c` `gdrive_log_http_failure`
The fix is local and does not change the producer-side ownership
contract. All other consumers of `data->data` in the tree already
use `(data->data, data->len)` together and are unaffected.
ASan reproducer of the exact write pattern (pre-patch):
```
==116==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1 at 0x506000000059 thread T0
#0 webdav_log_http_failure
0x506000000059 is located 0 bytes after 57-byte region
allocated by realloc / fake_net_http_response
```
Same harness with the patched logger: clean run, no ASan diagnostic.
Closes#17474. Likely closes#16834, #17731 and other "cloud sync
crash on startup" reports with the same `task_http_transfer_cleanup
→ malloc_printerr → abort` signature; if those reports are actually
the WebDAV "begin failed" / network-down case rather than this one,
they need separate diagnosis but should at least stop crashing.
================================================================
Four bugs found in a sweep of network/ alloc sites.
=== network/cloud_sync/s3.c: s3_url_encode / s3_trim_string ===
Both helpers had the same dead-code NULL guard:
static char* s3_url_encode(const char *input)
{
size_t input_len = strlen(input); /* segfaults on NULL */
size_t output_pos = 0;
char *output = malloc(input_len * 3 + 1);
size_t i;
if (!output)
return NULL;
if (!input) /* dead code */
return NULL;
...
The 'if (!input)' check sat after both strlen(input) and the
malloc sized by strlen's result, so a NULL input crashed in
strlen before we ever reached the guard. s3_trim_string at
line 443 had the identical pattern.
Fix: move the NULL-check to run FIRST, then strlen, then malloc
(also NULL-checked). Dropped the dead post-strlen guard.
These callers feed arbitrary header values / URL path components
through these helpers; a malformed S3 response or a malformed
user-supplied bucket/path was one NULL away from crashing the
cloud-sync subsystem.
=== network/cloud_sync/s3.c: s3_build_auth_header ===
Straight OOM NULL-deref:
auth_header = malloc(1024);
snprintf(auth_header, 1024, ...); /* NULL-deref on OOM */
Also leaked 'signature' (heap-allocated just above by
s3_calculate_signature_v4_with_time) on the crash path - the
free at the bottom of the function was unreachable.
Fix: NULL-check the malloc, free signature + return NULL on OOM.
=== network/cloud_sync/google_drive.c: gdrive_do_patch ===
filestream_seek(cb_st->rfile, 0, SEEK_SET);
len = filestream_get_size(cb_st->rfile);
buf = malloc((size_t)(len + 1)); /* unchecked */
filestream_read(cb_st->rfile, buf, len); /* NULL-deref */
...
task_push_http_transfer_with_content(url, "PATCH",
buf, (size_t)len, ...); /* sends NULL */
Two issues folded together:
- malloc not NULL-checked.
- filestream_get_size returns int64_t; on file-not-readable /
error it returns negative. (size_t)(-1 + 1) == 0, so
malloc(0) is either NULL or a valid zero-sized pointer
depending on libc, and the subsequent filestream_read with
a negative size_t-cast len is undefined at best.
Fix: bail early on len <= 0 (the patch is a PUT of the file's
contents, empty or error both mean nothing to upload), then
NULL-check the malloc. Function is void-returning, so on OOM
just silently skip this upload and let the next sync poll
retry.
=== network/netplay/netplay_frontend.c: state-buffer growth ===
When a peer sends a state_size larger than ours, all buffer
entries are grown to match:
netplay->state_size = state_size;
for (i = 0; i < netplay->buffer_size; i++)
{
netplay->buffer[i].state = realloc(netplay->buffer[i].state, netplay->state_size);
if (!netplay->buffer[i].state)
return false;
}
Classic realloc-assign-self leak: on OOM realloc returns NULL
but leaves the prior buffer[i].state allocation intact; the
self-assign overwrites the only pointer to it and leaks the
whole ring entry.
Fix: realloc into a temp, NULL-check, commit to the ivar only on
success. Still return false on OOM; the caller disconnects the
netplay connection on false return, so the partially-grown
buffer (entries 0..i-1 at new size, i..end at old size) is
about to be torn down anyway and the inconsistency is transient.
=== Thread-safety ===
Unchanged. s3 and google_drive cloud-sync helpers run on the
task-queue worker thread; netplay state handling runs on the
netplay thread. No new locks or ordering requirements
introduced.
=== Reachability ===
Cloud-sync helpers fire on every background sync poll
(configurable, default ~5-minute interval). s3_url_encode and
s3_trim_string are called on every request path / header field.
The netplay realloc is hit when a peer with a different-version
core joins and the two sides disagree on save-state size. All
four are reachable in realistic runtime conditions.
* Ensure we're using C89 standards and other libretro standards
* Use more appropriate logging levels so we don't spam the log
* Fix incorrect S3 part size
* Massive reduction in heap space allocation, going from settings struct
264kb to 119Kb
* Use NAME_MAX_LENGTH for base paths/names, etc
* Use DIR_MAX_LENGTH for directory sizes