Skip to content

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

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

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

Conversation

@KernelPanicNT

Copy link
Copy Markdown
Owner

What

Depends on #14, 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.

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 host 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.

Changes:

  • ScanGuest becomes ScanGuestSource, a template over an input source:
    • StringScanSource holds the existing string cursor. The sscanf family on both platforms (including the Windows _s forms) goes through it with unchanged behaviour.
    • StreamScanSource (Linux only) reads a host FILE*. Literals and whitespace are matched with one getc/ungetc pair per peeked byte. Each conversion is one glibc fscanf call with a trailing %n. %n counts the bytes the call consumed.
  • On Linux, 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.
  • 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. %ls and %l[ are terminated, %lc is not.
  • 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.
  • 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 Linux, run on tmpfile streams. Each case also checks the next byte 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.
  • A 300-byte word: read in full, terminated, and the following space is left.
  • An empty stream: EOF, with the guest EOF flag set.
  • %*ls%d on ab: returns 0.
  • %1$ls: throws with the buffer and the stream untouched.

Progress (tools/progress.py, base 8ffbb49 plus the PR this depends on): system libraries 3244/3671 → 3245/3671 (+fscanf_nid_postfix). The tool counted fscanf as unimplemented because its single definition reached NotImplemented_nid_no_patch on Linux.

Tested

Head c4a894ac4cf5df8aa164b647e1cae8442196cc35, base 3a94783e170a1797f6074c5fa9128c77050f437f.

  • Linux (Ubuntu 22.04, GCC 12.3.0, Ninja, Release, BUILD_TESTING=ON, tests run under QEMU 8.2 user mode with -cpu EPYC-Rome because the build host lacks AVX2): guest_standard_stream_tests 5/5, guest_formatting_tests 5/5 passes.
  • 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 pass there, as expected for regression coverage. Restoring the PR source and rebuilding passes guest_standard_stream_tests 5/5, guest_formatting_tests 5/5.
  • Windows: fork Build workflow pending; result added here when it finishes.
  • 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 current main; 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: feat(libc): scan 16-bit wide conversions in sscanf and vsscanf on Linux #14
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes (Claude)

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Progress report (b08db6a → e32dea4)

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

✅ 2 implemented
Library Name
libc fscanf_nid_postfix
libc vsscanf_nid_postfix

@KernelPanicNT
KernelPanicNT force-pushed the feat/libc-fscanf-guest-wide-conversions branch 2 times, most recently from 5c33c30 to cdf9322 Compare October 14, 2026 18:47
KernelPanicNT and others added 2 commits October 15, 2026 11:41
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>
@KernelPanicNT
KernelPanicNT force-pushed the feat/libc-fscanf-guest-wide-conversions branch from cdf9322 to e32dea4 Compare October 15, 2026 13:16
@KernelPanicNT

Copy link
Copy Markdown
Owner Author

Superseded: published upstream; closing the CI draft.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant