Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions .github/workflows/ext-link.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,63 @@ jobs:
# failing `cargo test --no-run` IS the regression this job exists to
# catch, so its exit status must abort the step under any shell.
set -euo pipefail
args=""
while read -r pkg; do
[ -n "$pkg" ] || continue
args="$args -p $pkg"
done <<< "$PKGS"

# `--message-format=json` so the count below comes from cargo rather
# than being inferred from exit status. The counting lives in the
# script (`--count-linked`) so this file carries no inline Python —
# a heredoc at column 0 silently breaks the YAML block scalar.
# shellcheck disable=SC2086
cargo test --release --no-run --message-format=json $args > /tmp/ext-link.json
#
# First check each package independently. This catches undeclared
# feature dependencies (for example Argon2 relying on another package
# to enable rand_core/getrandom) without paying for a full release
# link per package.
default_args=()
fetch_args=()
ws_args=()
while read -r pkg; do
[ -n "$pkg" ] || continue
echo "Checking ${pkg}'s independent feature graph..."
cargo check --release -p "$pkg"
case "$pkg" in
perry-ext-fetch)
fetch_args+=(-p "$pkg")
;;
perry-ext-fastify|perry-ext-ws)
ws_args+=(-p "$pkg")
;;
*)
default_args+=(-p "$pkg")
;;
esac
done <<< "$PKGS"

# Release-link packages in three feature-compatible groups. A single
# all-package command lets perry-ext-fetch's
# `external-fetch-symbols` and the WS providers' corresponding mode
# leak into unrelated test binaries, which then expect provider
# symbols they correctly do not link. Three groups preserve the real
# feature boundary while compiling the expensive runtime only a few
# times, keeping the gate inside its measured 120-minute backstop.
: > /tmp/ext-link.json
run_link_group() {
local group="$1"
shift
[ "$#" -gt 0 ] || return 0
local group_json="/tmp/ext-link-${group}.json"
echo "Linking ${group} feature group..."
set +e
cargo test --release --no-run --message-format=json "$@" > "$group_json"
local cargo_status=$?
set -e
cat "$group_json" >> /tmp/ext-link.json
if [ "$cargo_status" -ne 0 ]; then
jq -r 'select(.reason == "compiler-message") | .message.rendered // empty' \
"$group_json" >&2
exit "$cargo_status"
fi
}
run_link_group default "${default_args[@]}"
run_link_group fetch "${fetch_args[@]}"
run_link_group ws "${ws_args[@]}"

linked="$(python3 scripts/ci_ext_link_scope.py --count-linked /tmp/ext-link.json)"
echo "linked test binaries: $linked"
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,15 @@ jobs:
uses: dtolnay/rust-toolchain@stable
- uses: ./.github/actions/setup-llvm22

- name: Set up ARM64 MSVC environment
# The native ARM64 runner has the Windows SDK installed, but its
# PowerShell environment does not include the SDK's ARM64 library
# directories. Populate LIB / INCLUDE / PATH so the final PE link can
# resolve system import libraries such as user32.lib.
uses: ilammy/msvc-dev-cmd@v1
with:
arch: amd64_arm64

- uses: Swatinem/rust-cache@v2
with:
shared-key: "${{ runner.os }}-${{ runner.arch }}-perry"
Expand All @@ -1318,7 +1327,12 @@ jobs:
--target windows-aarch64 `
--no-auto-optimize `
-o target/windows-arm64-smoke.exe
$output = target/windows-arm64-smoke.exe
# In expression position PowerShell treats a relative path as a
# command name and does not search the working directory. Invoke the
# freshly linked binary explicitly so the ARM64 gate tests the
# artifact instead of failing in shell dispatch.
$smoke = (Resolve-Path 'target/windows-arm64-smoke.exe').Path
$output = & $smoke
if ($output.Trim() -ne 'ok') {
throw "unexpected ARM64 smoke output: '$output'"
}
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions changelog.d/8087-release-readiness-gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Restore the pre-release CI gates after several independent maintenance changes
had outpaced their checks. Test-only helpers and duplicated unwind context
declarations were still compiled in product targets, so the warning-denied jobs
failed even though normal builds succeeded; `eh.rs`, `eh_walker.rs`, and the GC
stack-map modules now share the real unwind ABI, while unused helpers in the GC
and CLI crates are limited to tests or removed. The structural audits had also
drifted: `property_set.rs` placed a valid pointer-free store marker outside the
inventory's bounded context, a test reused a class ID, and
`global_sink_isolation.py` treated immutable `RealmAtomic` handles as shared
state even though their mutable slots are `perry_thread_local!`. The audit now
resolves only the runtime's actual wrapper types, including qualified paths,
and rejects unrelated aliases.

