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>
What
On Linux,
sscanf,vsscanf,sscanf_sandvsscanf_sthrow for the 16-bit wide conversions%ls,%lc,%l[,%Sand%C(RejectGuestWideScan, added in #3984): glibc would store a 32-bitwchar_tper 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, commit8c63994) in its C locale, which this library's multibyte functions already follow:vfscanf.cL594-L612:%Sand%Care%sand%cwith thelflag, which they set whatever length modifier precedes them, so%lSand%hCare wide too. L683-L728: withl,%c,%[and%scallconvert_wchar,convert_wcclandconvert_wstring(L161-L308). Each reads one character at a time through the locale'smbrtowc(__fgetwc_mbs).%lsstops atiswspaceand is terminated.%l[testsccltab[wctob(wi)], and an empty match leaves the buffer untouched.%lcreadswidthcharacters (default 1) with no terminator.none.cL99-L113: in the C localembrtowcconsumes one byte and stores its value. So a wide directive reads exactly the bytes the narrow directive reads, andiswspace/wctobagree withisspaceand the narrow scanset.mbrtowc(fix(runtime): run converted SDK samples through clean C++ shutdown #98) does the same, and the scanner calls it for every byte.vfscanf.cL804-L805: an input failure returnsEOFonly when no conversion, suppressed or not, has completed.That the console's
libc.prxscans this way is assumed, not checked: the console's locale and scanner were not read.Changes:
sscanfandvsscanfon Linux send a format with a 16-bit wide conversion toScanGuest, the per-directive scanner thatsscanf_sandvsscanf_salready 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 madevsscanf_suse it). Formats without one still go to glibc unchanged.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 throughmbrtowcinto the guest's buffer.%lsand%l[are terminated,%lcis not. In the_sforms the capacity counts 16-bit units, and a string that does not fit stores an empty string, as%salready does. The temporary byte buffer is bounded by the remaining input, so a large guest width (%2000000000ls) does not size it.ScanGuestrather 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 returnsEOF. The debt entry records this.%Sand%Cafter 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.ScanGuestscans%Sand%Cas wide whatever length modifier precedes them, as FreeBSD does (L594-L596, L607-L609). Before, a format whose only wide directive was%lSor%hCwent to glibc, which stored a 32-bitwchar_tper character. The pre-scan andScanGuestalso accept theqlength modifier, as FreeBSD does (L529-L531,qislong long), so%qSand%qCare wide and%qdor%qnin a format scanned byScanGueststores along long.ScanGuesthands a narrows,cor[to the host without any length modifier other thanl, so%lls,%qsor%Lcthere is narrow, as in FreeBSD, whereq,ll,L,j,zandtonly change integer and floating conversions. Before,sscanf_sandvsscanf_spassedll,L,j,zandton these conversions to glibc, which scans them as wide and wrote a 32-bitwchar_tper character into a buffer whose capacity the guest gave in bytes.Still throwing, and why:
%1$ls) or the'flag throwsstd::runtime_error, becauseScanGuestimplements neither.fscanfkeeps 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.sscanfandvsscanf, so there%lls,%Ls,%qs,%js,%zsand%ts(and these modifiers withcor[) are still scanned as wide with a 32-bitwchar_tper character, as on the base. These formats are undefined behaviour in C; the debt entry records them.%ls,%lcand%l[still go to the host, whosewchar_tis 16 bits. The only Windows change is inScanGuest, whichsscanf_sandvsscanf_suse there:qis handed to the host asllfor integer conversions, ands,cand[lose any length modifier other thanl.%Sand%Cstill throw insscanf,vsscanfandfscanfthere, in the format translation.Tests: on Linux,
guest_formattingno longer expects these formats to throw. It now checks%ls,%S,%3lcafter 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_scapacity (fit,%lcwith capacity 1, overflow → empty string). It also checks%qS,%qC, a width of 2000000000 on short input for%lsand%lc,%ls %qd%qn(two conversions, along longand along longcount), the variadicsscanf_swith%ls %C, and%qs %ll[a-z] %Lcthroughsscanf_s, which must store narrow bytes. Guard units after every result must stay untouched.%2$C,%'lsand%1$lsstill throw without writing, and%1$lsis checked throughsscanf,vsscanf,vsscanf_sandsscanf_s.Progress (
tools/progress.pyon the branch base5d9dcc4and on the branch head): system libraries 3299/3706 → 3300/3706 (+vsscanf_nid_postfix). The tool countedvsscanfas unimplemented because its single definition reachedNotImplemented_nid_no_patchon Linux.sscanf,sscanf_sandvsscanf_salready counted as implemented through their Windows definitions.vsscanfnow counts as implemented because the one case still unimplemented on Linux, a wide conversion with a positional argument or the'flag, throwsstd::runtime_errorrather than callingNotImplemented_nid_no_patch.Tested
Head
29cfd9985511d8e779d52d6f6dc6612343ce88c1, base5d9dcc42d2ec5401aebd089028b8d4d7410a77b4.BUILD_TESTING=ON, AMD EPYC 7763 (GitHub Codespaces), tests run natively):guest_formatting_tests,guest_scan_count_testsandguest_string_testseach pass in 5 of 5 runs, the test executable being run directly five times.core/libs/prx/libc/src/Formatting.cppwith the base version:guest_formatting_testsfails withscanf 16-bit wide conversion on Linux not implemented;guest_scan_count_tests,guest_string_testsstill pass there, as expected for regression coverage. Restoring the PR source and rebuilding,guest_formatting_tests,guest_scan_count_testsandguest_string_testseach pass in 5 of 5 runs again.mainat5d9dcc42d2ec5401aebd089028b8d4d7410a77b4, 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.git diff --checkpass. 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
ScanGuestplus per-byte widening. The design overlaps with that PR; credit to @Gashrod1. feat(libc): scan 16-bit wide conversions on Linux #5285 by @Nixitov (closed 2026-10-14) decoded UTF-8 into UTF-16 instead, which does not match the C locale ofnone.c. feat(libc): scan 16-bit wide conversions in fscanf on Linux #5291 by @Nixitov (closed 2026-10-14) was an earlier attempt at thefscanffollow-up and changed the sameRejectGuestWideScanandfscanflines as this PR. fix(libc): retain suppressed scanf conversions before EOF #4966 by @Dripl1x (closed 2026-10-14) made the same change to return 0 when input fails after only suppressed conversions, at these call sites; credit to @Dripl1x for that part. A maintainer closed all four with the same comment: "Closing: no title is known to reach this. With the queue at 700+ open PRs, please hold off on new PRs that don't name a title needing the change until it's down." It asks to reopen with the title ID and the log line once a title stops on the change. This PR names no title either (see Unblocks). fix(libc): throw for 16-bit wide scanf conversions on Linux #3984 by @zmeizy introduced the throw this PR replaces.sscanf,vsscanf,sscanf_sandvsscanf_s, their NIDs (1Pk0qZQGeWo,UTrpOVLcoOA,24m4Z4bUaoY,tfNbpqL3D0M) and "wide scanf". No open PR implements wide scanning.Formatting.cpp,core/libs/tests/Formatting.cppordocs/dev/debt/libc.mdinto this branch (git merge --no-commit) and intomain, and read the diffs that touch the scanf code.RejectGuestWideScansetserrnotoENOTSUPand the scanf functions returnEOFfor a wide conversion, instead of scanning it. It conflicts with this PR inFormatting.cpp. If it is merged first, this PR replaces that early return.#include <cstdio>incore/libs/tests/Formatting.cpp, where this PR adds#include <string_view>.collateline ofdocs/dev/debt/libc.md, directly above the scanf line this PR rewrites.main; the first two do so indocs/dev/debt/libc.md.Formatting.cppand its tests), and 24 PRs that change other lines ofdocs/dev/debt/libc.md.mainmerged fix(libc): fail stream calls on a stream that a failed freopen closed #5632 (a closed-stream check at the top offscanfand the printf functions), fix(libc): check allocation result in vasprintf and guard null pointer in strndup #5709, fix(libc): ignore the space and + flags for unsigned printf conversions #5774 and feat(libc): implement wide and unlocked stream I/O #5816 in these files. The rebase keeps them unchanged.5d9dcc4and merges into5d9dcc4without conflicts.Checklist
5d9dcc4; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)