Files
RetroArch/libretro-common/test/compat/test_strl.c
T
LibretroAdmin b9be38fd55 strl: copy string literals without scanning them at runtime
strlcpy() begins with strlen() of its source. It lives in another
translation unit, so with no LTO that scan happens at runtime even when
the source is a literal whose length the compiler already measured, and
the call itself cannot be folded away either.

strlcpy_lit() passes the length over as sizeof(). When the destination
size is also a constant -- sizeof(buf), the usual case -- nothing is
left but the stores memcpy() compiles to:

  strlcpy      "core_options"  sizeof(buf)   7.01 ns
  strlcpy_lit  "core_options"  sizeof(buf)   0.42 ns
  strlcpy      41-char literal sizeof(buf)   7.74 ns
  strlcpy_lit  41-char literal sizeof(buf)   0.71 ns
  strlcpy      "core_options"  runtime len   7.11 ns
  strlcpy_lit  "core_options"  runtime len   0.70 ns

The destination bound is kept. When the literal does not fit it defers
to strlcpy(), so the copy truncates as before rather than overrunning;
a form that took only the literal would have turned every one of these
sites into an overflow waiting for a small buffer. The literal is
concatenated with "" so a non-literal source is a compile error rather
than a sizeof() that silently measures a pointer.

818 sites across 104 files were converted by parsing arguments rather
than by pattern-matching text: only a three-argument call whose second
argument is a bare string literal is rewritten, so a macro that happens
to expand to a literal is left alone. Line endings are preserved, so
the CRLF files carry only their own changed lines.

test/compat/test_strl.c compares the two against each other over every
destination size from 0 past the literal's length, for literals that
are empty, single-character, path-shaped, adjacently concatenated and
escape-bearing, checking the return, the terminator and the whole
buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and
TSan both 4/4. The full menu_displaylist_build_list walk over every
displaylist type is byte-identical to before the change
(md5 a2a081bcac09cdc8d72d67efd14ac40e).
2026-08-30 02:10:20 +02:00

123 lines
4.3 KiB
C

/* Tests for strlcpy_lit(), the literal-source form of strlcpy().
*
* strlcpy() opens with strlen() of its source, and because it lives in
* another translation unit that scan happens at runtime even when the
* source is a literal the compiler has already measured. strlcpy_lit()
* hands the length over as sizeof(), so the copy folds to stores.
*
* The property that matters is that it is a drop-in: for every literal
* length against every destination size, the bytes written, the
* terminator and the return value all have to match what strlcpy()
* would have produced -- including when the literal does not fit, where
* dropping the bound would turn a truncation into an overrun. The
* exhaustive case below pins that across the whole small-size matrix
* rather than at a few hand-picked points.
*/
#include <check.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <compat/strl.h>
#define SUITE_NAME "strl"
/* Compares strlcpy_lit() against strlcpy() for one literal, over every
* destination size from 0 up past the literal's length. The buffer is
* poisoned first so an unwritten tail is visible, and the full buffer
* is compared so an overrun past the terminator is caught. */
#define CHECK_LIT(lit) \
do { \
size_t sz; \
for (sz = 0; sz <= sizeof("" lit) + 4; sz++) \
{ \
char a[64], b[64]; \
size_t ra, rb; \
memset(a, '\xA5', sizeof(a)); \
memset(b, '\xA5', sizeof(b)); \
ra = strlcpy(a, "" lit, sz); \
rb = strlcpy_lit(b, lit, sz); \
ck_assert_uint_eq(ra, rb); \
ck_assert_mem_eq(a, b, sizeof(a)); \
} \
} while (0)
START_TEST (test_lit_matches_strlcpy)
{
CHECK_LIT("");
CHECK_LIT("a");
CHECK_LIT("ab");
CHECK_LIT("core_options");
CHECK_LIT("input_player1_analog_dpad_mode");
CHECK_LIT("/");
CHECK_LIT(".lpl");
/* Adjacent literals concatenate, so sizeof still measures the
* characters and not a pointer. */
CHECK_LIT("smb:" "//");
/* Embedded escapes must not confuse sizeof. */
CHECK_LIT("a\tb\\c");
}
END_TEST
START_TEST (test_lit_return_is_source_length)
{
char s[8];
/* Fits. */
ck_assert_uint_eq(strlcpy_lit(s, "abc", sizeof(s)), 3);
ck_assert_str_eq(s, "abc");
/* Exactly fits: 7 characters plus the terminator. */
ck_assert_uint_eq(strlcpy_lit(s, "abcdefg", sizeof(s)), 7);
ck_assert_str_eq(s, "abcdefg");
/* Does not fit: truncates and reports the source length, as
* strlcpy() does. */
ck_assert_uint_eq(strlcpy_lit(s, "abcdefgh", sizeof(s)), 8);
ck_assert_str_eq(s, "abcdefg");
}
END_TEST
START_TEST (test_lit_runtime_size)
{
char s[16];
/* A size the compiler cannot fold, which is what a 'len' parameter
* looks like from inside a function. */
size_t n = 4;
memset(s, '\xA5', sizeof(s));
ck_assert_uint_eq(strlcpy_lit(s, "abcdefgh", n), 8);
ck_assert_str_eq(s, "abc");
ck_assert_int_eq(s[4], '\xA5');
}
END_TEST
START_TEST (test_lit_zero_size_writes_nothing)
{
char s[4];
memset(s, '\xA5', sizeof(s));
ck_assert_uint_eq(strlcpy_lit(s, "abc", 0), strlcpy(s, "abc", 0));
ck_assert_int_eq(s[0], '\xA5');
}
END_TEST
Suite *create_suite(void)
{
Suite *s = suite_create(SUITE_NAME);
TCase *tc_core = tcase_create("Core");
tcase_add_test(tc_core, test_lit_matches_strlcpy);
tcase_add_test(tc_core, test_lit_return_is_source_length);
tcase_add_test(tc_core, test_lit_runtime_size);
tcase_add_test(tc_core, test_lit_zero_size_writes_nothing);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
int num_fail;
Suite *s = create_suite();
SRunner *sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
num_fail = srunner_ntests_failed(sr);
srunner_free(sr);
return (num_fail == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}