Skip to content

Fix over-aligned key path indices (wasm LocalStorage<UInt64> trap) + wasm runtime smoke - #89

Merged
mansbernhardt merged 3 commits into
mainfrom
claude/overaligned-keypath-index
Sep 29, 2026
Merged

mansbernhardt merged 3 commits into
mainfrom
claude/overaligned-keypath-index

Conversation

@mansbernhardt

@mansbernhardt mansbernhardt commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Reading a LocalStorage<UInt64> on wasm32 trapped with "null function" in keypath_destroy, called from Context.willAccessStorage. imagien hit this in #2428 and worked around it in #2471 by switching to LocalStorage<Int>.

The cause is a Swift compiler bug, reproduced on 6.3.3 and 6.4.0 with about 10 lines of plain Swift and no SwiftModel. It hits a key path that:

  • is formed in generic code,
  • has a subscript index whose layout depends on a generic parameter, and
  • has an index aligned beyond a pointer.

The call site packs the index into the key path's argument buffer at pointer-size offset, without aligning it. The generated keypath_arg_init reads it back at roundUp(pointerSize, alignment). So the index is read as garbage: sometimes a silently wrong value, sometimes a trap later on destroy.

  • wasm32 (4-byte pointers): every 8-byte-aligned type fails: UInt64, Int64, Double. Int works, which is why the imagien workaround helped.
  • 64-bit: 16-byte-aligned types fail, e.g. SIMD4<Float>. This happens in native macOS builds too.

SwiftModel formed three such key paths:

  • [_metadata: ContextStorage<V>] for local and environment storage,
  • [_preference: PreferenceStorage<V>] for preferences,
  • [cursor: ContainerCursor<ID, …>] for collection elements. A [Row] with UInt64 ids crashed on wasm32 the same way.

Fix

  • The _ModelStateType [_metadata:] / [_preference:] stub subscripts are now indexed by the storage's non-generic AnyHashableSendable key. They are now internal, and V appears only in the result type.
  • ContainerCursor stores its id in a class box, so its layout no longer depends on ID.
  • The unused generic Model[_metadata:] / Model[_preference:] subscripts are removed.

Validation

  • OverAlignedKeyPathIndexTests (native) covers local, environment, preference and container-element paths with SIMD2<Double>, which is 16-byte aligned. That makes the bug reproduce on 64-bit hosts, so regular macOS and Linux CI catches a regression. All four tests crashed with SIGSEGV before the fix.
  • scripts/wasm-smoke (new) builds Tests/WASMSmoke, a small separate package, for wasm32-unknown-wasip1 and runs it under wasmtime. It checks UInt64 / Int64 / Double storage, environment, preference, [Row] with UInt64 ids, and memoize. Against origin/main it traps with exactly the downstream stack (keypath_destroy ← Context.willAccessStorage). With this branch, all checks pass.
    • CI's WASM job now runs it: it installs wasmtime via bytecodealliance/actions/wasmtime/setup@v1 and runs scripts/wasm-smoke after the compile steps, so WASM code is executed in CI, not just compiled. The job timeout goes from 10 to 15 minutes; the job took about 4 minutes, and the smoke adds one more SwiftModel build. The suite itself still can't run on WASI, because GlobalTickScheduler is GCD-backed.
  • scripts/test (parallel) and scripts/test --no-parallel are both green locally: 746 + 124 + 7 + 29 tests.
  • The wasm swift build --build-tests passes with the 6.4.0 SDK (SWIFTPM_TARGET_WASI=1 OMIT_DYNAMIC_TEST_SUPPORT=1). The local 6.3.x toolchains can't compile this repo's manifest against the macOS 27 SDK, so the 6.3 lane is left to CI's Linux container.
  • Benchmarks, 2 runs each against main: the container / collection rows (5, 5b, 5c), which build cursor key paths, are neutral to 10% faster, so the extra id-box allocation doesn't show. Rows 2d and 4 vary by about 40% between main's own two runs, so they're noise.

🤖 Generated with Claude Code

mansbernhardt and others added 3 commits September 29, 2026 10:46
A Swift compiler bug (present in 6.3.3 and 6.4.0) misplaces a key path's
subscript index when the path is formed in generic code, the index's
layout depends on a generic parameter, and the index is aligned beyond a
pointer. The caller packs it at pointer-size offset; the generated
argument-init thunk reads it at the offset rounded up to its alignment.

SwiftModel formed such paths for storage ([_metadata: ContextStorage<V>]),
preferences ([_preference: PreferenceStorage<V>]) and container elements
([cursor: ContainerCursor<ID, ...>]). On wasm32 that made
LocalStorage<UInt64>/Int64/Double read garbage or trap in keypath_destroy
under Context.willAccessStorage; on 64-bit, 16-byte-aligned types such as
SIMD vectors did the same.

Index the storage and preference stub paths by the non-generic storage
key, box ContainerCursor's id, and drop the unused generic Model
[_metadata:]/[_preference:] subscripts.

OverAlignedKeyPathIndexTests reproduces all four paths natively with
SIMD2<Double> (each crashed before). scripts/wasm-smoke builds a small
executable for wasm32-unknown-wasip1 and runs it under wasmtime; it
trapped with the downstream stack before the fix and passes after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test suite can't run on WASI yet, but a plain executable can, so the
WASM job now installs wasmtime and runs scripts/wasm-smoke after the
compile steps. Job timeout 10 -> 15 min for the extra build (the job
took ~4 min; the smoke adds one more SwiftModel build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The swift:6.3.0 container has no xz, so the action couldn't unpack the
wasmtime .tar.xz. Also pin wasmtime to 49.0.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mansbernhardt
mansbernhardt merged commit 3458375 into main Sep 29, 2026
7 checks passed
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