Skip to content

fix(libc): stop the strtol family at a binary prefix on Linux - #1459

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
nauroth:claude/strtol-c17-prefix
Oct 9, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
nauroth:claude/strtol-c17-prefix

Conversation

@nauroth

@nauroth nauroth commented Oct 8, 2026 •

Copy link
Copy Markdown

What

On Linux, the narrow integer converters strtol, strtoul, strtoll, strtoull, _Stoull, strtoimax, strtoumax and _Stoul pass the guest's text and base straight to glibc. The wide converters wcstol, wcstoll, wcstoul and wcstoull pass the ASCII prefix of the guest's text. Since glibc 2.38, code built as GNU C++ calls the C23 __isoc23_* functions (g++ defines _GNU_SOURCE). Those accept a 0b/0B binary prefix in base 0 and 2. C17 has no binary prefix (7.22.1.4p3): the subject sequence of "0b101" is "0", so the result is 0 and the end pointer stops at the b.

Results on Linux with the code on main:

call main (glibc 2.39) C17
strtol("0b101", &end, 0) 5, end +5 0, end +1
strtol("0b101", &end, 2) 5, end +5 0, end +1
strtol(" -0B1", &end, 0) -1, end +5 0, end +3
strtoul("0b11", &end, 2) 3, end +4 0, end +1
strtoimax("0b101", &end, 0) 5, end +5 0, end +1
strtoumax("0b101", &end, 0) 5, end +5 0, end +1
_Stoul("0b101", &end, 0) 5, end +5 0, end +1
wcstol(u"0b101", &end, 0) 5, end +5 0, end +1