GC allocation windows in `json_tape.rs` and `object/spill.rs` now reacquire raw
pointers through rooted handles, lowering the raw-handle debt baseline instead
of raising it. The two timer drain tests moved to
`timer/drain_expired_tests.rs`, returning `timer.rs` below the 2,000-line gate.
Finally, the Windows ARM64 workflow initializes the ARM64 MSVC environment so
the linker can find the installed Windows SDK import libraries, then resolves
the linked executable and invokes it with PowerShell's call operator. The smoke
gate now reaches the final PE link and runs the artifact instead of relying on
an uninitialized SDK path or looking for the relative executable in `PATH`.

Validation covered warning-denied runtime, product, and host-compatible
workspace checks; both CI clippy scopes; the pre-tag structural audit suite;
the raw-handle, store-site, class-ID, file-size, and global-sink self-tests and
real-tree audits; targeted moving-GC, unwind, timer, class, compile-cache, and
publish-config tests; workflow lint; the RustSec audit; and the Windows command
path through actionlint. The repository's gated release sweep and PR checks
provide the remaining platform-hosted coverage.

Release-sweep tier 1 now mirrors that CI contract instead of running
`perry-runtime` inside a parallel workspace test: it excludes the runtime from
the normal workspace pass and invokes its release tests separately with
`RUST_TEST_THREADS=1`, preventing shared test-state races from masquerading as
release regressions.

The native GC root-dominance corpus also reads the production statepoint pass
constant independently of rustfmt's one-line or wrapped layout, preserving the
single-source drift check when the Rust declaration is reformatted.

The full extension release-link gate now checks every extension independently,
then links three feature-compatible groups so provider runtime features cannot
leak into unrelated test binaries. This prevents `perry-ext-fetch`'s
external-symbol mode from making node-forge expect fetch symbols it does not
link without multiplying the expensive release link across every package. If
Cargo does fail, its captured structured diagnostics are rendered back into the
Actions log instead of being lost behind the final compilation summary.

The scoped end-to-end inventory now classifies the typed-array local-length
specialization suite, keeping the codegen source-to-suite map complete as new
in-process regression coverage is added.

