Skip to content

testFFI: compare NaN returns semantically in the invoke-thunk differential - #367

Merged
Jarred-Sumner merged 2 commits into
mainfrom
jarred/testffi-nan-payload
Jul 28, 2026
Merged

testFFI: compare NaN returns semantically in the invoke-thunk differential#367
Jarred-Sumner merged 2 commits into
mainfrom
jarred/testffi-nan-payload

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

The invoke-thunk differential compared raw return slots bitwise. For fixtures whose return is a sum over the arguments (ffi_mix_1, ffi_mix_8), two argument NaNs make the result's NaN payload depend on addsd operand order — which the compiler may legally flip (addition is commutative), and does flip differently for the inlined -O2 reference call versus the outlined body reached through the thunk. On x86-64 release this surfaced as 0x7ff8000020000000 != 0x7ff8000000000001: identical arguments, identical body, different-but-legal instruction operand order. The same binary passes under Rosetta 2, which does not emulate x86 NaN-payload propagation, consistent with a payload artifact rather than a marshalling error.

NaN payloads are implementation-defined, so two NaN return slots of the same type now compare equal; real value mismatches, NaN-vs-number, and infinities still compare bitwise.

Follow-up to #319; unblocks the darwin-14-x64 / linux x64 release testFFI lanes on oven-sh/bun#35246.

…ntial

The invoke-thunk differential calls each fixture two ways -- through the
generated thunk and through a templated reference call -- with identical
argument slots, and compared the raw return slots bitwise. Fixtures like
ffi_mix_1 return a sum over their arguments, so when two argument NaNs are
in play (the f64 sNaN slot and the f32 sNaN slot) the result's NaN payload
is the payload of whichever operand is 'first' in an addsd. Addition is
commutative, so the compiler may legally flip operand order, and it does so
differently for the inlined reference call under -O2 than for the outlined
body reached through the thunk. That surfaced as
0x7ff8000020000000 != 0x7ff8000000000001 on x86-64 release only: same
delivered arguments, same body, different-but-legal instruction operand
order. The identical binary passes under Rosetta 2, which does not emulate
x86 NaN-payload propagation -- consistent with a payload artifact rather
than a marshalling error.

NaN payloads are implementation-defined, so treat two NaN return slots of
the same type as equal. Real value mismatches, NaN-vs-number, and
infinities still compare bitwise.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b94670e9-8e4a-493a-a4d3-51edb2f686cc

📥 Commits

Reviewing files that changed from the base of the PR and between a0f6135 and f8f2eb7.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/ffi/tests/testFFI.cpp

Walkthrough

Changes

FFI Result Validation

Layer / File(s) Summary
NaN-aware return comparison
Source/JavaScriptCore/ffi/tests/testFFI.cpp
Differential validation now treats NaN Double and Float results as equal using reconstructed raw slot values, while retaining standard equality for other results.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the fix, but it does not follow the required WebKit template or include the Bugzilla link and reviewed-by section. Rewrite it as the commit-message template with a Bugzilla URL, Reviewed by line, bug explanation, and changed-file bullet list.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the change: semantic NaN comparison in the testFFI invoke-thunk differential.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, test-only relaxation with a well-documented root cause; the inline nit about zero/one-arg fixtures is non-blocking.

Extended reasoning...

Overview

This PR touches a single function in Source/JavaScriptCore/ffi/tests/testFFI.cpp — the invoke-thunk differential harness. It changes the return-slot comparison from strict bitwise equality to bitwise-first-then-semantic-NaN: if the raw 64-bit slots differ and the return type is Float/Double, both sides are reinterpreted as their FP type and compared with std::isnan. All other types, all non-NaN FP values (including infinities), and NaN-vs-number remain bitwise-strict.

Security risks

None. This is test-harness code in a standalone test binary (testFFI), not shipped in the JSC runtime or reachable from JavaScript. The change only relaxes an assertion; it introduces no new inputs, allocations, or control flow reachable from untrusted data.

Level of scrutiny

