Skip to content

other native entry points taking strings may mis-unbox SSO strings (as #11430/#11486 did) #11519

Description

@proggeramlug

#11430 (fixed by #11468 and #11486) had one root cause, found in two places. Since #10762, String(n), `${n}` and n.toString() return small values as SSO strings: the characters live inline in the NaN-box, with no heap StringHeader behind them. A native entry that unboxes a string argument with bits & POINTER_MASK (0x0000_FFFF_FFFF_FFFF) and casts the result to *const StringHeader turns those inline characters into an address, then segfaults or reads garbage. Code that first checks tag == 0x7FFF (heap string only) avoids the crash but treats the SSO value as "not a string", so it returns a wrong answer.

Both sites found so far were fixed only locally:

The pattern is systematic and there are more candidates. This is a grep on main at c1d93bb, not an audit; each hit still needs a look to decide whether an SSO value can reach it.

Runtime and stdlib: masked bits cast to *const StringHeader (count per file, tests excluded)

file hits
perry-runtime/src/proxy/put_value.rs 7
perry-runtime/src/json/replacer.rs 7
perry-runtime/src/json/stringify.rs 4
perry-runtime/src/object/buffer_dispatch.rs 4 (the hasOwnProperty / propertyIsEnumerable key and key-export format readers; #11468 fixed only the write arms)
perry-stdlib/src/fetch_blob.rs 3
perry-runtime/src/thread.rs 3
perry-runtime/src/string/concat.rs 3
perry-runtime/src/string/char_ops.rs 3
perry-runtime/src/array/subclass.rs, array/named_props.rs 3 each
perry-runtime/src/{map,set}.rs, value/dynamic_object.rs, symbol/constructors.rs, json/{stringify_shape_template,stringify_primitive_object,stringify_flat,raw_json}.rs 2 each

Heap-only tag checks (== 0x7FFF) in files that also read a StringHeader, which gives wrong answers rather than crashes: string/concat.rs, object/class_registry/class_meta.rs, array/{from_concat,generic,flat_clone,indexing_keyed}.rs, typed_feedback.rs, date.rs, proxy.rs, value/{dyn_index,dynamic_object}.rs, {map,set}.rs, typedarray/mod.rs, buffer/u8_codec.rs, and in perry-stdlib common/dispatch/sqlite.rs, fetch_blob.rs, webcrypto/util.rs.

Codegen: unbox_to_i64 passed to natives whose parameter is a string (the shape #11486 fixed in the crypto arms). The largest non-crypto users: lower_array_method.rs (28), expr/arrays_finds.rs (19), expr/logical_collections.rs (15), expr/url_main.rs (13), expr/instance_misc1.rs (13), expr/misc_methods.rs (12), lower_call/native/native_tui_layout_branch.rs (11), native_instance_branch.rs (8), lower_call/namespace_call.rs (7), expr/string_regex_proc.rs (7), expr/os_uri_dates.rs (7). Most of these unbox arrays or objects, which is fine. Only the ones whose runtime parameter is a StringHeader are affected, for example url_main.rs and string_regex_proc.rs.

Suggested guard. Add a ratchet in the style of string_payload_access_inventory.py that counts, per crate:

  1. <mask> as *const …StringHeader casts (and (bits & POINTER_MASK) as usize as *const StringHeader) outside value/nanbox.rs.
  2. (… >> 48) == 0x7FFF string-tag checks that are not paired with is_short_string() in the same function.
  3. In perry-codegen, unbox_to_i64( whose result is passed to a runtime function declared with a string parameter. This would need a small table of which js_* params are strings; the runtime_decls modules already name them.

New string arguments should go through js_ffi_arg_ptr (scratch copy, for natives that only read during the call) or js_get_string_pointer_unified / OwnedStringBytes (a heap or owned copy). The baseline would lock in today's counts, so each fix lowers it and new code cannot add to it.

A cheap way to find the live ones is a differential sweep: take existing gap tests that pass a string literal to a builtin, replace the literal with String(<short number>) or a template, and compare against Node. #11468 and #11486 each had a program that crashed outright on main this way.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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