Separately, on Windows, _Stoul was declared with the host's unsigned long and returned std::strtoul, both 32-bit in the LLP64 build, while the guest's unsigned long is 64-bit (#2424). "4294967296" clamped to 4294967295 with ERANGE and "-1" gave 0xffffffff, so a guest std::stoul threw out_of_range on values the console accepts.

Changes, one commit each:

  • StopAtBinaryPrefix_nid_no_patch (Strings.cpp, declared in General.hpp) handles text in base 0 or 2. After C-locale white space and an optional sign, if the text starts with 0b or 0B and a binary digit, it returns 0 and sets the end pointer after the 0. The eight narrow exports check it first. Every other input still goes to the host function, so errno, overflow and the 0/0x prefixes are unchanged. guest_math gets rows for base 0 and base 2, a sign with leading white space, 0b2 (where glibc already stops after the 0) and 0b101 in base 16 (still the hex value 0xb101), and checks strtoll, strtoull, strtoumax, _Stoull and _Stoul on " -0B11".
  • The four wide exports apply the same check to the ASCII prefix they already build; ParseAsciiPrefix maps the end pointer back to the guest's UTF-16 text as before. guest_wide_strings checks them on u" -0b101" in base 0 and base 2, and u"0b101" in base 16.
  • _Stoul_nid_postfix returns std::uint64_t from std::strtoull, as strtoul_nid_postfix and _Stoull already do, with the prefix check still in front. guest_math checks "4294967296", "18446744073709551615" and "-1". On Linux this changes nothing.
  • The scanf family's %i reaches glibc's C23 conversion the same way: sscanf("0b101", "%i", &value) stores 5. Because the format is handed to glibc, this is recorded in TechnicalDebt rather than changed here.

The Windows behaviour of the prefix check is unchanged: the UCRT functions accept only the 0 and 0x prefixes, so the new check returns what they already return.

Thanks to @Zheorgyan for catching the wide functions, @AchrafReyani for #2424, @SurajKhanna for #2442, and @MarcZX100 for suggesting the fold.

Tested

  • Linux, local: Ubuntu 24.04, glibc 2.39, GCC 13.3.0, Release, -DBUILD_TESTING=ON, this branch on d724cad (current main). guest_math and guest_wide_strings pass; guest_math fails on main with "Guest strtol failed for '0b101' in base 0", and guest_wide_strings fails on the first commit, before the wide change. The table above comes from main's function bodies, built with the same compiler and glibc.
  • Windows: the _Stoul change is the one fix(libc): return 64-bit values from _Stoul on Windows #2442 tested on Windows 11, GCC 16.2.0 (UCRT64), where guest_math aborted on the first new case before the change. The CI Windows run on the previous push (419/419) predates that commit.
  • CI: the Build workflow passed on an earlier push (Linux 423/423, Windows 419/419). The current head needs a collaborator-triggered run (ci(build): require manual full builds and auto-test relinker for contr #2113).
  • python3 tools/check_conventions.py --base upstream/main passes.

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 in TechnicalDebt
  • 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: none
  • AI-assisted: yes

@Zheorgyan

Copy link
Copy Markdown
Contributor

the wide versions have the same problem and aren't covered here. wcstol, wcstoll, wcstoul and wcstoull in Strings.cpp also go straight to glibc, and its C23 wcstoll/wcstoull take the prefix too. with this branch merged onto current main, wcstol_nid_postfix(L"0b101", &end, 0) returns 5 with end +5, and wcstoul_nid_postfix in base 2 does the same. maybe worth a wide twin of StopAtBinaryPrefix_nid_no_patch, or a follow-up if you'd rather keep this one narrow

the rest checks out on Linux, Ubuntu 24.04 in Docker with glibc 2.39 and GCC 14.2. guest_math passes with the change and fails on main with "Guest strtol failed for '0b101' in base 0"

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@nauroth
nauroth force-pushed the claude/strtol-c17-prefix branch from 0541c12 to b709647 Compare October 8, 2026 10:57
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 8, 2026
@nauroth

nauroth commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thank you @Zheorgyan!

The wide versions now go through the same check: wcstol, wcstoll, wcstoul and wcstoull call StopAtBinaryPrefix_nid_no_patch on the ASCII prefix before glibc, and ParseAsciiPrefix maps the end pointer back like before (second commit). guest_wide_strings now covers u" -0b101" in base 0 and 2 for all four, plus a base 16 case to make sure 0b101 still parses as hex there. It fails on the previous commit and passes with this one.

I also rebased onto current main to clear the TechnicalDebt conflict with #1278.

@nauroth

nauroth commented Oct 9, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (d70b899); the branch had drifted 90 commits and conflicted again in TechnicalDebt.md. No code changes: still the same two commits and 55 lines. On the new base guest_math and guest_wide_strings pass on Linux (Ubuntu 24.04, GCC 13.3.0) and tools/check_conventions.py passes.

The earlier red Linux run was agc_driver_flat_store timing out on its cold first run, which #1715 has since serialized on main. Since #2113 the full builds only run when a collaborator dispatches Build with the PR number, and they build the PR merged into main, so the rebase was needed before that can run. Could a collaborator trigger it when convenient?

@reyani-dev reyani-dev mentioned this pull request Oct 9, 2026
9 tasks done
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 9, 2026
@MarcZX10

MarcZX10 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

One related issue surfaced while reviewing the Windows _Stoul report: #2424 found that _Stoul_nid_postfix uses unsigned long/strtoul, which are only 32-bit in the Windows LLP64 build, while the guest ABI expects the PS5’s 64-bit result. The proposed fix in #2442 (std::uint64_t + std::strtoull, with two guest_math cases) was auto-closed by the intake pause, not rejected on code quality.

This PR also changes _Stoul_nid_postfix in RuntimeSupport.cpp to enforce C17 binary-prefix parsing on Linux. The two changes appear compatible: the Linux prefix guard can stay as-is, while the Windows fallback delegates to the 64-bit parser. Would you prefer to include the small Windows-width correction in this branch, or keep this PR narrowly scoped and record that #2424 needs a follow-up when intake resumes? I have not prepared or pushed a duplicate patch.

claude added 2 commits October 9, 2026 21:26
C17 has no binary prefix: with base 0 or 2, strtol("0b101", &end, base)
reads the subject "0", returns 0 and leaves end at the 'b'. With glibc
2.38 or later, g++ (which defines _GNU_SOURCE) binds std::strtoll,
strtoull, strtoul, strtoimax and strtoumax to the C23 __isoc23_*
functions, which accept 0b/0B: the Linux exports returned 5 and
consumed the whole string. The Windows UCRT already follows C17, so the
same guest call gave different results on the two hosts.

strtol, strtoul, strtoll, strtoull, _Stoull, strtoimax, strtoumax and
_Stoul now return 0 with the end pointer after the '0' when, after
white space and an optional sign, the text starts with 0b or 0B and a
binary digit, in base 0 or 2. Everything else still goes to the host
function. The scanf family's %i has the same glibc behaviour; it is
recorded in TechnicalDebt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wcstol, wcstoll, wcstoul and wcstoull parse the ASCII prefix of the
guest's UTF-16 text with std::strtoll and std::strtoull, which glibc 2.38
and later bind to the C23 __isoc23_* functions: wcstol(u"0b101", &end, 0)
returned 5 with the end pointer after the whole string. They now check
StopAtBinaryPrefix_nid_no_patch first, like the narrow functions, and the
end pointer maps back through ParseAsciiPrefix as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nauroth
nauroth force-pushed the claude/strtol-c17-prefix branch from 74dde55 to 30ebbed Compare October 9, 2026 21:27
nauroth pushed a commit to nauroth/AnyPS5 that referenced this pull request Oct 9, 2026
The guest's unsigned long is 64 bits. _Stoul was declared with the
host's unsigned long and returned std::strtoul, which are 32 bits on
Windows (LLP64): "4294967296" clamped to 4294967295 with ERANGE and
"-1" gave 0xffffffff, so a guest std::stoul threw out_of_range on values
the console accepts. It now returns std::uint64_t from std::strtoull,
as strtoul_nid_postfix and _Stoull already do; on Linux nothing changes.
The binary-prefix check stays in front of the host call.

Reported in boykopovar#2424 by AchrafReyani; the same change was proposed in boykopovar#2442
by SurajKhanna, whose two guest_math cases are taken here, with the "-1"
case from boykopovar#1579 by ewraj. Folded in at MarcZX100's suggestion on boykopovar#1459.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The guest's unsigned long is 64 bits. _Stoul was declared with the
host's unsigned long and returned std::strtoul, which are 32 bits on
Windows (LLP64): "4294967296" clamped to 4294967295 with ERANGE and
"-1" gave 0xffffffff, so a guest std::stoul threw out_of_range on values
the console accepts. It now returns std::uint64_t from std::strtoull,
as strtoul_nid_postfix and _Stoull already do; on Linux nothing changes.
The binary-prefix check stays in front of the host call.

Reported in boykopovar#2424 by AchrafReyani; the same change was proposed in boykopovar#2442
by SurajKhanna, whose two guest_math cases are taken here. Folded in at
MarcZX100's suggestion on boykopovar#1459.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@nauroth
nauroth force-pushed the claude/strtol-c17-prefix branch from 30ebbed to 2243e17 Compare October 9, 2026 21:33
@nauroth

nauroth commented Oct 9, 2026

Copy link
Copy Markdown
Author

Thanks @MarcZX100 for connecting the dots, and thanks @AchrafReyani for the report in #2424 and @SurajKhanna for the patch in #2442. Folded in as a third commit: _Stoul_nid_postfix now returns std::uint64_t from std::strtoull, with the binary-prefix check still in front of the host call, exactly as you described. The two guest_math cases from #2442 are taken as they were plus a "-1" → UINT64_MAX case. On Linux nothing changes, since unsigned long is already 64-bit there. Also rebased onto current main (d724cad) so the manual build can run; guest_math, guest_wide_strings and the conventions check pass on Linux.

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on Linux (glibc 2.39, GCC): guest_math and guest_wide_strings pass and both fail without the change; the 0b/0B stop matches C17 7.22.1.4 (subject "0", end at the b), base 16 and the 0/0x prefixes are untouched, and the _Stoul 64-bit return matches strtoul_nid_postfix/_Stoull. TechnicalDebt line is in the right section. The Windows _Stoul behaviour is not run here. Merging.

@adriadam10
adriadam10 merged commit 787239e into boykopovar:main Oct 9, 2026
6 of 7 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
The guest's unsigned long is 64 bits. _Stoul was declared with the
host's unsigned long and returned std::strtoul, which are 32 bits on
Windows (LLP64): "4294967296" clamped to 4294967295 with ERANGE and
"-1" gave 0xffffffff, so a guest std::stoul threw out_of_range on values
the console accepts. It now returns std::uint64_t from std::strtoull,
as strtoul_nid_postfix and _Stoull already do; on Linux nothing changes.
The binary-prefix check stays in front of the host call.

Reported in boykopovar#2424 by AchrafReyani; the same change was proposed in boykopovar#2442
by SurajKhanna, whose two guest_math cases are taken here. Folded in at
MarcZX100's suggestion on boykopovar#1459.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
fix(libc): stop the strtol family at a binary prefix on Linux
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants