From 89bc9d10821bf4260f20119f94652012ea6e6bcd Mon Sep 17 00:00:00 2001 From: milescoviello Date: Mon, 12 Oct 2026 02:35:47 +0000 Subject: [PATCH] fix(libc): read %i without the C23 binary prefix in the scanf family on Linux On Linux sscanf, vsscanf, fscanf and sscanf_s hand their conversions to glibc. Since glibc 2.38, code built as GNU C++ calls the C23 entry points (__isoc23_vsscanf, __isoc23_vfscanf, __isoc23_sscanf), whose %i reads a 0b/0B binary prefix. On main, sscanf("0b101 x", "%i%n%7s", &value, &end, rest) stores value 5 and end 5 and leaves "x" for the %s; " -0B11" gives -3. TechnicalDebt records this. FreeBSD's %i knows only the 0 and 0x prefixes. FreeBSD 11.4 lib/libc/stdio/vfscanf.c, parseint, lines 345-353 and 371-380: case '0': if (base == 0) { base = 8; flags |= PFXOK; } ... /* letters ok iff hex */ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': /* no need to fix base here */ if (base <= 10) break; /* not legal here */ so "0b101" stores 0, consumes the "0" and leaves "b101", as C17 does and as the strtol family here already does. With glibc the scanf family now calls __isoc99_vsscanf and __isoc99_vfscanf, the entry points GNU C++ used before glibc 2.38 (exported since 2.7), which read %i without the binary prefix. Other hosts keep std::vsscanf and std::vfscanf. The TechnicalDebt entry is removed. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Formatting.cpp | 41 +++++++++++++++++++++--- core/libs/tests/Formatting.cpp | 18 +++++++++++ core/libs/tests/GuestStandardStreams.cpp | 8 +++++ docs/dev/TechnicalDebt.md | 1 - 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 7982d342a..de05ddae0 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -21,6 +21,13 @@ #include "prx/libc/include/WindowsScanning.hpp" #endif +#if defined(__GLIBC__) +extern "C" { +int __isoc99_vsscanf(const char*, const char*, std::va_list) noexcept; +int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); +} +#endif + namespace { #ifndef _WIN32 @@ -64,6 +71,32 @@ std::string HostLength(const std::string& length, char conversion) { return length; } +int HostVsscanf(const char* input, const char* format, std::va_list args) { +#if defined(__GLIBC__) + return __isoc99_vsscanf(input, format, args); +#else + return std::vsscanf(input, format, args); +#endif +} + +int HostSscanf(const char* input, const char* format, ...) { + std::va_list args; + va_start(args, format); + const int result = HostVsscanf(input, format, args); + va_end(args); + return result; +} + +#ifndef _WIN32 +int HostVfscanf(std::FILE* stream, const char* format, std::va_list args) { +#if defined(__GLIBC__) + return __isoc99_vfscanf(stream, format, args); +#else + return std::vfscanf(stream, format, args); +#endif +} +#endif + template int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { if (buffer == nullptr || format == nullptr) return EOF; @@ -147,7 +180,7 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n directive += specifier; directive += "%n"; int consumed = -1; - const int matched = suppress ? std::sscanf(input, directive.c_str(), &consumed) : std::sscanf(input, directive.c_str(), target, &consumed); + const int matched = suppress ? HostSscanf(input, directive.c_str(), &consumed) : HostSscanf(input, directive.c_str(), target, &consumed); if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && *input == '\0' ? EOF : assigned); if (capped && conversion != 'c') { const char next = input[consumed]; @@ -257,7 +290,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = std::vfscanf(native, format, args); + const int result = HostVfscanf(native, format, args); va_end(args); #endif stream->SyncStatus(); @@ -341,7 +374,7 @@ int APS5_VABI vsscanf_nid_postfix(const char* input, const char* format, VaList* RejectGuestWideScan(format); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); - const int result = std::vsscanf(input, format, copy); + const int result = HostVsscanf(input, format, copy); va_end(copy); return result; #endif @@ -360,7 +393,7 @@ int APS5_VABI sscanf_nid_postfix(const char* input, const char* format, ...) { RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = std::vsscanf(input, format, args); + const int result = HostVsscanf(input, format, args); va_end(args); return result; } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index b841dece1..5072df969 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -18,6 +18,7 @@ int APS5_VABI sscanf_nid_postfix(const char*, const char*, ...); int APS5_VABI vsnprintf_nid_postfix(char*, size_t, const char*, VaList*); int APS5_VABI vsnprintf_s_nid_postfix(char*, size_t, const char*, VaList*); int APS5_VABI vsscanf_s_nid_postfix(const char*, const char*, VaList*); +int APS5_VABI vsscanf_nid_postfix(const char*, const char*, VaList*); int APS5_VABI vprintf_nid_postfix(const char*, VaList*); int APS5_VABI asprintf_nid_postfix(char**, const char*, ...); void APS5_VABI free_nid_postfix(void*); @@ -49,6 +50,16 @@ static int APS5_VABI FormatListS(char* buffer, size_t size, const char* format, return result; } +static int APS5_VABI ScanList(const char* buffer, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + VaList list; + std::memcpy(&list, args, sizeof(list)); + const int result = vsscanf_nid_postfix(buffer, format, &list); + __builtin_sysv_va_end(args); + return result; +} + static int APS5_VABI ScanListS(const char* buffer, const char* format, ...) { __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -199,6 +210,13 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { &first, &second, &third, &fourth, &fifth, &sixth, word, &real) == 8); Require(first == 1 && second == 2 && third == 3 && fourth == 4 && fifth == 5 && sixth == 6); Require(std::strcmp(word, "seven") == 0 && real == 8.5); + int binary = -1, binaryEnd = -1; + char binaryRest[8] = {}; + Require(sscanf_nid_postfix("0b101", "%i%n%7s", &binary, &binaryEnd, binaryRest) == 2); + Require(binary == 0 && binaryEnd == 1 && std::strcmp(binaryRest, "b101") == 0); + Require(ScanList(" -0B11", "%i", &binary) == 1 && binary == 0); + Require(ScanListS("0b1", "%i", &binary) == 1 && binary == 0); + Require(sscanf_nid_postfix("0x1f", "%i", &binary) == 1 && binary == 31); #ifdef _WIN32 int scanned = -1; int position = -1; diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 3173ea8d9..9c483d89b 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -445,6 +445,14 @@ int main() { Require(feof_nid_postfix(&scanned) && (scanned.GuestState().flags & 0x20)); scanned.Close(); + FileStream scanBinary(std::tmpfile()); + Require(std::fputs("0b101", scanBinary.GetHandle()) >= 0); + std::rewind(scanBinary.GetHandle()); + int binaryValue = -1; + Require(fscanf_nid_postfix(&scanBinary, "%i", &binaryValue) == 1 && binaryValue == 0); + Require(fgetc_nid_postfix(&scanBinary) == 'b'); + scanBinary.Close(); + #ifdef _WIN32 FileStream scanSuppressed(std::tmpfile()); Require(std::fputs("12", scanSuppressed.GetHandle()) >= 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 20ddec679..e489befcf 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -626,7 +626,6 @@ Throughout the project, every function at every stage either **does exactly what - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); `%m` and argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. -- [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close`