Skip to content

SIGSEGV: indexing or iterating a short concatenated string (SSO value mask-cast to StringHeader*) #6887

Description

@proggeramlug

Summary

Indexing or iterating a short string produced by concatenation segfaults. "ab" + "c" yields an inline SHORT_STRING_TAG (SSO) JSValue whose payload is the characters, not a heap address — but two places mask its low 48 bits and treat the result as a StringHeader*.

Repro

const a = "ab";
const b = "c";
const s = a + b;
console.log(s.length);   // 3  — fine
console.log(s[0]);       // SIGSEGV
bun     3 / a
perry   3 / <exit 139>

typeof s, s.length and printing s all work, which is what makes this so easy to miss — the value is a perfectly good string right up until something indexes it.

Also crashing on the same value:

for (const ch of s) {}      // SIGSEGV
Array.from(s)               // SIGSEGV
[...s]                      // SIGSEGV
s.split("")                 // SIGSEGV

Not affected

  • string literals: "abc"[0] is fine (heap-allocated at init)
  • join() results: ["ab","c"].join("")[0] is fine
  • long concatenations: "a".repeat(40) + "b".repeat(40) is fine — it exceeds the SSO threshold and is heap-backed

That last one is the clean discriminator: a 3-char concat crashes, a 64-char concat does not.

Cause

Two independent mask-unboxings of a possibly-SSO value:

  1. crates/perry-codegen/src/expr/index_get.rs — the s[i] fast path does
    unbox_to_i64(blk, &s_box) and passes the result to js_string_index_get,
    which expects a real StringHeader*. Emitted IR:

    %r10 = call double @js_string_concat_box(double %r8, double %r9)
    %r12 = bitcast double %r11 to i64
    %r13 = and i64 %r12, 281474976710655        ; low 48 bits
    %r14 = call double @js_string_index_get(i64 %r13, double 0.0)

    For an SSO value %r13 is the packed characters, not an address.

  2. crates/perry-runtime/src/array/from_concat.rs::js_array_from_value receives
    the value boxed but then does its own bits & 0x0000_FFFF_FFFF_FFFF behind a
    (bits >> 48) >= 0x7FF8 test, which an SSO value passes.

Impact

This is the blocker behind #6872 and the last thing stopping the Milo compiler
(https://github.com/milo-language/milo) from working under Perry. Its codegen does:

const fmt = partFmts.join("") + "\n";   // "%.*s\n" — 5 chars, SSO
for (const ch of fmt) { ... }           // SIGSEGV

Every Milo program importing std/platform, std/os, std/sync, std/runtime
or std/event died here.

String concatenation feeding an index or a for...of is about as common as
TypeScript gets, so the blast radius is wide — it just happens to require the
result to be short enough for SSO.

Environment

  • perry main @ 7ae2fb4, macOS arm64

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions