Skip to content

path.resolve/extname/normalize/matchesGlob read an SSO string's inline bytes as a StringHeader pointer #7621

Description

@proggeramlug

path.resolve(base, computedShortString) throws ERR_INVALID_ARG_TYPE where node returns the path.

Repro

import * as path from "node:path";
function seg(n: number): string { return "s" + String(n); }

console.log(path.resolve("/root", "s1"));       // node: /root/s1   perry: /root/s1
console.log(path.resolve("/root", seg(1)));     // node: /root/s1   perry: TypeError
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string.

Exit code 1. The two-argument and the three-argument forms both fail; only literal segments work.

It is the SSO inline representation, not GC staleness

Bisected by string length — a computed segment longer than the SSO inline threshold works, a short one does not:

function longSeg(n: number): string { return "segment-that-is-definitely-longer-than-sso-" + String(n); }
path.resolve("/root", longSeg(1));   // OK, /root/segment-that-is-...-1
path.resolve("/root", seg(1));       // TypeError

path.resolve(a, b, c) folds to PathResolveJoin(PathResolveJoin(a, b), c) (HIR), and Expr::PathResolveJoin's lowering in crates/perry-codegen/src/expr/arrays_finds.rs unboxes both operands with unbox_to_i64:

let a_handle = unbox_to_i64(blk, &a_box);
let b_handle = unbox_to_i64(blk, &b_box);
let result = blk.call(I64, "js_path_resolve_join", &[(I64, &a_handle), (I64, &b_handle)]);

unbox_to_i64 masks the low 48 bits. For an interned literal that is a real StringHeader*; for an SSO string those bits are the inline characters, so js_path_resolve_join dereferences character data as a header. This is the #214 SSO class — the same hazard Expr::EnvGetDynamic two hundred lines away already documents and routes around with unbox_str_handle.

Same shape, unaudited, in the neighbouring arms: PathExtname, PathNormalize, PathResolve, PathMatchesGlob.

Why it is not a one-line fix

unbox_str_handle calls js_get_string_pointer_unified, whose SSO branch allocates (it materialises the inline string onto the heap). Substituting it into PathResolveJoin therefore introduces a collection point between the two operand unboxes — the #7213 window — so the fix has to arrive with the rooting strategy for that window, not on its own.

Found while migrating expr/arrays_finds.rs for the Layer 1 rooting campaign (#7620 / #7615), and deliberately left there: that PR is IR-identical-or-justified by construction and a semantic fix to five path arms does not belong inside a refactor. Confirmed present on both arms of that PR, i.e. on main.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions