Skip to content

fix(libc): follow snprintf_s and sscanf_s constraints as measured on a PS5 - #9087

Open
orezraey wants to merge 27 commits into
boykopovar:mainfrom
orezraey:fix/libc-formatting-measured-ps5
Open

orezraey wants to merge 27 commits into
boykopovar:mainfrom
orezraey:fix/libc-formatting-measured-ps5

Conversation

@orezraey

@orezraey orezraey commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

snprintf_s, vsnprintf_s, sscanf_s and vsscanf_s followed C11 Annex K with guessed constants. Their debt entries said the constraint handler is not invoked and the console's values are unverified. In a payload process on a PS5 (firmware 13.60, libSceLibcInternal.sprx) most of those guesses differ.

snprintf_s / vsnprintf_s (both behave the same; errno never changes):

Case Payload process, firmware 13.60 main before this PR here now
RSIZE_MAX 2^61 - 1 (a boundary search: 0x1fffffffffffffff passes, 0x2000000000000000 fails) SIZE_MAX >> 1 2^61 - 1
null buffer, size 0 or above RSIZE_MAX -1, buffer untouched, handler called with "snprintf_s: bad argument" (or vsnprintf_s: …), a null pointer and 22 -1, no handler, SIZE_MAX >> 1 accepted as on the console
null format -1, buffer[0] = 0, handler "…: bad format argument" -1, buffer[0] = 0, no handler as on the console
%n / %hn (also after truncation) -23; the output before it stays written without a terminator, then buffer[0] = 0; handler "printf_s: %n disallowed"; the pointer is not written -1, terminated output as on the console
null %s argument (also %.0s) -23, same buffer handling, handler "printf_s: bad %s argument" -1, no handler as on the console
null %ls / %S argument -23, handler "printf_s: bad %ls argument" -1, no handler as on the console

sscanf_s / vsscanf_s (both behave the same):

