Skip to content

feat(libc): scan 16-bit wide conversions in fscanf on Linux - #6732

Closed
KernelPanicNT wants to merge 2 commits into
boykopovar:mainfrom
KernelPanicNT:feat/libc-fscanf-guest-wide-conversions
Closed

KernelPanicNT wants to merge 2 commits into
boykopovar:mainfrom
KernelPanicNT:feat/libc-fscanf-guest-wide-conversions

Conversation

@KernelPanicNT

@KernelPanicNT KernelPanicNT commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

This replaces #6018, which I closed because it needed a rebase. The code is the same; it is now stacked on #6731, which replaces #6017, and both are rebased on 5d9dcc4. The only conflicts were the neighbouring collate line in docs/dev/debt/libc.md and the new CheckClosedStreamWrites call of #5632 in main() of core/libs/tests/GuestStandardStreams.cpp, where both calls are kept.

Review note. This branch is stacked on #6731, so the Files changed tab also shows that PR's commit. This PR is only the second commit, ccc10a4; please merge #6731 first, after which this diff shrinks to that commit. The progress report's +2 counts vsscanf from #6731 and fscanf from this PR.

Depends on #6731, which adds 16-bit wide scanning to sscanf, vsscanf, sscanf_s and vsscanf_s on Linux. This PR is the single commit on top of it. On Linux, fscanf still throws for %ls, %lc, %l[, %S and %C (RejectGuestWideScan, called from fscanf), because glibc would store a 32-bit wchar_t per character into the guest's 16-bit buffer. This PR scans them from the stream on glibc hosts.

