You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
gc: constructor arguments on lower_new's non-class branches (Readline / imported ctor / new Function) and builtin.rs are still not precise roots #6986
Found while fixing #6969 in #6983, and deliberately left out of that PR's scope.
#6983 roots the constructor arguments built by lower_new_impl_inner's main
class path (crates/perry-codegen/src/lower_call/new.rs, the lowered_args
loop reached after the ctx.classes lookup). Three earlier branches of the
same function build multi-argument lists the same way and are still unrooted, as
are the multi-argument built-in constructors in lower_call/builtin.rs.
Sites
crates/perry-codegen/src/lower_call/new.rs:
new Readline(...) (node:readline/promises) — lowers output, then options, then for extra in args.iter().skip(2) { lower_expr(...) }, and
passes output/options live into js_readline_promises_readline_new. A
later argument's lowering can collect while the first two sit in SSA
registers.
new <importedFn>(...) — for a in args { lowered_args.push(lower_constructor_arg(ctx, a)?) }
then lower_js_args_array, which is a plain alloca pack with no rooting
(expr/helpers.rs), passed to js_new_function_construct.
new Function(...) with a dynamic body — the identical loop, into js_function_ctor_from_strings.
crates/perry-codegen/src/lower_call/builtin.rs (untouched by #6983): several
built-in constructors lower args[0] then args[1] sequentially via plain lower_expr. lower_builtin_new is called from inside lower_new_impl_inner,
so these are on the same path. I did not audit each one; the shape is the same.
Why these are cheap to fix
#6983 already wraps the whole function in an expression-scope temp-root barrier
(temp_root_scope_begin / _end in lower_new_impl), and it is a stack cut —
so these branches are already inside a scope that releases correctly on every
return path. What is missing is only the per-argument push + re-read. The
primitives are temp_root::operand_needs_root, temp_root_push_double, temp_root_get_double, and operand_is_reloadable for operands that read a
registered root.
One trap, learned the hard way in #6983: the push must be interleaved with
the lowering loop, not appended after it. Rooting the finished list publishes an
already-dangling argument 0 to the scanner, which turns a silent DIFF into a
SIGSEGV — strictly worse than not rooting at all.
Detection
Same as the rest of the family: invisible with the conservative native-stack
scan on. Reproduces under PERRY_CONSERVATIVE_STACK_SCAN=off PERRY_GC_HEAP_LIMIT=8 with a shape like new Function(fresh(0), "return " + churn(N)), and under the evacuating arm
from #6977 (PERRY_GC_INCREMENTAL=0 + the above).
The new.rs half is fixed in #7699. builtin.rs is not, and I am leaving this issue open for it — see the inventory at the bottom.
What landed
The three branches this issue names all early-return before the main class loop ever adopts into the enclosing scope, so lower_new_impl's open_rooted_group was open around them and empty — lexical containment is not protection when nothing is adopted, and RootedGroup::release truncates from the group's own lowest slot, so an empty group emits nothing at all.
new Readline(…) — output sat in a bare SSA register across options' lowering and across every trailing argument's;
new <importedFn>(…) — func_double across every argument, and argument i across the arguments after it;
new Function(…) with a dynamic body — the same loop.
All three now adopt into the group as each operand is produced and re-read at the call. Interleaved, not appended: this issue's own trap ("rooting the finished list publishes an already-dangling argument 0 to the scanner, which turns a silent DIFF into a SIGSEGV") is restated on the shared adopt_constructor_args helper so the next edit reads it.
the undefined fillers for absent readline arguments stay literals, so new Readline() still costs no slot.
lower_js_args_array is confirmed to be no rescue and is untouched: alloca_entry_array emits a bare LLVM alloca with no js_shadow_slot_bind and no shadow-slot reservation (the opt-in is expr/scalar_slot_root.rs's root_entry_alloca, which nothing on these paths calls). It copies whatever bits it is handed, stale or not, so the repair has to happen before it runs — which is where it now is.
Still open: lower_call/builtin.rs
lower_builtin_new(ctx, class_name, args) takes no rooting context at all, and is called from lower_new_impl_inner before that function's group is populated — so these are structurally unreachable from a new.rs-level fix. Each needs its own rooting::with_operands_rooted over the argument list.
The arms that lower 2+ argument expressions sequentially and then use the earlier register(s):
Ruled out on inspection, so they are not missing from the list: EventEmitter, EventEmitterAsyncResource, BlockList, SocketAddress, X509Certificate, PerformanceObserver, StringDecoder, Decimal, CountQueuingStrategy, ByteLengthQueuingStrategy, CompressionStream, ReadableStreamBYOBReader, WeakMap, WeakSet — each lowers its extra arguments with let _ = lower_expr(...), so nothing is retained across the loop to go stale. Same for new.rs's own Crypto | CryptoKey | SubtleCrypto arm and the empty-placeholder fallback: they emit plenty of IR, but no value survives to be consumed later, which is a different thing from a zero-emission false positive.
CronJob is the one that needs care rather than a mechanical wrap: its first operand is a raw i64 from get_raw_string_ptr (which is lower_exprplus a real js_get_string_pointer_unified call), so it is #7280 taxonomy (a) and wants the derivation moved below the later operands, not a slot.
Activity
proggeramlug commentedon Aug 9, 2026
The
new.rshalf is fixed in #7699.builtin.rsis not, and I am leaving this issue open for it — see the inventory at the bottom.What landed
The three branches this issue names all early-
returnbefore the main class loop ever adopts into the enclosing scope, solower_new_impl'sopen_rooted_groupwas open around them and empty — lexical containment is not protection when nothing is adopted, andRootedGroup::releasetruncates from the group's own lowest slot, so an empty group emits nothing at all.new Readline(…)—outputsat in a bare SSA register acrossoptions' lowering and across every trailing argument's;new <importedFn>(…)—func_doubleacross every argument, and argumentiacross the arguments after it;new Function(…)with a dynamic body — the same loop.All three now adopt into the group as each operand is produced and re-read at the call. Interleaved, not appended: this issue's own trap ("rooting the finished list publishes an already-dangling argument 0 to the scanner, which turns a silent DIFF into a SIGSEGV") is restated on the shared
adopt_constructor_argshelper so the next edit reads it.Two details worth recording:
lower_constructor_argrather thanRootedGroup::lower, because it clearsctx.discard_expr_valuefor the operand — fix(codegen): a typed-array element store used as an expression evaluates to 0 — discard_expr_value leaks into operand position #7590: that flag means "this STATEMENT's value is discarded", is not cleared on recursion, and lowering an operand under it can evaluate a typed-array store to0;undefinedfillers for absent readline arguments stay literals, sonew Readline()still costs no slot.lower_js_args_arrayis confirmed to be no rescue and is untouched:alloca_entry_arrayemits a bare LLVMallocawith nojs_shadow_slot_bindand no shadow-slot reservation (the opt-in isexpr/scalar_slot_root.rs'sroot_entry_alloca, which nothing on these paths calls). It copies whatever bits it is handed, stale or not, so the repair has to happen before it runs — which is where it now is.Still open:
lower_call/builtin.rslower_builtin_new(ctx, class_name, args)takes no rooting context at all, and is called fromlower_new_impl_innerbefore that function's group is populated — so these are structurally unreachable from anew.rs-level fix. Each needs its ownrooting::with_operands_rootedover the argument list.The arms that lower 2+ argument expressions sequentially and then use the earlier register(s):
Uint8Array(≥2 args)js_uint8array_viewInt8Array…Float16Array, ≥2 args)js_typed_array_viewDataViewjs_data_view_newRegExpjs_regexp_constructEventjs_event_newCustomEventjs_custom_event_newDOMExceptionjs_dom_exception_newConsole(2-arg form)js_console_new2DatabaseSyncjs_node_sqlite_database_sync_newStatementSyncjs_node_sqlite_statement_sync_newSessionjs_node_sqlite_session_newCronJobjs_cron_job_newAsyncResourcejs_async_resource_newSuppressedErrorjs_suppressed_error_newResponse(init fields)js_response_newBlobjs_blob_newFilejs_file_newHeaders(per-key loop, accumulator + pair)js_headers_setRequest(init fields)js_request_new/_from_initReadableStreamjs_readable_stream_new_with_strategy_and_source_typeWritableStreamjs_writable_stream_new_with_sink_typeTransformStreamjs_transform_stream_newTextDecoderStreamjs_stream_web_text_decoder_stream_newRuled out on inspection, so they are not missing from the list:
EventEmitter,EventEmitterAsyncResource,BlockList,SocketAddress,X509Certificate,PerformanceObserver,StringDecoder,Decimal,CountQueuingStrategy,ByteLengthQueuingStrategy,CompressionStream,ReadableStreamBYOBReader,WeakMap,WeakSet— each lowers its extra arguments withlet _ = lower_expr(...), so nothing is retained across the loop to go stale. Same fornew.rs's ownCrypto | CryptoKey | SubtleCryptoarm and the empty-placeholder fallback: they emit plenty of IR, but no value survives to be consumed later, which is a different thing from a zero-emission false positive.CronJobis the one that needs care rather than a mechanical wrap: its first operand is a rawi64fromget_raw_string_ptr(which islower_exprplus a realjs_get_string_pointer_unifiedcall), so it is #7280 taxonomy (a) and wants the derivation moved below the later operands, not a slot.fix(codegen): root new's non-class ctor args and the computed-key win…
fix(codegen): the lowering-window cluster — push order, member update…
fix(gc): root builtin.rs's constructor arguments across each other's …
fix(gc): root builtin.rs's constructor arguments across each other's …
gc: fix the phi-edge checker false positives and the static-dispatch …
gc: fix root-dominance phi false positives + 3 of 5 real hits, lower …