Low. Test-only, ~5 lines, mechanically simple, and the PR description gives a precise root cause (compiler-chosen addsd operand order producing different-but-legal NaN payloads between the inlined -O2 reference call and the outlined thunk body) with a corroborating observation (passes under Rosetta 2, which doesn't emulate x86 NaN-payload propagation). I confirmed canonicalizeSlot stores Float in the low 32 bits (raw & 0xffffffffull), so the static_cast<uint32_t> truncation before bit_cast<float> is correct, and Double slots are stored raw so bit_cast<double> on the full 64 bits is correct.

Other factors

The bug hunter flagged one nit: the NaN fallback also covers zero/one-arg FP fixtures (ffi_ret_impure_nan_f64, ffi_ret_nan_f32, ffi_echo_f*) that have no operand-order nondeterminism, so a future thunk regression that quiets an SNaN or drops a payload bit would now pass silently in the differential. That's a real but minor loss of coverage — those fixtures' primary purification-before-boxing check at lines 2077-2090 is untouched — and the suggested sizeof...(Arguments) >= 2 gate is an easy follow-up if desired. Not worth blocking a CI-unblocking fix on.

Comment thread Source/JavaScriptCore/ffi/tests/testFFI.cpp
The operand-order nondeterminism that justifies treating two NaN return
slots as equal only exists when two or more argument NaNs are in play.
For zero/one-argument fixtures the bitwise check is sound and would
catch a future thunk that quiets an SNaN or canonicalizes a payload, so
apply the fallback only when sizeof...(Arguments) >= 2.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — thanks for gating the fallback on sizeof...(Arguments) >= 2; the zero/one-arg NaN fixtures keep strict bitwise comparison and only the multi-arg sum cases get the semantic-NaN relaxation.

What was reviewed:

  • Confirmed canonicalizeSlot stores Float in the low 32 bits, so bit_cast<float>(static_cast<uint32_t>(slot)) recovers the right bits for isnan.
  • Non-FP returns, NaN-vs-number, and infinities still fall through to the bitwise actual == expected check.
  • The if constexpr gate compiles the fallback out entirely for the ffi_ret_*_nan_* / ffi_echo_* instantiations.
Extended reasoning...

Overview

Test-only change to Source/JavaScriptCore/ffi/tests/testFFI.cpp (+9/-1) in the differentialCase template. The invoke-thunk differential previously compared the raw 64-bit return slot bitwise against the native reference call. For multi-argument sum fixtures (ffi_mix_*), two NaN inputs make the resulting NaN payload depend on addsd operand order, which the compiler may (and does) choose differently for the inlined reference vs. the outlined thunk-reached body — a legal codegen difference, not a marshalling bug. The fix keeps the bitwise check as the primary comparison but, when it fails and the return type is Float/Double and the fixture has ≥2 arguments, falls back to isnan(actual) && isnan(expected).

Security risks

None. This is a standalone test binary comparing return values; no runtime, JIT, heap, or API code is touched.

Level of scrutiny

Low. Test-only, ~9 lines, unblocks flaky release-build CI lanes. The rationale (IEEE 754 NaN payloads are implementation-defined for arithmetic results) is sound, and the Rosetta 2 observation in the PR description corroborates that this is a payload-propagation artifact rather than a real thunk defect.

Other factors

  • My previous review flagged that the original patch also relaxed zero/one-argument NaN fixtures where bitwise comparison was still meaningful. The author addressed this exactly as suggested by wrapping the fallback in if constexpr (sizeof...(Arguments) >= 2), and the thread is resolved.
  • Verified the Float path: canonicalizeSlot masks Float slots to the low 32 bits (raw & 0xffffffffull at line 1363), so static_cast<uint32_t>bit_cast<float> correctly reconstructs the value for the isnan check.
  • Non-NaN mismatches, NaN-vs-finite, and ±∞ still hit returnsEqual = actual == expected and fail as before.
  • Author reports 9519/0 locally after the follow-up commit.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
f8f2eb7f autobuild-preview-pr-367-f8f2eb7f 2026-07-28 16:08:59 UTC

Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
The invoke-thunk differential compared NaN return payloads bitwise; for
multi-argument sum fixtures the payload legally depends on addsd operand
order, which the compiler flips differently for the inlined reference
call versus the outlined body. That failed only on x86-64 release
hardware. #367 compares NaN returns semantically for the multi-argument
cases (single/zero-argument fixtures keep the strict bitwise check).
Verified: the shipped x64 testFFI from this preview reports 9524/0.
robobun pushed a commit to oven-sh/bun that referenced this pull request Jul 28, 2026
The invoke-thunk differential compared NaN return payloads bitwise; for
multi-argument sum fixtures the payload legally depends on addsd operand
order, which the compiler flips differently for the inlined reference
call versus the outlined body. That failed only on x86-64 release
hardware. #367 compares NaN returns semantically for the multi-argument
cases (single/zero-argument fixtures keep the strict bitwise check).
Verified: the shipped x64 testFFI from this preview reports 9524/0.
@Jarred-Sumner
Jarred-Sumner merged commit 83d6ba7 into main Jul 28, 2026
43 checks passed
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Jul 29, 2026
The invoke-thunk differential compared NaN return payloads bitwise; for
multi-argument sum fixtures the payload legally depends on addsd operand
order, which the compiler flips differently for the inlined reference
call versus the outlined body. That failed only on x86-64 release
hardware. #367 compares NaN returns semantically for the multi-argument
cases (single/zero-argument fixtures keep the strict bitwise check).
Verified: the shipped x64 testFFI from this preview reports 9524/0.
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