◐ Shell
clean mode source ↗

Tests: Reproducer for Swift 6.3 typed-throws async closure miscompile by krodak · Pull Request #12 · PassiveLogic/JavaScriptKit

Overview

A standalone, runnable reproducer for the Swift compiler bug that blocks the Swift-to-JS async throws reject path in the closures work (the "Known limitation" in the feature PR). This branch is stacked on top of that feature branch so reviewers see only the reproducer test in the diff.

Root cause identified: this is swiftlang/swift#89320 ("Crash with typed/untyped throwing async functions on Wasm"), with a fix in progress in swiftlang/swift#89715 (IRGen: fix async typed throws miscompiles on Wasm). On wasm32, IRGen places the indirect typed-error pointer after swiftself at thick call sites, but a captureless closure or function reference goes through thin_to_thick_function whose thin signature has no swiftself; LLVM's wasm swiftcc padding appends the missing parameters, producing equal-arity but permuted signatures. The callee writes the thrown error through the caller's null swiftself, and the caller reads a never-written slot.

The bug triggers only when both conditions hold:

  1. the typed error is too large for the direct error convention (roughly >16 bytes on wasm32; JSException is ~36 bytes), and
  2. the closure value was produced by thin_to_thick_function (a captureless closure literal or a function reference).

Tests/BridgeJSRuntimeTests/TypedThrowsAsyncClosureBugTests.swift demonstrates both conditions with labelled [REPRO] output. The suite stays green: the buggy cases are encoded as current-behaviour assertions with a comment to flip once swiftlang/swift#89715 lands (XCTExpectFailure is not available in the wasm XCTest runtime).

What it shows

[REPRO] jsexc / typed-throws async closure   object!=nil: false   <- payload lost
[REPRO] large / typed-throws async closure   garbage values       <- payload lost (no JavaScriptKit types involved)
[REPRO] jsexc / async function               object!=nil: true
[REPRO] jsexc / capturing typed-throws closure object!=nil: true  <- capturing closures are unaffected
[REPRO] jsexc / untyped async closure        object!=nil: true
[REPRO] value / typed-throws async closure   payload: ALIVE       <- small errors ride the direct convention
[REPRO] ref   / typed-throws async closure   payload: ALIVE
case error size callee shape result
JSException from captureless async closure ~36 B thin_to_thick LOST
32-byte plain-Swift error from captureless async closure 32 B thin_to_thick LOST
JSException from async function ~36 B direct preserved
JSException from capturing async closure ~36 B partial apply preserved
JSException from untyped async closure ~36 B untyped convention preserved
small struct / class-reference errors 4-12 B thin_to_thick preserved

Two additional observations that raise severity beyond the trap documented in swiftlang/swift#89320:

  • the corruption is silent: the caller observes a zeroed or stale-garbage error value instead of trapping;
  • the garbage is dangerous to even read: a corrupted error carrying a String field trapped with UnsafeBufferPointer has a nil start and nonzero count during interpolation, and releasing corrupted references crashes in swift_release.

Relationship to existing Swift issues

Purpose

Internal review artifact to confirm the characterization before coordinating with the maintainers on the preferred path (merge with the documented limitation, box JSException storage to fit the direct error convention, or wait for swiftlang/swift#89715). Run with make unittest.