Case Payload process, firmware 13.60 main before this PR here now
size argument 64 bits: a size of 2^32 + 2 is used as such read as unsigned int 64 bits
%s / %[ field of size or more characters the first size characters stored without a terminator; matching failure, returns the items assigned so far buffer[0] = 0 as on the console
%c with a width above size stores size characters and succeeds, consuming only those failure, buffer[0] = 0 as on the console
%ls / %l[ and %lc with a width above size (titles' own libc.prx, measured by JJenkx) %ls and %l[ follow the narrow rules; %lc stores size units and succeeds, but consumes the whole width %lc consumed only the stored characters as measured
size above RSIZE_MAX handler "scanf_s: bad %c, %s, or %[ size" not checked as on the console
null %s / %c / %[ destination handler "scanf_s: bad %c, %s, or %[ argument", checked before reading input (also on empty input) EOF, no handler as on the console
null integer, %p or floating-point destination checked only after a successful conversion: empty input gives EOF and "x" gives 0, without the handler EOF as on the console
result of any violation the handler with the console's message, a null pointer and 22; returns the assignments so far, or -23 when no conversion (suppressed ones and %n included) has completed EOF as on the console
null input or format EOF, handler "sscanf_s: bad argument" / "vsscanf_s: bad argument" EOF, no handler as on the console
literal mismatch at the end of input ("" with "x%d") 0 EOF 0

fscanf_s / vfscanf_s / scanf_s: a literal mismatch at the end of input now returns the assignments, as sscanf_s does: empty input with "x%2c%n" returns 0 and leaves errno, the %n target and the destination untouched, following JJenkx's CPU-only run of two reference libc modules (earlier heads and main return EOF). %% at the end of input still returns EOF. The stream forms were not run on a console.

The -23 is the same with a recording handler, an empty one and ignore_handler_s. set_constraint_handler_s, abort_handler_s and ignore_handler_s are exported, and passing NULL reinstalls the default.

The formatter's terminator: to leave the partial output unterminated on a violation, FormatOutput now writes the terminator once at the end instead of after every piece. Every caller goes through FormatWindows, which does that at the end. On success every output is the same as before, which the existing formatting tests check.

Not measured, recorded in docs/dev/debt/libc.md:

  • The console's default handler writes "<message>\n" before ending the process; AnyPS5's default still aborts without the message.
  • An unknown conversion such as %y is copied to the output by the console and still throws here.
  • A null %hs argument was not measured.
  • swscanf_s's size width (it still reads unsigned int) was not measured.

Tests: guest_formatting, guest_scan_count (also JJenkx's wide rows) and guest_strings check the measured cases, with the old expectations updated and size arguments passed as size_t. guest_scan_count fails if the -23 constant is changed.

How the console was measured

  • Setup:
    • The console runs firmware 13.60 and is jailbroken.
    • The probes are ELF payloads built with ps5-payload-dev/sdk v0.44 and clang 18.1.8 and started through elfldr, so they ran in a payload process, not in a title.
  • Provenance: libSceLibcInternal.sprx was loaded with dlopen, and every function was taken from it with dlsym.
  • Method:
    • Buffers are filled with ~ and integer destinations preset to -7.
    • A recording constraint handler logs the message, pointer and error of each call.
    • libcfmt3 and libcfmt4 reinstall the default handler and end their own process with it, to see what it does.
  • Files: the probe sources and full logs are in this gist.
Console output (excerpt, copied unchanged)
libcfmt: meta fw=0x13600007 pid=153 sdk=v0.44 compiler=Clang 18.1.8 built=Oct 21 2026 08:20:42 log=/data/anyps5-probes/libcfmt.log
libcfmt: case snprintf_s truncate size=5 "abcdef" ret=6 errno=0 buf="abcd\x00~~~~~~~~~~~" handler=0
libcfmt: case snprintf_s size=0 ret=-1 errno=0 buf="~~~~~~~~~~~~~~~~" handler=1 msg="snprintf_s: bad argument" err=22 ptr_off=-2
libcfmt: case size_boundary last_ret_1=0x1fffffffffffffff first_ret_-1=0x2000000000000000
libcfmt: case snprintf_s %n ret=-23 errno=0 buf="\x00b~~~~~~~~~~~~~~" handler=1 msg="printf_s: %n disallowed" err=22 ptr_off=-2
libcfmt: case vsscanf_s "abc1" %[a-z] cap=4 ret=1 errno=0 n1=-7 n2=-7 a="abc\x00~~~~" b="~~~~~~~~" handler=0
libcfmt: case vsscanf_s "abcd1" %[a-z] cap=4 ret=0 errno=0 n1=-7 n2=-7 a="abcd~~~~" b="~~~~~~~~" handler=0
libcfmt: case vsscanf_s "x" %3c cap=2 ret=1 errno=0 n1=-7 n2=-7 a="x~~~~~~~" b="~~~~~~~~" handler=0
libcfmt2: case vsscanf_s null %s first ret=-23 n1=-7 n2=-7 f=-7 a="~~~~~~~~" b="~~~~~~~~" handler=1 msg="scanf_s: bad %c, %s, or %[ argument" err=22
libcfmt2: case vsscanf_s "ab" %s cap=2^61 ret=-23 n1=-7 n2=-7 f=-7 a="~~~~~~~~" b="~~~~~~~~" handler=1 msg="scanf_s: bad %c, %s, or %[ size" err=22
libcfmt3: case vsscanf_s "5 ab" %d %s(null) %d ret=1 n1=5 n2=-7 f=-7 a="~~~~~~~~" b="~~~~~~~~" handler=1 msg="scanf_s: bad %c, %s, or %[ argument" err=22

Tested

  • Head d826fd16d3: main was merged in, with fix(libc): match bundled narrow scansets and scan directive parsing #12428; the unknown-conversion exit now comes before the secure block. guest_formatting, guest_wide_formatting, guest_scan_count, guest_stdio_variadic, guest_safe_errors and guest_strings pass three runs in a row on Windows (MinGW, GCC 15.2) and on Linux (WSL, GCC); check_conventions passes. Disregard the Linux result recorded for 24f4242cbd: another checkout changed the shared tree while that build ran. This Linux run replaces it, on a tree nothing else touched.
  • Head 24f4242cbd: main (1d91f2966d, with fix(libc): read wide scanf fields byte by byte, as titles' libc.prx does #12468) was merged in and the secure bounds were re-applied on its source scanner: for sscanf_s and vsscanf_s, %ls, %lc and %l[ keep the first size characters without a terminator when they do not fit, and %lc consumes the rest of its width, which now counts bytes as fix(libc): read wide scanf fields byte by byte, as titles' libc.prx does #12468's loop does. Stream forms keep main's behaviour. libc.md keeps fix(libc): read wide scanf fields byte by byte, as titles' libc.prx does #12468's scanf_s wording and this PR's. At this head guest_formatting, guest_scan_count, guest_wide_formatting, guest_stdio_variadic, guest_strings, guest_wide_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions reports no errors.
  • Head e16c92d39f merges main (93dbd0f82a, with feat(libc): add Dinkumware float bit helpers from firmware #8397); only the sscanf_s debt line met feat(libc): add Dinkumware float bit helpers from firmware #8397's new _LDnorm entry, and both are kept. At this head the twelve tests below pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL, GCC 16.2), and check_conventions reports no errors (one debt-length warning on a libc.md entry this PR did not add).
  • Head 053ebd8a33: main (1ff6c8739b) was merged in. Only libc.md met, at the scanf_s and freopen_s entries: it keeps this PR's scanf_s wording with JJenkx's end-of-input result and main's new freopen_s entry with its constraint handler calls. At this head guest_formatting, guest_scan_count, guest_wide_formatting, guest_stdio_variadic, guest_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head 29d8de386f: main (3b32cc7b61, with fix(libc): read swscanf scanset ranges and string lengths as titles do #12420) was merged in. Only libc.md met, in the _WStoul/swscanf_s entry: it keeps fix(libc): read swscanf scanset ranges and string lengths as titles do #12420's scanset range, length and %p wording and this PR's measured sscanf_s comparison (64-bit size, first size characters stored without a terminator). At this head guest_formatting, guest_scan_count, guest_wide_formatting, guest_stdio_variadic, guest_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head d00ac4ec91: main (0362b1521c, with feat(libc): implement dprintf and vdprintf #12933) was merged in. Only libc.md met: it keeps feat(libc): implement dprintf and vdprintf #12933's new dprintf/vdprintf entry and this PR's measured snprintf_s/vsnprintf_s entry, with the old vsscanf_s entry still removed. At this head guest_formatting, guest_scan_count, guest_wide_formatting, guest_stdio_variadic, guest_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head 4eaf64fee1: sscanf_s/vsscanf_s take a %c width by its low 32 bits and read one character when it is zero or above INT_MAX, following JJenkx's reference rows (%2147483648c on abc stores one byte with size 2 or 4; %4294967298c reads two). guest_scan_count gains those three rows, which fail on 16a242bbaa. The stream scanner keeps main's path until fix(libc): retain short stream character scan fields #13057 and fix(libc): retain ordinary short character scan fields #13119 land. At this head guest_formatting, guest_scan_count, guest_wide_formatting, guest_stdio_variadic, guest_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head 16a242bbaa: the stream scanner (fscanf_s, vfscanf_s, scanf_s) returns the assignments for a literal mismatch at the end of input, following JJenkx's reference rows. guest_stdio_variadic gains the empty-input "x%2c%n" case (0, errno 97, %n -77 and the 0xa5 destination untouched) and %% at the end of file (EOF); the first check fails on the previous head. The twelve tests listed below pass three runs in a row on Windows (WinLibs GCC) and on Linux (WSL Ubuntu, GCC); check_conventions passes (the existing _WStoul debt-length warning only).
  • Head 3b0fa2fc4d: main (157b02c4f1, with fix(libc): end sscanf and vsscanf at a q length on Linux and macOS #11852) was merged in. Only Formatting.cpp met, in sscanf: it keeps fix(libc): end sscanf and vsscanf at a q length on Linux and macOS #11852's cut at a q length directive (applied to sscanf and vsscanf only, as on main) and this PR's ScanGuest signature with a null secure name. At this head guest_formatting, guest_scan_count, guest_stdio_variadic, guest_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head ec862a495a: %lc with a width above size consumes the whole width, following JJenkx's run of titles' libc.prx (PPSA07923, PPSA05916). guest_scan_count gains CheckWideFields with the eight rows of that table, and its two %lc rows fail on acb1ec494f. At this head guest_formatting, guest_scan_count, guest_stdio_variadic, guest_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head acb1ec494f: main (f80ce6cec1, with docs(libc): record feholdexcept, fegetexcept, setlocale and strcasecmp as measured on a PS5 #11121) was merged in. Only libc.md met: it keeps this PR's snprintf_s/vsnprintf_s and set_constraint_handler_s wording and its removal of the old vsscanf_s entry, and main's localeconv entry with its removal of the setlocale entry. At this head guest_formatting, guest_scan_count, guest_stdio_variadic, guest_strings and guest_safe_errors pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head 0055557985: main (5bdda3661e, with docs(libc): record that libSceLibcInternal does not export memmem or memccpy #10176 and fix(libc): leave errno alone for out-of-range float and double in swscanf #9973) was merged in. Only libc.md met: it keeps this PR's measured sscanf_s/vsscanf_s and _WStoul/swscanf_s entries, main's firmware 13.60 export note on memmem and memccpy and fix(libc): leave errno alone for out-of-range float and double in swscanf #9973's new floating scan errno entry. At this head the twelve tests below pass three runs in a row on Windows (WinLibs GCC 15.2) and on Linux (WSL2, GCC); check_conventions passes (the one debt-length warning on the _WStoul entry, as before).
  • Head 2f05aac2d2: main (5b85225ef3, with feat(libc): add the Annex K stream printf, scanf and freopen_s functions #9527's ScanSource) was merged in 5652af2d1a. This PR's measured sscanf_s/vsscanf_s rules were ported onto ScanSource for StringSource only; StreamSource keeps main's fscanf_s/scanf_s path. 2f05aac2d2 makes feat(libc): add the Annex K stream printf, scanf and freopen_s functions #9527's GuestStdioVariadic checks expect the measured snprintf_s (-23 with a handler call) and undersized sscanf_s %s results instead of the old ones. At this head the twelve tests below pass three runs in a row on Windows and on Linux, and the 49 libc tests matching scan|format|print|string|stdio|safe|constraint|bounds|crt|locale|wide pass on Windows. Before that, head after merging main again (9dbc017c71, with fix(libc): convert scanf integers as FreeBSD does on Windows #9479): efeb582327; only core/libs/tests/Formatting.cpp met, keeping this PR's constraint-handler helpers and measured sscanf_s too-small %s check together with fix(libc): convert scanf integers as FreeBSD does on Windows #9479's CheckIntegerScanWidths and its RunChecks call. At this head the twelve tests below pass three runs in a row on Windows and on Linux (the Linux public_headers audit times out at 30 s on the WSL /mnt/c checkout, unrelated to this change). Before that, main (ac65cf490e, with docs(debt): cite the bundled libc.prx for _Mtx_trylock, strdup and strndup #9613) was merged in b824e3b277; only the strndup and snprintf_s debt lines met, keeping docs(debt): cite the bundled libc.prx for _Mtx_trylock, strdup and strndup #9613's strndup disassembly reading and this PR's measured snprintf_s entry (the old vsscanf_s line stays removed, as its measured sscanf_s entry replaces it). At this head the twelve tests below pass three runs in a row on Windows and on Linux. Before that, main (66e6772ca2, with docs(libc): correct the RSIZE_MAX that swprintf_s and vswprintf_s assume #9360) was merged in 6b9e2f1574; only the snwprintf_s and swprintf_s debt lines met, keeping this PR's snwprintf_s wording and main's swprintf_s limit. Before that, main (f26434afa3, with refactor(libs): add full export headers for libs #10706) was merged in 7b400cbcfe. The tests take their declarations from libc/include/Api.hpp. guest_formatting, guest_scan_count, guest_wide_formatting, guest_wide_stream_scan, guest_stdio_variadic, guest_locale, guest_strings, guest_wide_strings, guest_wide_conversion, guest_safe_errors, guest_crt and guest_bounds_checking pass three runs in a row on Windows (WinLibs GCC) and on Linux (WSL Ubuntu, GCC), which now also builds the #ifndef _WIN32 wide scan path. public_headers audit and check_conventions pass (one debt-length warning on the _WStoul entry, already 3083 characters on main).
    Windows 11, WinLibs GCC 15.2.0, Release: the 30 libc tests whose names cover formatting, scanning, strings, wide characters, streams, locales and bounds checking pass. Linux not built: the #ifndef _WIN32 wide scan path in ScanGuest was changed with the narrow rules and only passed a g++ syntax check, so this PR relies on the Linux CI job to build and run it.

Checklist

orezraey and others added 2 commits October 21, 2026 08:57
…a PS5

Measured on libSceLibcInternal.sprx of a PS5 on firmware 13.60:

- snprintf_s and vsnprintf_s: RSIZE_MAX is 2^61 - 1. A null buffer,
  size 0 or a size above RSIZE_MAX returns -1 with the buffer untouched;
  a null format returns -1 with buffer[0] zeroed; %n or a null %s, %ls
  or %S argument returns -23, keeps the output written before it and
  zeroes buffer[0]. Every violation calls the constraint handler with
  the console's message, a null pointer and EINVAL.
- sscanf_s and vsscanf_s: the size argument is 64 bits. A %s or %[
  field of at least size characters stores its first size characters
  without a terminator and is a matching failure; %c stores at most
  size characters and succeeds. Null destinations and sizes above
  RSIZE_MAX call the handler and return the assignments, or -23 before
  any conversion. A literal mismatch at the end of input returns the
  assignments, and %n counts as a conversion for the EOF result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 7851 docs/dev/debt/libc.md L93-94
PR 9863 docs/dev/debt/libc.md L244
PR 11621 docs/dev/debt/libc.md L96
PR 12530 docs/dev/debt/libc.md L127
PR 12532 docs/dev/debt/libc.md L127
PR 12598 docs/dev/debt/libc.md L96-97

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5021 core/libs/prx/libc/include/WindowsFormatting.hpp
core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 5303 docs/dev/debt/libc.md
PR 6819 docs/dev/debt/libc.md
PR 7447 docs/dev/debt/libc.md
PR 7715 docs/dev/debt/libc.md
PR 8150 docs/dev/debt/libc.md
PR 8965 core/libs/tests/GuestStrings.cpp
docs/dev/debt/libc.md
PR 9371 docs/dev/debt/libc.md
PR 9447 docs/dev/debt/libc.md
PR 9526 docs/dev/debt/libc.md
PR 9929 docs/dev/debt/libc.md
PR 10113 docs/dev/debt/libc.md
PR 10226 docs/dev/debt/libc.md
PR 10245 docs/dev/debt/libc.md
PR 10250 docs/dev/debt/libc.md
PR 10253 docs/dev/debt/libc.md
PR 10265 docs/dev/debt/libc.md
PR 10274 docs/dev/debt/libc.md
PR 10325 docs/dev/debt/libc.md
PR 10328 docs/dev/debt/libc.md
PR 10345 docs/dev/debt/libc.md
PR 10349 docs/dev/debt/libc.md
PR 10405 docs/dev/debt/libc.md
PR 10506 docs/dev/debt/libc.md
PR 10510 docs/dev/debt/libc.md
PR 10512 docs/dev/debt/libc.md
PR 10565 docs/dev/debt/libc.md
PR 10987 docs/dev/debt/libc.md
PR 11260 docs/dev/debt/libc.md
PR 11262 docs/dev/debt/libc.md
PR 11266 docs/dev/debt/libc.md
PR 11632 docs/dev/debt/libc.md
PR 11634 docs/dev/debt/libc.md
PR 11831 docs/dev/debt/libc.md
PR 11907 core/libs/prx/libc/include/WindowsFormatting.hpp
core/libs/prx/libc/src/Formatting.cpp
core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 12122 docs/dev/debt/libc.md
PR 12397 core/libs/prx/libc/include/WindowsFormatting.hpp
core/libs/prx/libc/src/Formatting.cpp
core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 12560 docs/dev/debt/libc.md
PR 12574 docs/dev/debt/libc.md
PR 12593 docs/dev/debt/libc.md
PR 12627 docs/dev/debt/libc.md
PR 12682 docs/dev/debt/libc.md
PR 12688 docs/dev/debt/libc.md
PR 12700 docs/dev/debt/libc.md
PR 12965 docs/dev/debt/libc.md
PR 13390 docs/dev/debt/libc.md
PR 13804 docs/dev/debt/libc.md
PR 14212 docs/dev/debt/libc.md
PR 14585 docs/dev/debt/libc.md
PR 14610 docs/dev/debt/libc.md

@MarcZX10

Copy link
Copy Markdown
Contributor

Overlap coordination for a follow-up to #9321: I have a narrow wide-scanner patch for incomplete-exponent and literal-at-EOF behavior. Its code and tests are separate from this PR's measured formatting constraints, but both branches update the same libc debt bullet, which pr_overlap.py flags. Would you prefer that follow-up to depend on this PR and rebase the debt update after yours merges, or do you have another sequencing preference?

@orezraey

Copy link
Copy Markdown
Contributor Author

Thanks for checking. Making your follow-up depend on this PR and rebasing the debt bullet after it merges works for me. If this one is still waiting when yours is ready, go ahead first and I'll rebase the debt bullet here.

@EagleBearerYS

Copy link
Copy Markdown
Contributor

@orezraey a merge-order note: #9479 (Windows scanf integer conversions) adds a check function after CheckWidePrecision in core/libs/tests/Formatting.cpp and a call at the end of RunChecks, where this PR adds its own, so the two conflict in that test file only. The library changes are in different files: this PR's in Formatting.cpp and WindowsFormatting.hpp, #9479's in WindowsScanning.hpp. MarcZX10's code review on #9479 is resolved, and MarcZX10 asked that the two agree on the order (comment). #9479 proposes going first, and this PR would rebase the test file over it, keeping both check functions and both calls. If you'd rather this one went first, say so and #9479 will list it under Depends on and do the rebase itself.

@orezraey

Copy link
Copy Markdown
Contributor Author

Thanks for coordinating. #9479 going first works for me; once it merges I'll rebase this PR's test file over it, keeping both check functions and both calls in RunChecks.

@JJenkx

JJenkx commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

A data point for the set_constraint_handler_s debt line, from a different module than the one it cites. Your text describes libSceLibcInternal.sprx (firmware 13.60), whose default handler writes the message and a newline, then ends the process.

The libc.prx that PPSA13934 ships in sce_module/ behaves differently: its default handler returns. I read this from disassembly, not by measuring.

  • The default handler is installed at load and when NULL is passed. set_constraint_handler_s (ENLfKJEZTjE, 0x4360) stores handler ? handler : 0x43b0. The slot's initial value is an R_X86_64_RELATIVE relocation with addend 0x43b0. That is neither abort_handler_s (0x43e0) nor ignore_handler_s (0x4410).
  • 0x43b0, the default handler, writes the message and a newline, then returns:
    lea    _Stderr,%rbx          ; H8AprKeZtNg, 0x110480
    mov    %rbx,%rsi
    call   fputs                 ; QrZZdJ8XsX0, 0x44a0 (message in rdi)
    mov    $0xa,%edi
    mov    %rbx,%rsi
    call   fputc                 ; aZK8lNei-Qw, 0x4730
    add    $0x8,%rsp
    pop    %rbx
    pop    %rbp
    ret
    
  • abort_handler_s (0x43e0) makes the same two calls, then calls abort (L1SBTkC+Cvw, 0x48d0).

What this means: sources disagree on whether the default handler ends the process. For a title that installs no handler, this bundled libc.prx writes the line to stderr and lets the _s function return its error. That matches the wcsrtombs_s debt line ("writes the message and a newline to stderr before returning").

main's SonyDefaultConstraintHandler calls std::abort() without writing anything. The copies of PPSA07923 and PPSA05916 have the same default handler: fputs and fputc to _Stderr, then ret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 22, 2026
…les ship

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@orezraey

Copy link
Copy Markdown
Contributor Author

Thanks, that is a useful distinction. In 49903de the set_constraint_handler_s debt entry now records both console modules: libSceLibcInternal.sprx's default handler (measured) writes the message and ends the process, while the libc.prx that PPSA13934, PPSA07923 and PPSA05916 ship writes it to stderr and returns, citing your disassembly here. This PR keeps the current default (abort) so it stays on the measured _s constraints; making the default handler write and return, as the bundled libc.prx this module stands for does, will be a separate PR.

@orezraey

Copy link
Copy Markdown
Contributor Author

@JJenkx Thanks for the matrix. 4eaf64f makes sscanf_s/vsscanf_s take a %c width by its low 32 bits and read one character when that value is zero or above INT_MAX, so %2147483648c on abc stores one byte with size 2 or 4 and %4294967298c reads two. guest_scan_count gains those rows (they fail on 16a242b); the formatting, scan, stdio and safe-error tests pass three times on Windows and Linux. The stream path keeps main's code here; when #13057 and #13119 land I will carry the capacity clamp and this width rule into the shared stream character loop.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 30, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 30, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 30, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Nov 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 2, 2026
JJenkx pushed a commit to JJenkx/AnyPS5 that referenced this pull request Nov 2, 2026
Squash the seven authored boykopovar#9087 prerequisite commits recorded at ec862a4 onto current main, preserving the measured scanner/secure formatter implementation and current-main architecture.

Original commits:
3dea1c1
f8019fd
49903de
3770a87
4c26c0e
2f05aac
ec862a4
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Nov 2, 2026
docs/dev/debt/libc.md: keep this branch's set_constraint_handler_s and swscanf_s text and take main's qsort/qsort_s split and _WStoul sentence (sentence-level 3-way merge, no sentence edited by both sides).
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 2, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Hi orezraey,

#9087 conflicts with main again, this time in core/libs/prx/libc/src/Formatting.cpp, and it can't be merged mechanically. #12468 rewrote the wide scanf conversions (%ls, %lc, %l[) to read through the scanner's source (source.Peek(), byte-based accepts, restoring invalid characters to streams). Your branch's sscanf_s bounds handling (bounded, capacity, the kind == 'c' skip loop and the contiguous secure block) is written against the old field pointer. Could you merge current main and re-apply the secure-variant bounds on top of the new source-based scanner? docs/dev/debt/libc.md conflicts with it on the same entries, and core/libs/tests/Formatting.cpp only needs both sides kept. Thanks.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Nov 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@orezraey

orezraey commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean Done in 24f4242: current main (with #12468) is merged and the sscanf_s bounds now sit on the source scanner. For sscanf_s and vsscanf_s, %ls, %lc and %l[ keep the measured rules: the first size characters are stored without a terminator when they do not fit, and %lc still consumes the rest of its width. fscanf_s and the other stream forms keep main's behaviour. The %lc width now counts bytes, as #12468's loop does, where it counted characters before.

The wide path of the secure block is gone, since those conversions no longer reach it. guest_formatting, guest_scan_count, guest_wide_formatting, guest_stdio_variadic, guest_strings, guest_wide_strings and guest_safe_errors pass three runs in a row on Windows and on Linux (WSL), and check_conventions passes.

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Nov 2, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Hi orezraey,

Thanks for merging main earlier today. Since then #12428 (merged as 954ddf4176) has landed, and #9087 (head 24f4242cbd) conflicts with main again in one place: core/libs/prx/libc/src/Formatting.cpp, in ScanSource, right after the wide-conversion block's final continue;.

Main now has the new unknown-conversion exit there:

if (!std::strchr("diouxXaAeEfFgGcspn[SC", conversion)) return finish(assigned);

and your branch inserts the if constexpr (TSource::Contiguous) if (secure) { ... } block at the same spot. This is not just a matter of keeping both: the order matters. The exit has to come first. Otherwise an unknown conversion in sscanf_s reaches the secure block, which takes a destination with nextPointer() and can throw "Null sscanf_s destination ..." or report a constraint violation instead of ending the scan the way #12428 intends.

Two other #12428 changes merge without conflicts but feed into your secure block, so please check them after the merge:

  • On Windows, HostLength now returns an empty length for s, c and [ unless the length is l.
  • On Windows, specifier for %[ is now built by LibcDetail::TitleScanset, and your block passes it into the host directive.

Length parsing also changed: it now accepts only one length letter, or a doubled hh/ll.

core/libs/tests/Formatting.cpp and docs/dev/debt/libc.md merge without conflicts. Could you merge current main, keep the exit before the secure block and re-run the Formatting tests? Thanks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@orezraey

orezraey commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Merged current main as d826fd16d3, with the unknown-conversion exit before the secure block. #12428's %y, %5y and %k rows through ScanListS depend on that order, and they pass.

  • HostLength: the secure block uses it unchanged, so s, c and [ drop every length except l. The %lls, %hhs and %ll[a-z] rows through ScanListS pass.
  • TitleScanset: the block passes specifier straight into the host directive after its width, the same way as the path below it. The scanset rows through ScanListS pass on Windows.
  • Length parsing: the block only reads length, so it needs no change. The %hls and %llls rows return 0.

The six formatting and scan tests pass three runs in a row on Windows and Linux.

A correction to my last reply: the Linux result I gave for 24f4242cbd wasn't clean, because another checkout changed the shared tree during that build. This Linux run replaces it, and Tested says so.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants