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 <noreply@anthropic.com>
This commit is contained in:
milescoviello 2026-10-12 02:35:47 +00:00
parent 32e62f2f70
commit 89bc9d1082
4 changed files with 63 additions and 5 deletions

View File

@ -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 <typename NextPointer, typename NextCapacity>
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<std::va_list*>(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;
}

View File

@ -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;

View File

@ -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);

View File

@ -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<char>::_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`