Source of the behaviour. No console measurement is involved. The model is the same as in the PR this depends on, now read from a stream:

  • FreeBSD 11.4 (release/11.4.0, commit 8c63994) vfscanf.c L161-L308 and L683-L728: with l, %c, %[ and %s read the stream one character at a time through the locale's mbrtowc. %S/%C are %s/%c with l (L594-L612). FreeBSD's vsscanf runs the same __svfscanf on a string FILE (vsscanf.c L61-L71), so fscanf and sscanf share these semantics.
  • none.c L99-L113: the C-locale mbrtowc maps one byte to its value, as this library's mbrtowc does.
  • The stream side uses the m assignment-allocation character of POSIX.1-2008 fscanf (added in Issue 7, SD5-XSH-ERN-132; accepted by glibc's __isoc99_vfscanf). The number of bytes a stream directive reads is not known in advance, and m makes glibc allocate exactly that many. This is host plumbing only; the guest sees no allocation. Apple's libc has no m (vfscanf.c handles j, l, q, t, z, L and h only), so the stream path is built only where __GLIBC__ is defined. The string functions do not use m.

Changes:

  • The body of ScanGuest moves into ScanGuestSource, a template over an input source. ScanGuest now wraps it with a StringScanSource, which holds the existing string cursor and, as in the PR this depends on, scans a wide directive into a buffer bounded by the remaining input. Its callers (Linux sscanf/vsscanf for wide formats, sscanf_s/vsscanf_s on both platforms) keep their behaviour on every host.
    • StreamScanSource (glibc hosts only) reads a host FILE* and holds its lock (flockfile, released by the destructor, also when the scanner throws) for the whole fscanf call, so another guest thread cannot read the same stream between the per-directive calls. Literals and whitespace are matched with one getc/ungetc pair per peeked byte. Each conversion other than %n is one glibc fscanf call with a trailing %n, which counts the bytes the call consumed. A guest %n is filled from the scanner's own count, without a glibc call.
  • On glibc hosts, fscanf sends a format with a 16-bit wide conversion to ScanGuestSource over the stream. Formats without one still go to glibc's vfscanf unchanged, and SyncStatus still runs after either path. On a non-Windows host without glibc (macOS), a format with a wide conversion throws std::runtime_error before reading the stream; other formats go to the host vfscanf as before.
  • On the stream, a wide directive becomes %<width>m<narrow conversion>. glibc allocates the bytes, which are widened through mbrtowc as in the string case and then freed. Every byte the conversion stored is widened, counted by the trailing %n, so a NUL byte that %ls or %l[ reads from the stream becomes a 0 unit and the bytes after it are kept, as FreeBSD's convert_wstring and convert_wccl store L'\0' and continue. %ls and %l[ are terminated, %lc is not. The q handling and the narrow s, c and [ of the PR this depends on apply to the stream too: %qS is wide and %qs narrow.
  • RejectGuestWideScan had no other caller and is removed.

Still throwing, and why:

  • A wide conversion with a positional argument (%1$ls) or the ' flag throws std::runtime_error before reading the stream, as in the PR this depends on.
  • An invalid length on %n (%ls%Ln, std::invalid_argument) throws only when the scanner reaches that directive. By then fscanf has consumed the stream up to it, and the throw skips va_end and SyncStatus. This is recorded in docs/dev/debt/libc.md.
  • On a non-Windows host without glibc, any 16-bit wide conversion in fscanf throws std::runtime_error before reading the stream, because the host has no m. The previous throw was NotImplemented_nid_no_patch on every non-Windows host.
  • Windows is unchanged: fscanf translates the format for the host, whose wchar_t is 16 bits. %S and %C still throw in that translation.

Tests: guest_standard_streams gains CheckWideScan on glibc hosts, run on tmpfile streams. Each case also checks what is left in the stream through fgetc:

  • %ls%n %3lc%d on ab cdef 12: 2 assignments, %n is 2, %d fails on f, and f is left.
  • %C+%lc on a-b: 1 assignment, and - is left.
  • %l[a-z] on 12: returns 0, the buffer is untouched, and 1 is left.
  • %S on bytes \xe9t\xff : U+00E9, t, U+00FF, and the space is left.
  • A 300-byte word: read in full, terminated, and the following space is left.
  • An empty stream: EOF, with the guest EOF flag set, and fgetc returns EOF.
  • %*ls%d on ab: returns 0, and fgetc returns EOF.
  • %1$ls: throws with the buffer and the stream untouched, and a is left.
  • %l[^\n] on the bytes a, NUL, b, newline: 1 assignment, the units a, 0, b, 0, the guard units after them untouched, and the newline is left.
  • %qS on xy z: 1 assignment, x, y, 0, and the space is left.

On other non-Windows hosts, CheckWideScan checks that %ls on ab throws std::runtime_error with the buffer untouched and a left.

Progress (tools/progress.py, base 5d9dcc4 plus the PR this depends on): system libraries 3300/3706 → 3301/3706 (+fscanf_nid_postfix). The tool counted fscanf as unimplemented because its single definition reached NotImplemented_nid_no_patch on Linux.

Tested

Head ccc10a467756c30cc426e76f9e8db2a1622bb84c, base 29cfd9985511d8e779d52d6f6dc6612343ce88c1.

  • Linux (Ubuntu 24.04, GCC 14.2.0, Ninja, Release, BUILD_TESTING=ON, AMD EPYC 7763 (GitHub Codespaces), tests run natively): guest_standard_stream_tests and guest_formatting_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_standard_stream_tests fails with fscanf 16-bit wide conversion on Linux not implemented; guest_formatting_tests still passes there, as expected for regression coverage. Restoring the PR source and rebuilding, guest_standard_stream_tests and guest_formatting_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

KernelPanicNT and others added 2 commits October 16, 2026 07:31
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>
fscanf threw for %ls, %lc, %l[, %S and %C on Linux. The body of
ScanGuest moves into ScanGuestSource, which reads from a source: a
string, as before through ScanGuest, or a stream, where each conversion
other than %n is one glibc fscanf call and the scanner peeks with
getc/ungetc under the stream's lock. fscanf uses the stream source for
formats with a wide conversion, so a wide directive reads its bytes as
the narrow one and widens them through this library's mbrtowc, as
sscanf already does.

On the stream, wide directives take their bytes with the POSIX.1-2008
m assignment-allocation modifier, since a stream's string length is not
known before it is read. The string source keeps its buffer bounded by
the remaining input. Every byte the stream conversion stored is
widened, so a NUL byte read by %ls or %l[ is kept as a 0 unit, as
FreeBSD's convert_wstring and convert_wccl store it. m needs glibc, so
on other non-Windows hosts fscanf throws std::runtime_error for a wide
conversion.

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

Copy link
Copy Markdown

Progress report (5d9dcc4 → ccc10a4)

📈 System libraries: 88.88% (+0.05%, +2 implemented functions)

✅ 2 implemented
Library Name
libc fscanf_nid_postfix
libc vsscanf_nid_postfix

@MarcZX10

Copy link
Copy Markdown
Contributor

I checked the current PR bases and file lists: #6732 targets main and changes the same Formatting.cpp, Formatting.cpp test, and libc debt file as #6731; its current diff therefore includes the string-scan implementation as well as the stream changes. Since the description says this is stacked on #6731, please ensure #6731 is integrated first and rebase/reduce this PR to only the fscanf delta before both are merged, so the string-scan changes are not duplicated or conflicted.

@MarcZX10

Copy link
Copy Markdown
Contributor

One relevant intake context: the earlier PRs for these same wide fscanf conversions (#5291 and #5285) were closed on Oct 14 with the maintainer request to hold changes until a title is known to need them, and to reopen with the title ID and log line. This PR currently lists Unblocks: none and does not identify a title. Has new current-main title evidence emerged since those closures? If so, could you add the title ID and stop/log line; otherwise it may be best to check with maintainers before spending more review time on the same change.

@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 5668 core/libs/tests/GuestStandardStreams.cpp
PR 5840 docs/dev/debt/libc.md
PR 6044 core/libs/tests/Formatting.cpp
core/libs/tests/GuestStandardStreams.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 core/libs/tests/GuestStandardStreams.cpp
docs/dev/debt/libc.md

@blawesom

Copy link
Copy Markdown
Contributor

Pairing note for you and #6731: the two PRs are complementary halves of one change — this one covers fscanf and #6731 covers sscanf/vsscanf — 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 FindGuestWideScan/WidenGuestScan refactor. I have confirmed these are complementary halves; I’ll keep the merge order coordinated and rebase the second PR onto the first after the first lands so the common refactor is not applied twice.

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