The Argon2 extension now enables `rand_core`'s OS-randomness feature directly,
so its salt generation builds in isolation instead of depending on another
workspace package to feature-unify `OsRng` into the graph.
4 changes: 2 additions & 2 deletions crates/perry-codegen/src/expr/property_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1464,14 +1464,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
// Guarded raw-f64 slots are pointer-free by typed
// shape descriptor; non-number writes miss the
// guard and use the boxed setter fallback.
// GC_STORE_AUDIT(POINTER_FREE): typed raw-f64 class
// slots contain numbers only.
let blk = ctx.block();
let numeric_value =
canonicalize_raw_f64_numeric_store_value(
blk,
&val_double,
);
// GC_STORE_AUDIT(POINTER_FREE): typed raw-f64 class
// slots contain numbers only.
blk.store(DOUBLE, &numeric_value, &field_ptr);
Some(numeric_value)
} else {
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-codegen/src/lower_call/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,13 @@ pub(super) fn lower_builtin_new<'a>(
// new Headers(init?) — init can be an object literal or another
// Headers/array iterable.
let h = ctx.block().call(DOUBLE, "js_headers_new", &[]);
// `js_headers_new` produces the only reference to the new handle.
// Initializer evaluation and string coercion can collect, so keep
// that emitted value in the surrounding constructor root group and
// re-read it at every use below. In particular, a dynamic header
// value may run `toString()` before `js_headers_set` consumes the
// handle (#8087's native statepoint corpus caught this window).
let h_root = group.adopt_emitted(ctx, rooting::Repr::Boxed, &h, !args.is_empty());
if !args.is_empty() {
if let Some(props) = extract_options_fields(ctx, &args[0]) {
for (k, vexpr) in &props {
Expand All @@ -1168,6 +1175,7 @@ pub(super) fn lower_builtin_new<'a>(
let val_ptr =
ctx.block()
.call(I64, "js_jsvalue_to_string", &[(DOUBLE, &value)]);
let h = group.reread_emitted(ctx, h_root);
ctx.block().call(
DOUBLE,
"js_headers_set",
Expand All @@ -1176,14 +1184,15 @@ pub(super) fn lower_builtin_new<'a>(
}
} else {
let init = lower_expr(ctx, &args[0])?;
let h = group.reread_emitted(ctx, h_root);
ctx.block().call(
DOUBLE,
"js_headers_init_from_value",
&[(DOUBLE, &h), (DOUBLE, &init)],
);
}
}
Ok(Some(h))
Ok(Some(group.reread_emitted(ctx, h_root)))
}

"FormData" => {
Expand Down
35 changes: 20 additions & 15 deletions crates/perry-codegen/src/lower_call/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use anyhow::Result;
use perry_hir::Expr;

use crate::expr::{lower_expr, FnCtx};
use crate::rooting::{with_rooted_group, Repr};
use crate::types::{DOUBLE, I64};

mod abort;
Expand All @@ -45,21 +46,25 @@ pub(in crate::lower_call) fn build_headers_from_object(
ctx: &mut FnCtx<'_>,
props: &[(String, Expr)],
) -> Result<String> {
let h = ctx.block().call(DOUBLE, "js_headers_new", &[]);
for (k, vexpr) in props {
let key_expr = Expr::String(k.clone());
let key_ptr = get_raw_string_ptr(ctx, &key_expr)?;
let value = lower_expr(ctx, vexpr)?;
let val_ptr = ctx
.block()
.call(I64, "js_jsvalue_to_string", &[(DOUBLE, &value)]);
ctx.block().call(
DOUBLE,
"js_headers_set",
&[(DOUBLE, &h), (I64, &key_ptr), (I64, &val_ptr)],
);
}
Ok(h)
with_rooted_group(ctx, 1, |ctx, group| {
let h = ctx.block().call(DOUBLE, "js_headers_new", &[]);
let h_root = group.adopt_emitted(ctx, Repr::Boxed, &h, !props.is_empty());
for (k, vexpr) in props {
let key_expr = Expr::String(k.clone());
let key_ptr = get_raw_string_ptr(ctx, &key_expr)?;
let value = lower_expr(ctx, vexpr)?;
let val_ptr = ctx
.block()
.call(I64, "js_jsvalue_to_string", &[(DOUBLE, &value)]);
let h = group.reread_emitted(ctx, h_root);
ctx.block().call(
DOUBLE,
"js_headers_set",
&[(DOUBLE, &h), (I64, &key_ptr), (I64, &val_ptr)],
);
}
Ok(group.reread_emitted(ctx, h_root))
})
}

/// Phase 3 compat: extract `{key: value, ...}` pairs from an options
Expand Down
1 change: 1 addition & 0 deletions crates/perry-ext-argon2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ crate-type = ["staticlib", "rlib"]
[dependencies]
perry-ffi.workspace = true
argon2 = "0.5"
rand_core = { version = "0.6", features = ["getrandom"] }

[dev-dependencies]
perry-ffi = { workspace = true, features = ["runtime-link"] }
3 changes: 2 additions & 1 deletion crates/perry-ext-argon2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
//! perry-ffi v0.5.1's async surface — same recipe as bcrypt.

use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
use perry_ffi::{
alloc_string, read_string, spawn_blocking, JsPromise, JsString, Promise, StringHeader,
};
use rand_core::OsRng;

/// `argon2.hash(password) -> Promise<string>` — Argon2id with
/// default parameters.
Expand Down
7 changes: 5 additions & 2 deletions crates/perry-runtime/src/eh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ extern "C" {
fn _Unwind_SetIP(ctx: *mut UnwindContext, value: usize);
fn _Unwind_GetCFA(ctx: *mut UnwindContext) -> usize;
fn _Unwind_Backtrace(
trace: extern "C" fn(*mut UnwindContext, *mut core::ffi::c_void) -> UnwindReasonCode,
trace: unsafe extern "C" fn(*mut UnwindContext, *mut core::ffi::c_void) -> UnwindReasonCode,
arg: *mut core::ffi::c_void,
) -> UnwindReasonCode;
}
Expand Down Expand Up @@ -113,7 +113,10 @@ fn selfcheck_frame_a() -> usize {

#[inline(never)]
fn selfcheck_frame_b() -> usize {
extern "C" fn count(_ctx: *mut UnwindContext, arg: *mut core::ffi::c_void) -> UnwindReasonCode {
unsafe extern "C" fn count(
_ctx: *mut UnwindContext,
arg: *mut core::ffi::c_void,
) -> UnwindReasonCode {
unsafe { *(arg as *mut usize) += 1 };
// _URC_NO_REASON: the ONLY value that lets _Unwind_Backtrace keep
// walking — any other reason code stops the trace after one frame.
Expand Down
9 changes: 5 additions & 4 deletions crates/perry-runtime/src/eh_walker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -918,17 +918,18 @@ mod tests {
/// Collect frame PCs via the SYSTEM unwinder (_Unwind_Backtrace) —
/// the oracle the owned walk must match.
fn system_pcs(max: usize) -> Vec<u64> {
use crate::eh::UnwindContext;
use core::ffi::{c_int, c_void};
unsafe extern "C" {
fn _Unwind_Backtrace(
trace: extern "C" fn(*mut c_void, *mut c_void) -> c_int,
trace: unsafe extern "C" fn(*mut UnwindContext, *mut c_void) -> c_int,
arg: *mut c_void,
) -> c_int;
fn _Unwind_GetIP(ctx: *mut c_void) -> u64;
fn _Unwind_GetIP(ctx: *mut UnwindContext) -> usize;
}
extern "C" fn cb(ctx: *mut c_void, arg: *mut c_void) -> c_int {
unsafe extern "C" fn cb(ctx: *mut UnwindContext, arg: *mut c_void) -> c_int {
let v = unsafe { &mut *(arg as *mut Vec<u64>) };
unsafe { v.push(_Unwind_GetIP(ctx)) };
unsafe { v.push(_Unwind_GetIP(ctx) as u64) };
0
}
let mut v: Vec<u64> = Vec::with_capacity(max);
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/oldgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1836,6 +1836,7 @@ pub(super) fn evacuate_tenured_nursery_objects_collecting(
evacuated
}

#[cfg(test)]
pub(super) fn old_object_pages_all_selected(
header: *mut GcHeader,
total_size: usize,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ pub(super) fn gc_register_mutable_root_scanner_named_with_source(
});
}

#[cfg(test)]
pub(super) fn gc_register_budgeted_mutable_root_scanner_with_source(
scanner: MutableRootScanner,
budgeted_scanner: BudgetedMutableRootScanner,
Expand Down
9 changes: 3 additions & 6 deletions crates/perry-runtime/src/gc/roots/stack_maps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1340,10 +1340,7 @@ fn loaded_stack_map_section() -> Option<&'static [u8]> {
mod unwind {
use super::*;

#[repr(C)]
struct UnwindContext {
_private: [u8; 0],
}
type UnwindContext = crate::eh::UnwindContext;

unsafe extern "C" {
fn _Unwind_Backtrace(
Expand Down Expand Up @@ -1724,8 +1721,8 @@ mod fp_chain {
#[cfg(target_vendor = "apple")]
fn stack_top() -> usize {
unsafe extern "C" {
fn pthread_self() -> usize;
fn pthread_get_stackaddr_np(thread: usize) -> *mut c_void;
fn pthread_self() -> *mut c_void;
fn pthread_get_stackaddr_np(thread: *mut c_void) -> *mut c_void;
}
unsafe { pthread_get_stackaddr_np(pthread_self()) as usize }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@
use std::ffi::c_void;
use std::hint::black_box;

#[repr(C)]
struct UnwindContext {
_private: [u8; 0],
}
use crate::eh::UnwindContext;

unsafe extern "C" {
fn _Unwind_Backtrace(
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/roots/stack_maps_verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ fn describe(out: &mut String, index: &StackMapIndex, root: &ResolvedRoot) {
/// of the very data under suspicion. This is the same set the walker's
/// `match_records` containment check consults, so a dump gated on it reads
/// only what the walk already read.
#[cfg(any(target_arch = "aarch64", test))]
fn map_vouches_for(index: &StackMapIndex, function_address: usize) -> bool {
index
.function_starts
Expand Down
Loading
Loading