libretro-common/string: add strlcpy_append helper and apply to database_info_build_query_enum

Add strlcpy_append, a bound-checked replacement for the unsafe
idiom that's widespread in this codebase:

   _len += strlcpy(s + _len, src, len - _len);

That looks like a self-bounding chain but isn't.  strlcpy
returns strlen(source) regardless of how much was actually
written, so on truncation _len overshoots len and the next
(len - _len) underflows to a huge size_t, driving the next
strlcpy into an OOB-write or memcpy of attacker-influenced
size.  This is the same primitive that drove the shader-
wildcard fix (commit bac6894).

strlcpy_append takes (s, len, *pos, src), checks the
truncation condition correctly, and on overflow:
  - leaves s NUL-terminated at len - 1 (strlcpy's contract)
  - clamps *pos to len - 1 so subsequent calls in a chain
    short-circuit (also returning -1)
  - returns -1 to the caller

The clamp is what makes chained use safe with a single check
at the end of the chain.

Apply to database_info_build_query_enum (database_info.c).
That function builds DB query strings of the shape

   {'KEY':"PATH"}      (or numeric variants without the quotes,
                        or {'developer':glob('*PATH*')})

via 22 switch cases, each of which was a hand-unrolled chain
of s[++_len] = '...' character writes followed by a path
strlcpy and another short s[++_len] = '...' chain to close
the brace.  None of those writes was bounded against len.
A caller passing a buffer smaller than the prefix length
got OOB-writes from the prefix; a caller whose buffer was
just barely enough for prefix + path got the trailing
s[_len]='"';s[++_len]='}' writes off the end (up to 3 bytes
OOB).  All callers in tree pass query[4096] from the menu
displaylist, so practical buffers are large -- but the
pattern is wrong and the path component is attacker-
influenced when scanning content metadata.

Side fix in DATABASE_QUERY_ENTRY_BBFC_RATING: pre-rewrite
the case wrote s[++_len] = '"' after the path strlcpy
instead of s[_len] = '"', leaving the strlcpy's NUL
terminator embedded in the query.  BBFC rating searches
have been silently returning nothing since this code was
written.  Fixed while rewriting.

Adds libretro-common/samples/string/strlcpy_append/
strlcpy_append_test as a regression test, registered in
.github/workflows/Linux-libretro-common-samples.yml's
RUN_TARGETS allowlist (which builds with ASan + UBSan
default since the cdfs/chd commit).  Eight cases covering
the contract: basic append, chained append, exact-fit
boundary, wide-margin truncation, chain short-circuiting
after truncation, empty source, NULL/zero-length args, and
*pos already at or past len.

Test follows the existing libretro-common/samples pattern
(heap-allocated destinations sized to exactly the legal
capacity so any reintroduction of the unbounded chain is
flagged by ASan).  Verified to fire under ASan when the
helper is replaced with the unsafe naive idiom: heap-
buffer-overflow WRITE of size 2 at offset 26 of an 8-byte
allocation in test_chain_short_circuits_after_truncation.
With the safe helper all 8 cases pass clean.

Verified database_info_build_query_enum produces identical
output for all 21 enum values at len=4096 (the production
caller's buffer size); also verified that at len=8 the
function returns -1 with no OOB write, which is the
behaviour the unsafe naive replacement would have failed at.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre committed 2026-04-28 06:44:59 +02:00
1 parent c41e955464
commit 78c52ab36e
6 files changed
+495 -399

No files matched your search

@@ -69,6 +69,7 @@ jobs:
cdrom_cuesheet_overflow_test
cdfs_dir_record_test
chd_meta_overflow_test
strlcpy_append_test
http_parse_test
rjson_test
rtga_test
+114 -399
View File
@@ -35,450 +35,165 @@ int database_info_build_query_enum(char *s, size_t len,
enum database_query_type type,
const char *path)
{
/* Build queries of the shape {'KEY':"PATH"} (or {'KEY':PATH}
* for numeric/rating fields, or the DEVELOPER glob form).
*
* Pre-this-rewrite each case unrolled the per-character
* writes with s[++_len]= and then a path strlcpy followed
* by another short s[++_len]= chain. None of those writes
* was bounded against len; if the caller's buffer was
* smaller than the prefix's length the writes ran off the
* end of s, and after the path strlcpy the trailing
* s[_len]='"';s[++_len]='}' sequence could OOB-write up to
* 3 bytes when path filled s. Naively replacing the chain
* with _len += strlcpy(s+_len, lit, len-_len) does not
* help: strlcpy returns strlen(source) regardless of
* truncation, so on overflow _len passes len and the next
* (len - _len) underflows size_t, producing an
* unbounded-size strlcpy and the same OOB.
*
* Use strlcpy_append (libretro-common/string/stdstring.c),
* which is bound-checked and clamps *pos to len - 1 on
* truncation, so subsequent calls in the chain short-
* circuit safely. The caller checks the final return
* value to detect any truncation in the chain. */
size_t _len = 0;
if (!s || len == 0)
return -1;
switch (type)
{
case DATABASE_QUERY_ENTRY:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'n';
s[++_len] = 'a';
s[++_len] = 'm';
s[++_len] = 'e';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'name':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_PUBLISHER:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'p';
s[++_len] = 'u';
s[++_len] = 'b';
s[++_len] = 'l';
s[++_len] = 'i';
s[++_len] = 's';
s[++_len] = 'h';
s[++_len] = 'e';
s[++_len] = 'r';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'publisher':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_DEVELOPER:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'd';
s[++_len] = 'e';
s[++_len] = 'v';
s[++_len] = 'e';
s[++_len] = 'l';
s[++_len] = 'o';
s[++_len] = 'p';
s[++_len] = 'e';
s[++_len] = 'r';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = 'g';
s[++_len] = 'l';
s[++_len] = 'o';
s[++_len] = 'b';
s[++_len] = '(';
s[++_len] = '\'';
s[++_len] = '*';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '*';
s[++_len] = '\'';
s[++_len] = ')';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'developer':glob('*");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "*')}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_ORIGIN:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'o';
s[++_len] = 'r';
s[++_len] = 'i';
s[++_len] = 'g';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'origin':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_FRANCHISE:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'f';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 'n';
s[++_len] = 'c';
s[++_len] = 'h';
s[++_len] = 'i';
s[++_len] = 's';
s[++_len] = 'e';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'franchise':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'e';
s[++_len] = 's';
s[++_len] = 'r';
s[++_len] = 'b';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'esrb_rating':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_BBFC_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'b';
s[++_len] = 'b';
s[++_len] = 'f';
s[++_len] = 'c';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[++_len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
/* Pre-rewrite this case wrote s[++_len] = '"' after
* the path strlcpy instead of s[_len] = '"', leaving
* the strlcpy's NUL terminator embedded in the query
* and silently breaking BBFC searches. Fix while
* rewriting. */
strlcpy_append(s, len, &_len, "{'bbfc_rating':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_ELSPA_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'e';
s[++_len] = 'l';
s[++_len] = 's';
s[++_len] = 'p';
s[++_len] = 'a';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'elspa_rating':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_ESRB_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'e';
s[++_len] = 's';
s[++_len] = 'r';
s[++_len] = 'b';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'esrb_rating':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_PEGI_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'p';
s[++_len] = 'e';
s[++_len] = 'g';
s[++_len] = 'i';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'pegi_rating':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_CERO_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'c';
s[++_len] = 'e';
s[++_len] = 'r';
s[++_len] = 'o';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'cero_rating':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_ENHANCEMENT_HW:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'e';
s[++_len] = 'n';
s[++_len] = 'h';
s[++_len] = 'a';
s[++_len] = 'n';
s[++_len] = 'c';
s[++_len] = 'e';
s[++_len] = 'm';
s[++_len] = 'e';
s[++_len] = 'n';
s[++_len] = 't';
s[++_len] = '_';
s[++_len] = 'h';
s[++_len] = 'w';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'enhancement_hw':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_EDGE_MAGAZINE_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'e';
s[++_len] = 'd';
s[++_len] = 'g';
s[++_len] = 'e';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'edge_rating':");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_EDGE_MAGAZINE_ISSUE:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'e';
s[++_len] = 'd';
s[++_len] = 'g';
s[++_len] = 'e';
s[++_len] = '_';
s[++_len] = 'i';
s[++_len] = 's';
s[++_len] = 's';
s[++_len] = 'u';
s[++_len] = 'e';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'edge_issue':");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_FAMITSU_MAGAZINE_RATING:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'f';
s[++_len] = 'a';
s[++_len] = 'm';
s[++_len] = 'i';
s[++_len] = 't';
s[++_len] = 's';
s[++_len] = 'u';
s[++_len] = '_';
s[++_len] = 'r';
s[++_len] = 'a';
s[++_len] = 't';
s[++_len] = 'i';
s[++_len] = 'n';
s[++_len] = 'g';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'famitsu_rating':");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_RELEASEDATE_MONTH:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'r';
s[++_len] = 'e';
s[++_len] = 'l';
s[++_len] = 'e';
s[++_len] = 'a';
s[++_len] = 's';
s[++_len] = 'e';
s[++_len] = 'm';
s[++_len] = 'o';
s[++_len] = 'n';
s[++_len] = 't';
s[++_len] = 'h';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'releasemonth':");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_RELEASEDATE_YEAR:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'r';
s[++_len] = 'e';
s[++_len] = 'l';
s[++_len] = 'e';
s[++_len] = 'a';
s[++_len] = 's';
s[++_len] = 'e';
s[++_len] = 'y';
s[++_len] = 'e';
s[++_len] = 'a';
s[++_len] = 'r';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'releaseyear':");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_MAX_USERS:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'u';
s[++_len] = 's';
s[++_len] = 'e';
s[++_len] = 'r';
s[++_len] = 's';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'users':");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_GENRE:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'g';
s[++_len] = 'e';
s[++_len] = 'n';
s[++_len] = 'r';
s[++_len] = 'e';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'genre':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_ENTRY_REGION:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = 'r';
s[++_len] = 'e';
s[++_len] = 'g';
s[++_len] = 'i';
s[++_len] = 'o';
s[++_len] = 'n';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'region':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
case DATABASE_QUERY_NONE:
s[ _len] = '{';
s[++_len] = '\'';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '\'';
s[++_len] = ':';
s[++_len] = '"';
s[++_len] = '\0';
_len += strlcpy(s + _len, path, len - _len);
s[ _len] = '"';
s[++_len] = '}';
s[++_len] = '\0';
strlcpy_append(s, len, &_len, "{'':':\"");
strlcpy_append(s, len, &_len, path);
if (strlcpy_append(s, len, &_len, "\"}"))
return -1;
break;
}
@@ -355,6 +355,38 @@ void string_replace_multi_space_with_single_space(char *str);
**/
size_t string_remove_all_whitespace(char *str_trimmed, const char *str);
/**
* strlcpy_append:
* @s : destination buffer
* @len : total size of @s, including space for the NUL
* @pos : in-out cursor. On entry, the offset within
* @s where @src should be appended; on
* successful return advanced past the appended
* bytes. On truncation clamped to @len - 1 so
* subsequent calls in a chain short-circuit.
* @src : NUL-terminated source string to append
*
* Bound-checked replacement for the unsafe pattern
*
* *pos += strlcpy(@s + *pos, @src, @len - *pos);
*
* which is widely used to build strings piece-by-piece but is
* NOT self-bounding -- strlcpy returns strlen(@src) regardless
* of how much was actually written, so on truncation *pos
* overshoots @len and the next @len - *pos subtraction underflows
* size_t, corrupting subsequent appends.
*
* On success returns 0 and advances *pos by strlen(@src). On
* truncation (the appended bytes plus a NUL would exceed @len)
* returns -1, leaves @s NUL-terminated at @len - 1, and clamps
* *pos to @len - 1 so a chain of strlcpy_append calls can
* short-circuit cleanly -- the caller need only check the bitwise
* OR of the chain's results once at the end.
*
* @return 0 on success, -1 on truncation or invalid arguments
**/
int strlcpy_append(char *s, size_t len, size_t *pos, const char *src);
/* Retrieve the last occurance of the given character in a string. */
int string_index_last_occurance(const char *str, char c);
@@ -0,0 +1,35 @@
TARGET := strlcpy_append_test
LIBRETRO_COMM_DIR := ../../..
# strlcpy_append lives in libretro-common/string/stdstring.c.
# Only the helper itself plus strlcpy (compat_strl.c) are needed
# at link time.
SOURCES := \
strlcpy_append_test.c \
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
$(LIBRETRO_COMM_DIR)/compat/compat_strl.c \
$(LIBRETRO_COMM_DIR)/compat/compat_posix_string.c
OBJS := $(SOURCES:.c=.o)
CFLAGS += -Wall -pedantic -std=gnu99 -g -O0 -I$(LIBRETRO_COMM_DIR)/include
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,268 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (strlcpy_append_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 strlcpy_append in
* libretro-common/string/stdstring.c.
*
* strlcpy_append exists because the unsafe idiom
* _len += strlcpy(s + _len, src, len - _len);
* silently corrupts @s on truncation: strlcpy returns
* strlen(@src), so once @src is too long for the remaining
* space, _len overshoots @len and the next subtraction
* (len - _len) underflows size_t.
*
* Contract: on success the function advances *pos by
* strlen(@src) and returns 0. On truncation it leaves @s
* NUL-terminated and clamps *pos to len - 1, so subsequent
* calls in a chain short-circuit (also returning -1). This
* lets callers chain three or more appends and check just
* the final return value.
*
* Heap-allocated destinations sized to exactly the legal
* capacity so AddressSanitizer flags any reintroduction of
* the unbounded chain.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string/stdstring.h>
static int failures = 0;
#define EXPECT_EQ_INT(got, want, msg) do { \
long _g = (long)(got), _w = (long)(want); \
if (_g != _w) { \
printf("[ERROR] %s:%d %s got=%ld want=%ld\n", \
__func__, __LINE__, (msg), _g, _w); \
failures++; \
} \
} while (0)
#define EXPECT_EQ_STR(got, want, msg) do { \
if (strcmp((got), (want)) != 0) { \
printf("[ERROR] %s:%d %s got=\"%s\" want=\"%s\"\n", \
__func__, __LINE__, (msg), (got), (want)); \
failures++; \
} \
} while (0)
#define EXPECT_TRUE(cond, msg) do { \
if (!(cond)) { \
printf("[ERROR] %s:%d %s\n", __func__, __LINE__, (msg)); \
failures++; \
} \
} while (0)
/* ---- tests ------------------------------------------------ */
static void test_basic_append(void)
{
char *s = (char*)malloc(16);
size_t pos = 0;
int rv, saved = failures;
if (!s) { printf("[ERROR] alloc\n"); failures++; return; }
memset(s, 0, 16);
rv = strlcpy_append(s, 16, &pos, "hello");
EXPECT_EQ_INT(rv, 0, "rv");
EXPECT_EQ_INT(pos, 5, "pos");
EXPECT_EQ_STR(s, "hello", "buffer");
free(s);
if (failures == saved) printf("[SUCCESS] basic append\n");
}
static void test_chained_append(void)
{
char *s = (char*)malloc(32);
size_t pos = 0;
int rv1, rv2, rv3, saved = failures;
if (!s) { printf("[ERROR] alloc\n"); failures++; return; }
memset(s, 0, 32);
rv1 = strlcpy_append(s, 32, &pos, "hello, ");
rv2 = strlcpy_append(s, 32, &pos, "world");
rv3 = strlcpy_append(s, 32, &pos, "!");
EXPECT_EQ_INT(rv1, 0, "rv1");
EXPECT_EQ_INT(rv2, 0, "rv2");
EXPECT_EQ_INT(rv3, 0, "rv3");
EXPECT_EQ_INT(pos, 13, "pos");
EXPECT_EQ_STR(s, "hello, world!", "buffer");
free(s);
if (failures == saved) printf("[SUCCESS] chained append\n");
}
static void test_truncation_at_boundary(void)
{
/* Buffer sized exactly for "hello" + NUL = 6. First append
* fits exactly. Second append truncates: rv = -1, pos
* clamps to len - 1 = 5, buffer remains "hello". */
char *s = (char*)malloc(6);
size_t pos = 0;
int rv, saved = failures;
if (!s) { printf("[ERROR] alloc\n"); failures++; return; }
memset(s, 0, 6);
rv = strlcpy_append(s, 6, &pos, "hello");
EXPECT_EQ_INT(rv, 0, "first rv");
EXPECT_EQ_INT(pos, 5, "first pos");
EXPECT_EQ_STR(s, "hello", "buffer after first");
rv = strlcpy_append(s, 6, &pos, "x");
EXPECT_EQ_INT(rv, -1, "second rv (truncation)");
EXPECT_EQ_INT(pos, 5, "pos clamped to len - 1");
EXPECT_EQ_STR(s, "hello", "buffer NUL-terminated at len - 1");
free(s);
if (failures == saved) printf("[SUCCESS] truncation at boundary\n");
}
static void test_truncation_wide(void)
{
/* Source much larger than destination. Pre-fix the naive
* idiom would have memcpy'd off the end of s; with
* strlcpy_append the call clamps without writing past the
* buffer. Heap-allocated to exactly 4 bytes so ASan flags
* any OOB. */
char *s = (char*)malloc(4);
size_t pos = 0;
int rv, saved = failures;
if (!s) { printf("[ERROR] alloc\n"); failures++; return; }
memset(s, 0, 4);
rv = strlcpy_append(s, 4, &pos,
"this string is much longer than the destination");
EXPECT_EQ_INT(rv, -1, "rv");
EXPECT_EQ_INT(pos, 3, "pos clamped to len - 1");
/* strlcpy fills 3 chars + NUL. Buffer is "thi". */
EXPECT_EQ_STR(s, "thi", "buffer truncated and NUL-terminated");
free(s);
if (failures == saved) printf("[SUCCESS] truncation by wide margin\n");
}
static void test_chain_short_circuits_after_truncation(void)
{
/* Once a previous call truncates and clamps *pos to len - 1,
* every subsequent call short-circuits with -1 without
* writing past the buffer. This is the property that makes
* "check only the last return value" safe. */
char *s = (char*)malloc(8);
size_t pos = 0;
int rv1, rv2, rv3, saved = failures;
if (!s) { printf("[ERROR] alloc\n"); failures++; return; }
memset(s, 0, 8);
rv1 = strlcpy_append(s, 8, &pos, "ab");
rv2 = strlcpy_append(s, 8, &pos,
"this exceeds the remaining space");
rv3 = strlcpy_append(s, 8, &pos, "cd");
EXPECT_EQ_INT(rv1, 0, "rv1 fits");
EXPECT_EQ_INT(rv2, -1, "rv2 truncates");
EXPECT_EQ_INT(rv3, -1, "rv3 short-circuits");
EXPECT_EQ_INT(pos, 7, "pos clamped at len - 1");
EXPECT_TRUE(strlen(s) <= 7, "buffer fits");
free(s);
if (failures == saved) printf("[SUCCESS] chain short-circuits after truncation\n");
}
static void test_empty_source(void)
{
char *s = (char*)malloc(8);
size_t pos = 0;
int rv, saved = failures;
if (!s) { printf("[ERROR] alloc\n"); failures++; return; }
memset(s, 0, 8);
rv = strlcpy_append(s, 8, &pos, "");
EXPECT_EQ_INT(rv, 0, "rv");
EXPECT_EQ_INT(pos, 0, "pos unchanged");
EXPECT_EQ_STR(s, "", "buffer empty");
rv = strlcpy_append(s, 8, &pos, "ok");
EXPECT_EQ_INT(rv, 0, "second rv");
EXPECT_EQ_INT(pos, 2, "second pos");
EXPECT_EQ_STR(s, "ok", "second buffer");
free(s);
if (failures == saved) printf("[SUCCESS] empty source\n");
}
static void test_null_args(void)
{
char buf[16];
size_t pos = 0;
int saved = failures;
EXPECT_EQ_INT(strlcpy_append(NULL, 16, &pos, "x"), -1, "NULL s");
EXPECT_EQ_INT(strlcpy_append(buf, 16, NULL, "x"), -1, "NULL pos");
EXPECT_EQ_INT(strlcpy_append(buf, 16, &pos, NULL), -1, "NULL src");
EXPECT_EQ_INT(strlcpy_append(buf, 0, &pos, "x"), -1, "zero len");
if (failures == saved) printf("[SUCCESS] NULL/zero args rejected\n");
}
static void test_pos_at_or_past_len(void)
{
char *s = (char*)malloc(8);
size_t pos = 8;
int rv, saved = failures;
if (!s) { printf("[ERROR] alloc\n"); failures++; return; }
memset(s, 0, 8);
rv = strlcpy_append(s, 8, &pos, "x");
EXPECT_EQ_INT(rv, -1, "pos == len rejects");
EXPECT_EQ_INT(pos, 7, "pos clamped to len - 1");
pos = 99;
rv = strlcpy_append(s, 8, &pos, "x");
EXPECT_EQ_INT(rv, -1, "pos > len rejects");
EXPECT_EQ_INT(pos, 7, "pos clamped to len - 1");
free(s);
if (failures == saved) printf("[SUCCESS] pos at/past len rejected and clamped\n");
}
int main(void)
{
test_basic_append();
test_chained_append();
test_truncation_at_boundary();
test_truncation_wide();
test_chain_short_circuits_after_truncation();
test_empty_source();
test_null_args();
test_pos_at_or_past_len();
if (failures)
{
printf("\n%d strlcpy_append test(s) failed\n", failures);
return 1;
}
printf("\nAll strlcpy_append regression tests passed.\n");
return 0;
}
+45
View File
@@ -680,6 +680,51 @@ size_t string_remove_all_whitespace(char *s, const char *str)
return s - start;
}
/**
* strlcpy_append:
*
* See header (libretro-common/include/string/stdstring.h) for the
* full contract. Bound-checked append; advances *pos by
* strlen(@src) on success, returns -1 on truncation.
*
* Truncation is signalled when *pos >= len already, or when
* strlcpy returns a value >= the remaining capacity. In either
* case the destination is left NUL-terminated (strlcpy guarantees
* this when its size argument is non-zero) and *pos is clamped to
* len - 1 so subsequent calls in a chain become no-ops that also
* return -1.
**/
int strlcpy_append(char *s, size_t len, size_t *pos, const char *src)
{
size_t remaining;
size_t n;
if (!s || !pos || !src || len == 0)
return -1;
if (*pos >= len)
{
/* Already saturated; clamp and report truncation. */
*pos = len - 1;
return -1;
}
remaining = len - *pos;
n = strlcpy(s + *pos, src, remaining);
if (n >= remaining)
{
/* strlcpy truncated. s + len - 1 is NUL-terminated by
* strlcpy's contract. Clamp *pos so subsequent appends
* short-circuit. */
*pos = len - 1;
return -1;
}
*pos += n;
return 0;
}
/**
* Retrieve the last occurrence of the given character in a string.
*/