Skip to content

feat(libc): scan 16-bit wide conversions in sscanf and vsscanf on Linux - #6731

Closed
KernelPanicNT wants to merge 1 commit into
boykopovar:mainfrom
KernelPanicNT:feat/libc-sscanf-guest-wide-conversions
Closed

KernelPanicNT wants to merge 1 commit into
boykopovar:mainfrom
KernelPanicNT:feat/libc-sscanf-guest-wide-conversions

Conversation

@KernelPanicNT

@KernelPanicNT KernelPanicNT commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

This replaces #6017, which I closed because it needed a rebase. The code is the same; the branch is now rebased on 5d9dcc4, and the only conflict was the neighbouring collate line in docs/dev/debt/libc.md.

On Linux, sscanf, vsscanf, sscanf_s and vsscanf_s throw for the 16-bit wide conversions %ls, %lc, %l[, %S and %C (RejectGuestWideScan, added in #3984): glibc would store a 32-bit wchar_t per character into the guest's 16-bit buffer. This PR scans them instead.

Source of the behaviour. No console measurement is involved. The model is the FreeBSD 11.4 libc (release/11.4.0, commit 8c63994) in its C locale, which this library's multibyte functions already follow:

  • vfscanf.c L594-L612: %S and %C are %s and %c with the l flag, which they set whatever length modifier precedes them, so %lS and %hC are wide too. L683-L728: with l, %c, %[ and %s call convert_wchar, convert_wccl and convert_wstring (L161-L308). Each reads one character at a time through the locale's mbrtowc (__fgetwc_mbs). %ls stops at iswspace and is terminated. %l[ tests ccltab[wctob(wi)], and an empty match leaves the buffer untouched. %lc reads width characters (default 1) with no terminator.
  • none.c L99-L113: in the C locale mbrtowc consumes one byte and stores its value. So a wide directive reads exactly the bytes the narrow directive reads, and iswspace/wctob agree with isspace and the narrow scanset.
  • This library's mbrtowc (fix(runtime): run converted SDK samples through clean C++ shutdown #98) does the same, and the scanner calls it for every byte.
  • vfscanf.c L804-L805: an input failure returns EOF only when no conversion, suppressed or not, has completed.

That the console's libc.prx scans this way is assumed, not checked: the console's locale and scanner were not read.

Changes:

  • sscanf and vsscanf on Linux send a format with a 16-bit wide conversion to ScanGuest, the per-directive scanner that sscanf_s and vsscanf_s already use (feat: boot PPSA04263 with its own sce_module, into the story mode load (supersedes #144) #153 introduced it, feat(libc): implement vsnprintf_s and vsscanf_s #3046 made vsscanf_s use it). Formats without one still go to glibc unchanged.
  • In ScanGuest, a wide directive is scanned by glibc as its narrow form (%S → %s, %C → %c, %l[ → %[) into a temporary byte buffer. Each byte is then widened through mbrtowc into the guest's buffer. %ls and %l[ are terminated, %lc is not. In the _s forms the capacity counts 16-bit units, and a string that does not fit stores an empty string, as %s already does. The temporary byte buffer is bounded by the remaining input, so a large guest width (%2000000000ls) does not size it.
  • The other directives of such a format follow ScanGuest rather than glibc. The one visible difference: when input fails after only suppressed conversions, the call returns 0, as FreeBSD (L804-L805) and C17 do, where glibc returns EOF. The debt entry records this.
  • The format pre-scan now skips length modifiers before the conversion, so it finds %S and %C after one (%lS, %hC). It also skips the contents of a %l[ scanset, which matters now that the pre-scan continues past the first wide conversion instead of throwing there. ScanGuest scans %S and %C as wide whatever length modifier precedes them, as FreeBSD does (L594-L596, L607-L609). Before, a format whose only wide directive was %lS or %hC went to glibc, which stored a 32-bit wchar_t per character. The pre-scan and ScanGuest also accept the q length modifier, as FreeBSD does (L529-L531, q is long long), so %qS and %qC are wide and %qd or %qn in a format scanned by ScanGuest stores a long long.
  • ScanGuest hands a narrow s, c or [ to the host without any length modifier other than l, so %lls, %qs or %Lc there is narrow, as in FreeBSD, where q, ll, L, j, z and t only change integer and floating conversions. Before, sscanf_s and vsscanf_s passed ll, L, j, z and t on these conversions to glibc, which scans them as wide and wrote a 32-bit wchar_t per character into a buffer whose capacity the guest gave in bytes.

Still throwing, and why:

  • A wide conversion together with a positional argument (%1$ls) or the ' flag throws std::runtime_error, because ScanGuest implements neither.
  • fscanf keeps throwing for wide conversions. It reads a host stream, so it needs a separate source; that is the follow-up, feat(libc): scan 16-bit wide conversions in fscanf on Linux #6732, which is stacked on this PR and should be merged after it.
  • A format with no 16-bit wide conversion still goes to glibc unchanged from sscanf and vsscanf, so there %lls, %Ls, %qs, %js, %zs and %ts (and these modifiers with c or [) are still scanned as wide with a 32-bit wchar_t per character, as on the base. These formats are undefined behaviour in C; the debt entry records them.
  • On Windows %ls, %lc and %l[ still go to the host, whose wchar_t is 16 bits. The only Windows change is in ScanGuest, which sscanf_s and vsscanf_s use there: q is handed to the host as ll for integer conversions, and s, c and [ lose any length modifier other than l. %S and %C still throw in sscanf, vsscanf and fscanf there, in the format translation.

Tests: on Linux, guest_formatting no longer expects these formats to throw. It now checks %ls, %S, %3lc after a suppressed %*s, %C, %l[a-z], %lS, %hC, a width, bytes above 0x7f (\xe9, \xff → U+00E9, U+00FF), a mix with %d, a suppressed %*ls, vsscanf, empty and blank input (EOF), an empty %l[ match (0, buffer untouched), and the _s capacity (fit, %lc with capacity 1, overflow → empty string). It also checks %qS, %qC, a width of 2000000000 on short input for %ls and %lc, %ls %qd%qn (two conversions, a long long and a long long count), the variadic sscanf_s with %ls %C, and %qs %ll[a-z] %Lc through sscanf_s, which must store narrow bytes. Guard units after every result must stay untouched. %2$C, %'ls and %1$ls still throw without writing, and %1$ls is checked through sscanf, vsscanf, vsscanf_s and sscanf_s.

Progress (tools/progress.py on the branch base 5d9dcc4 and on the branch head): system libraries 3299/3706 → 3300/3706 (+vsscanf_nid_postfix). The tool counted vsscanf as unimplemented because its single definition reached NotImplemented_nid_no_patch on Linux. sscanf, sscanf_s and vsscanf_s already counted as implemented through their Windows definitions. vsscanf now counts as implemented because the one case still unimplemented on Linux, a wide conversion with a positional argument or the ' flag, throws std::runtime_error rather than calling NotImplemented_nid_no_patch.

Tested

Head 29cfd9985511d8e779d52d6f6dc6612343ce88c1, base 5d9dcc42d2ec5401aebd089028b8d4d7410a77b4.

  • Linux (Ubuntu 24.04, GCC 14.2.0, Ninja, Release, BUILD_TESTING=ON, AMD EPYC 7763 (GitHub Codespaces), tests run natively): guest_formatting_tests, guest_scan_count_tests and guest_string_tests each pass in 5 of 5 runs, the test executable being run directly five times.
  • Negative control: keep the tests and replace only core/libs/prx/libc/src/Formatting.cpp with the base version: guest_formatting_tests fails with scanf 16-bit wide conversion on Linux not implemented; guest_scan_count_tests, guest_string_tests still pass there, as expected for regression coverage. Restoring the PR source and rebuilding, guest_formatting_tests, guest_scan_count_tests and guest_string_tests each pass in 5 of 5 runs again.
  • Windows, full Build workflow on my fork (run 38867947343), this branch and its stacked counterpart merged into the fork's main at 5d9dcc42d2ec5401aebd089028b8d4d7410a77b4, WinLibs MinGW-w64 GCC 15.2.0, Release, BUILD_TESTING=ON: 1300/1300 tests pass; the relinker (Linux, macOS), register-context and write-watch jobs of the same run also pass.
  • Conventions checker and git diff --check pass. The Linux job of the Build workflow now targets the project's self-hosted runner, so it was not run on the fork; no title was run.

Duplicate and overlap check

Checklist

  • Based on 5d9dcc4; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes (Claude)

On Linux, sscanf, vsscanf, sscanf_s and vsscanf_s threw for %ls, %lc,
%l[, %S and %C because glibc stores a 32-bit wchar_t per character into
the guest's 16-bit buffer. A format with one of these conversions is now
scanned directive by directive in ScanGuest: each wide directive reads
its bytes as the narrow directive does and widens every byte through
this library's mbrtowc, which is what FreeBSD 11.4's vfscanf.c reads in
the single-byte C locale of none.c. The byte buffer for a wide directive
is bounded by the remaining input, not only by the guest's width.

As in FreeBSD, %S and %C are wide whatever length modifier precedes
them. The format pre-scan and ScanGuest recognise the q length modifier
as FreeBSD does, so %lS, %hC and %qS are found too instead of reaching
glibc, and %qd or %qn in a format scanned by ScanGuest stores a long
long. On Windows, ScanGuest hands q to the host as ll for integer
conversions. A narrow s, c or [ is handed to the host without any length
modifier other than l, so %lls, %qs or %Lc in a format scanned by
ScanGuest are narrow, as in FreeBSD, instead of reaching glibc as wide.

A wide conversion combined with a positional argument or the ' flag
still throws. fscanf keeps throwing for wide conversions.

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

Copy link
Copy Markdown

Progress report (5d9dcc4 → 29cfd99)

📈 System libraries: 88.86% (+0.03%, +1 implemented functions)

✅ 1 implemented
Library Name
libc vsscanf_nid_postfix

@MarcZX10

Copy link
Copy Markdown
Contributor

I inspected the failed write-watch (macos-15-intel) job log: the build succeeded and 8/9 tests passed; only write_watch_threads timed out on its second --repeat until-fail:3 run at 30 seconds. This PR does not touch the write-watch target or test source, so please rerun or compare that check against main before treating it as a source regression.

@MarcZX10

Copy link
Copy Markdown
Contributor

Follow-up evidence: #6732 contains the #6731 scanner changes and its write-watch (macos-15-intel) check passed in 44 seconds (run 38870715248). That supports the timeout being intermittent rather than caused by the scanner diff.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

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 3965 docs/dev/debt/libc.md
PR 3987 core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 4627 core/libs/tests/Formatting.cpp
PR 5021 core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 5303 docs/dev/debt/libc.md
PR 5840 docs/dev/debt/libc.md
PR 6044 core/libs/tests/Formatting.cpp
PR 6061 docs/dev/debt/libc.md
PR 6107 docs/dev/debt/libc.md
PR 6124 docs/dev/debt/libc.md
PR 6144 docs/dev/debt/libc.md
PR 6217 docs/dev/debt/libc.md
PR 6222 core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 6226 docs/dev/debt/libc.md
PR 6315 core/libs/prx/libc/src/Formatting.cpp
core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 6322 docs/dev/debt/libc.md
PR 6330 docs/dev/debt/libc.md
PR 6331 docs/dev/debt/libc.md
PR 6334 docs/dev/debt/libc.md
PR 6339 docs/dev/debt/libc.md
PR 6345 docs/dev/debt/libc.md
PR 6447 core/libs/prx/libc/src/Formatting.cpp
PR 6469 docs/dev/debt/libc.md
PR 6483 core/libs/prx/libc/src/Formatting.cpp
core/libs/tests/Formatting.cpp
docs/dev/debt/libc.md
PR 6541 docs/dev/debt/libc.md
PR 6565 docs/dev/debt/libc.md
PR 6610 docs/dev/debt/libc.md
PR 6686 docs/dev/debt/libc.md
PR 6707 docs/dev/debt/libc.md
PR 6713 docs/dev/debt/libc.md
PR 6819 docs/dev/debt/libc.md
PR 6842 docs/dev/debt/libc.md
PR 6916 docs/dev/debt/libc.md
PR 6975 docs/dev/debt/libc.md
PR 6985 docs/dev/debt/libc.md
PR 7039 docs/dev/debt/libc.md
PR 7081 docs/dev/debt/libc.md
PR 7201 core/libs/tests/Formatting.cpp
PR 7280 docs/dev/debt/libc.md
PR 7282 docs/dev/debt/libc.md

@MarcZX10

Copy link
Copy Markdown
Contributor

I checked the current failed check: write-watch (macos-15-intel) is the only failure, and the report on this PR shows write_watch_threads timing out during its repeated run. This change only touches the libc scanner and its tests, not the write-watch target. The same check passed on the stacked #6732 workflow, so this looks intermittent rather than a scanner regression; a rerun would confirm the current head.

@blawesom

Copy link
Copy Markdown
Contributor

Pairing note for you and #6732: the two PRs are complementary halves of one change — this one covers sscanf/vsscanf and #6732 covers fscanf — but they both carry a byte-identical base refactor of Formatting.cpp (FindGuestWideScan/WidenGuestScan), so they will conflict with each other on merge. Whichever lands second should rebase onto the first rather than onto main directly; flagging both so the sequencing is a deliberate choice.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for flagging the shared refactor with #6732. These are complementary halves, so I’ll coordinate the merge order and rebase the second PR onto the first after the first lands.

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

Labels

docs libc needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants