Skip to content

Commit 1e8f843

Browse files
author
Ralph Küpper
committed
docs(changelog): #7621 changeset fragment for PR #7626
1 parent 1502447 commit 1e8f843

1 file changed

Lines changed: 53 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
### Fixed
2+
3+
- **`path.*` read an SSO string's inline bytes as a `StringHeader` pointer (#7621).**
4+
`path.resolve("/root", computedShortString)` threw
5+
`TypeError [ERR_INVALID_ARG_TYPE]` where node returns the path, while the same
6+
call with a *literal* segment worked.
7+
8+
Every `path.*` codegen arm unboxed its operand with `unbox_to_i64` — `bitcast
9+
double -> i64; and POINTER_MASK` — and handed the low 48 bits to a runtime
10+
entry that dereferences them as `*const StringHeader`. Those bits are the
11+
header for a **heap** string (`STRING_TAG` = 0x7FFF) and the **characters** for
12+
a small-string-optimized one (`SHORT_STRING_TAG` = 0x7FF9, length + up to
13+
`SHORT_STRING_MAX_LEN` = 5 inline bytes). A literal is interned onto the heap;
14+
a computed short string takes the inline form. The #214 class, bisected by
15+
length — 5 bytes threw, 6 bytes worked.
16+
17+
Twelve arms were affected, not the five the issue named — `resolve(a, b)`,
18+
`resolve(p)`, `join`, `win32.join`, `normalize`, `extname`, `dirname`,
19+
`basename`, `basename(p, ext)`, `isAbsolute`, `parse` and `matchesGlob`, plus
20+
the `path.win32.*` equivalents. `parse` and `matchesGlob` were **silently
21+
wrong** rather than throwing (`path.parse(short).base` was `""`;
22+
`matchesGlob` matched the pattern against `""`), which is why they had never
23+
been reported. `path.relative`, `path.format` and `path.toNamespacedPath`
24+
already took NaN-boxed values (`js_path_relative_checked`, #2995) and were
25+
unaffected — that is the precedent this change generalises.
26+
27+
Single-operand arms now call `js_path_arg_header`, which materialises **only**
28+
the SSO case and reproduces the old mask bit for bit for heap strings and for
29+
every non-string, so each entry point keeps its own established non-string
30+
behaviour (throw, or `unwrap_or_default`) unchanged. It is deliberately not
31+
`js_get_string_pointer_unified`, which coerces numbers to strings and would
32+
have turned `path.isAbsolute(5)` from Node's throw into `false`.
33+
34+
Two-operand arms hand both operands to the runtime NaN-boxed
35+
(`js_path_*_value` in `crates/perry-runtime/src/path/value_args.rs`). Codegen
36+
cannot close that window itself: materialising the first operand allocates, and
37+
`rooting::with_operands_rooted` yields registers rather than slots, so the
38+
second operand's register is stale the instant the first is materialised and
39+
there is no re-read to reach for. The runtime entry roots the first operand in
40+
a `RuntimeHandleScope` and re-reads it through `RuntimeHandle::across_const`.
41+
42+
That rooting half is defensive rather than instrument-proven, and the module
43+
says so at the site: reverting `across_const` to a pre-bound address produced
44+
zero faults under `PERRY_GC_ZEAL=1` + `PERRY_GC_PROTECT_FROMSPACE=1` (402k
45+
copying minors) and 0/400k mismatches under `PERRY_GC_FORCE_EVACUATE=1`,
46+
because a collection reached from inside an allocation runs with
47+
`GC_FLAG_IN_ALLOC` set and the copying minor is therefore ineligible — nothing
48+
moves at an allocation point today.
49+
50+
Covered by `test-files/test_gap_7621_path_sso_operands.ts` (both sides of the
51+
SSO boundary, computed and literal operands, absolute and relative bases,
52+
multi-segment resolves, all twelve arms, the non-string throw) plus five
53+
`perry-runtime` unit tests.

0 commit comments

Comments
 (0)