fix(libc): return the default quiet NaN from the strtod family

C17 7.22.1.3 and 7.29.4.1.1 interpret NAN(n-char-sequence) as a quiet
NaN. With MinGW-w64, std::strtod, strtof and strtold are mingw-w64's
gdtoa functions, which copy the hex digits into the significand
without the quiet bit, and for long double without the x87 integer
bit: strtod("nan(0x123)") returned the signaling NaN
0x7ff0000000000123 and strtold a pseudo-NaN. A payload whose only set
bit is the implicit or x87 integer bit gave infinity:
strtof("nan(0x800000)"), strtod("nan(0x10000000000000)") and
strtold("nan(0x8000000000000000)") returned +inf. glibc returns quiet
NaNs that keep the payload.

C17 7.12.11.2p2 makes nan("n-char-sequence") equivalent to
strtod("NAN(n-char-sequence)", NULL), and nan and nanf already return
the console's fixed quiet NaNs, ignoring the tag. ParseFloat, which
strtod, strtof, strtold, atof and the wide variants share, now returns
the same values, 0x7ff8000000000000 and 0x7fc00000, negated after a
minus sign, on Windows and Linux. strtold returns the x87 default
quiet NaN, since no console nanl was observed.
This commit is contained in:
Yunus Shaikh 2026-10-12 09:04:31 -07:00
parent 2a5eca11cd
commit aa4a9b4faf
4 changed files with 81 additions and 1 deletions

View File

@ -1,3 +1,4 @@
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
@ -45,12 +46,39 @@ auto ParseAsciiPrefix(const char16_t* text, char16_t** end, TParse parse) {
return value;
}
bool IsNanSubject(const char* text) {
while (*text == ' ' || (*text >= '\t' && *text <= '\r')) ++text;
if (*text == '+' || *text == '-') ++text;
return *text == 'n' || *text == 'N';
}
double QuietNan(double value, const char* text) {
const auto bits = std::bit_cast<std::uint64_t>(value);
if ((bits & 0x7ff0000000000000u) != 0x7ff0000000000000u || !IsNanSubject(text)) return value;
return std::bit_cast<double>((bits & 0x8000000000000000u) | 0x7ff8000000000000u);
}
float QuietNan(float value, const char* text) {
const auto bits = std::bit_cast<std::uint32_t>(value);
if ((bits & 0x7f800000u) != 0x7f800000u || !IsNanSubject(text)) return value;
return std::bit_cast<float>((bits & 0x80000000u) | 0x7fc00000u);
}
long double QuietNan(long double value, const char* text) {
std::uint16_t exponent;
std::memcpy(&exponent, reinterpret_cast<const unsigned char*>(&value) + sizeof(std::uint64_t), sizeof(exponent));
if ((exponent & 0x7fffu) != 0x7fffu || !IsNanSubject(text)) return value;
const std::uint64_t significand = 0xc000000000000000u;
std::memcpy(&value, &significand, sizeof(significand));
return value;
}
template<typename TFloat>
TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, char**)) {
char* parsedEnd = nullptr;
const TFloat value = parse(text, &parsedEnd);
if (end != nullptr) *end = parsedEnd;
return parsedEnd == text ? TFloat{0} : value;
return parsedEnd == text ? TFloat{0} : QuietNan(value, text);
}
}

View File

@ -148,6 +148,45 @@ static bool ExtendedBits(long double value, std::uint64_t mantissa, std::uint16_
return actualMantissa == mantissa && actualSignExponent == signExponent;
}
static void CheckNanConversions() {
const struct { const char* text; std::size_t consumed; bool negative; } cases[] = {
{"nan(0x123)", 10, false}, {" -NaN(1)!", 8, true}, {"nan(0x800000)", 13, false},
{"nan(0x10000000000000)", 21, false}, {"nan(0x8000000000000000)", 23, false},
};
bool correct = true;
for (const auto& test : cases) {
const char* expectedEnd = test.text + test.consumed;
const std::uint64_t expectedDouble = test.negative ? 0xfff8000000000000ull : 0x7ff8000000000000ull;
char* end = nullptr;
const auto value = std::bit_cast<std::uint64_t>(strtod_nid_postfix(test.text, &end));
if (value != expectedDouble || end != expectedEnd) {
std::fprintf(stderr, "strtod(\"%s\") = 0x%016llx\n", test.text, static_cast<unsigned long long>(value));
correct = false;
}
const auto ascii = std::bit_cast<std::uint64_t>(atof_nid_postfix(test.text));
if (ascii != expectedDouble) {
std::fprintf(stderr, "atof(\"%s\") = 0x%016llx\n", test.text, static_cast<unsigned long long>(ascii));
correct = false;
}
const auto single = std::bit_cast<std::uint32_t>(strtof_nid_postfix(test.text, &end));
if (single != (test.negative ? 0xffc00000u : 0x7fc00000u) || end != expectedEnd) {
std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast<unsigned>(single));
correct = false;
}
const long double extended = strtold_nid_postfix(test.text, &end);
if (!ExtendedBits(extended, 0xc000000000000000ull, test.negative ? 0xffff : 0x7fff) || end != expectedEnd) {
std::uint64_t significand = 0;
std::uint16_t signExponent = 0;
std::memcpy(&significand, &extended, sizeof(significand));
std::memcpy(&signExponent, reinterpret_cast<const unsigned char*>(&extended) + sizeof(significand), sizeof(signExponent));
std::fprintf(stderr, "strtold(\"%s\") = 0x%04x'%016llx\n", test.text, static_cast<unsigned>(signExponent),
static_cast<unsigned long long>(significand));
correct = false;
}
}
Require(correct);
}
static void CheckHypotSpecialValue() {
Require(std::bit_cast<std::uint64_t>(hypot_nid_postfix(INFINITY, std::bit_cast<double>(0x7ff0000000000001ull))) == 0x7ff0000000000000ull);
Require(std::bit_cast<std::uint64_t>(hypot_nid_postfix(std::bit_cast<double>(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull);
@ -609,6 +648,7 @@ int main() {
Require(std::isinf(strtof_nid_postfix("1e1000", nullptr)));
Require(*__error_nid_postfix() == 34);
Require(strtold_nid_postfix("1.0000000000000000001!", &end) > 1.L && *end == '!');
CheckNanConversions();
Require(fmodf_nid_postfix(5.5f, 2.f) == 1.5f);
Require(fmodf_nid_postfix(-5.5f, 2.f) == -1.5f);
Require(std::signbit(fmodf_nid_postfix(-4.f, 2.f)));

View File

@ -1,7 +1,11 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <bit>
#include <climits>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
extern "C" {
@ -173,6 +177,13 @@ int main() {
require(wcstof_nid_postfix(u"0.25", nullptr) == 0.25f);
const char16_t* half = u"-1.5é";
require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4);
const char16_t* wideNan = u" -nan(0x123)é";
require(std::bit_cast<std::uint64_t>(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000000u && end == wideNan + 12);
require(std::bit_cast<std::uint32_t>(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12);
const long double wideExtended = wcstold_nid_postfix(wideNan, &end);
std::uint64_t significand;
std::memcpy(&significand, &wideExtended, sizeof(significand));
require(std::isnan(wideExtended) && std::signbit(wideExtended) && significand == 0xc000000000000000u && end == wideNan + 12);
require(wcscoll_nid_postfix(u"a", u"b") < 0);
require(wcscoll_nid_postfix(u"￿", u"a") > 0);

View File

@ -444,6 +444,7 @@ Throughout the project, every function at every stage either **does exactly what
- [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws
- [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported
- [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target.
- [strtod, strtof, strtold](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`)
- [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep.
- [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`.
- [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing