diff --git a/changelog.d/7849-buffer-view-pointer-lifetime.md b/changelog.d/7849-buffer-view-pointer-lifetime.md new file mode 100644 index 0000000000..6f3e54af76 --- /dev/null +++ b/changelog.d/7849-buffer-view-pointer-lifetime.md @@ -0,0 +1,13 @@ +## Fixed + +- Native-region verification now tracks cached buffer-view pointer lifetime + independently of bounds and alias facts. Accessing a typed array's `.buffer` + directly or through a computed or named accessor invalidates every copied + view alias, runtime fallbacks retain that evidence, and checked or unchecked + native access through the invalidated pointer is rejected. Statically proven + canonical numeric string keys remain on the non-invalidating element path. + Scalar accesses through cached views must carry the same pointer-lifetime + evidence (#7220). + + Targeted verifier and native-proof regressions cover direct, computed, + copied-alias, scalar, and bulk-memory access after pointer invalidation. diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 98281412c0..d010c86150 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -9,7 +9,9 @@ use perry_hir::Function; use crate::expr::FnCtx; use crate::module::LlModule; -use crate::native_value::{AliasState, BufferElem, BufferIndexUnit, BufferViewSlot, LengthSource}; +use crate::native_value::{ + AliasState, BufferElem, BufferIndexUnit, BufferViewPointerState, BufferViewSlot, LengthSource, +}; use crate::stmt; use crate::strings::StringPool; use crate::types::{LlvmType, DOUBLE, I1, I32, I64, I8, PTR}; @@ -957,6 +959,7 @@ pub(super) fn compile_function( alias: AliasState::Unknown, length_source: Some(LengthSource::Unknown), native_owned: None, + pointer_state: BufferViewPointerState::Stable, // Declared-type hoist only — the construction form is unknown, // so no inline-storage proof. storage_inline_proven: false, @@ -1026,6 +1029,7 @@ pub(super) fn compile_function( None => LengthSource::Unknown, }), native_owned: None, + pointer_state: BufferViewPointerState::Stable, storage_inline_proven: true, }, ); diff --git a/crates/perry-codegen/src/expr/buffer_access.rs b/crates/perry-codegen/src/expr/buffer_access.rs index f8e5bc1e34..e8672090cc 100644 --- a/crates/perry-codegen/src/expr/buffer_access.rs +++ b/crates/perry-codegen/src/expr/buffer_access.rs @@ -8,7 +8,7 @@ use crate::native_value::{ use crate::types::{DOUBLE, F32, I16, I32, I8, PTR}; use super::{ - attach_native_owned_view_fact, bounds_for_buffer_access_width, buffer_alias_metadata_suffix, + attach_buffer_view_facts, bounds_for_buffer_access_width, buffer_alias_metadata_suffix, buffer_view_lowered_value, can_lower_expr_as_i32, effective_alias_state_for_access, int_range_expr, is_numeric_expr, lower_expr_native, FnCtx, }; @@ -248,6 +248,9 @@ pub(crate) fn lower_buffer_access_proof( }, _ => return Ok(None), }; + if !view.pointer_state.is_stable() { + return Ok(None); + } // A closure-captured buffer local is hazardous even before any escape // walk stamped `buffer_hazard_reasons` — the closure may mutate/realloc @@ -386,7 +389,7 @@ fn record_buffer_view( proof.may_emit_noalias, vec![format!("elem={:?}", proof.view.elem)], ); - attach_native_owned_view_fact(ctx, &proof.view); + attach_buffer_view_facts(ctx, &proof.view); } pub(crate) fn lower_buffer_load( @@ -423,7 +426,7 @@ pub(crate) fn lower_buffer_load( proof.may_emit_noalias, vec![format!("zext_to={}", result_i32)], ); - attach_native_owned_view_fact(ctx, &proof.view); + attach_buffer_view_facts(ctx, &proof.view); let result = LoweredValue::i32(result_i32); if let Some(consumer) = spec.result_consumer { let facts = access_facts_for_spec(spec, &proof.view, Some(&emission.len_i32)); @@ -442,7 +445,7 @@ pub(crate) fn lower_buffer_load( false, Vec::new(), ); - attach_native_owned_view_fact(ctx, &proof.view); + attach_buffer_view_facts(ctx, &proof.view); } Ok(Some(result)) } @@ -482,7 +485,7 @@ pub(crate) fn lower_buffer_store( proof.may_emit_noalias, vec![format!("source_i32={}", val_i32)], ); - attach_native_owned_view_fact(ctx, &proof.view); + attach_buffer_view_facts(ctx, &proof.view); let result = LoweredValue::i32(val_i32.clone()); Ok(Some(StoreResult { result })) } @@ -612,7 +615,7 @@ pub(crate) fn lower_typed_array_load( proof.may_emit_noalias, vec![format!("elem={:?}", proof.view.elem)], ); - attach_native_owned_view_fact(ctx, &proof.view); + attach_buffer_view_facts(ctx, &proof.view); Ok(Some(result)) } @@ -765,6 +768,6 @@ pub(crate) fn lower_typed_array_store( proof.may_emit_noalias, vec![format!("elem={:?}", proof.view.elem)], ); - attach_native_owned_view_fact(ctx, &proof.view); + attach_buffer_view_facts(ctx, &proof.view); Ok(Some(StoreResult { result })) } diff --git a/crates/perry-codegen/src/expr/buffer_views.rs b/crates/perry-codegen/src/expr/buffer_views.rs index bc93862302..d44c47a670 100644 --- a/crates/perry-codegen/src/expr/buffer_views.rs +++ b/crates/perry-codegen/src/expr/buffer_views.rs @@ -1,8 +1,8 @@ use perry_hir::{walker::walk_expr_children, Expr}; use crate::native_value::{ - AliasState, BoundsState, BufferElem, BufferIndexUnit, BufferViewSlot, LengthSource, - LoweredValue, MaterializationReason, NativeOwnedViewFact, + AliasState, BoundsState, BufferElem, BufferIndexUnit, BufferViewPointerState, BufferViewSlot, + LengthSource, LoweredValue, MaterializationReason, NativeOwnedViewFact, }; use crate::types::{I32, I64, I8, PTR}; @@ -111,6 +111,36 @@ pub(crate) fn downgrade_buffer_alias(ctx: &mut FnCtx<'_>, id: u32, reason: Mater ); } +/// Mark a cached data pointer as unusable after an operation changes which +/// storage the receiver aliases. Alias state alone cannot express this: the +/// pointer is stale, not merely shared. +pub(crate) fn invalidate_buffer_view_pointer( + ctx: &mut FnCtx<'_>, + id: u32, + reason: MaterializationReason, +) { + let affected_ids = if let Some(data_slot) = ctx + .buffer_view_slots + .get(&id) + .map(|view| view.data_slot.clone()) + { + ctx.buffer_view_slots + .iter() + .filter_map(|(view_id, view)| (view.data_slot == data_slot).then_some(*view_id)) + .collect::>() + } else { + vec![id] + }; + for affected_id in affected_ids { + if let Some(view) = ctx.buffer_view_slots.get_mut(&affected_id) { + view.pointer_state = BufferViewPointerState::Invalidated { + reason: reason.clone(), + }; + } + downgrade_buffer_alias(ctx, affected_id, reason.clone()); + } +} + fn owner_alias_invalidation_reason(reason: &MaterializationReason) -> MaterializationReason { match reason { MaterializationReason::UnknownCallEscape => MaterializationReason::MissingOwnerRoot, @@ -233,12 +263,27 @@ pub(crate) fn native_owned_fact_for_view(view: &BufferViewSlot) -> Option, view: &BufferViewSlot) { - let Some(fact) = native_owned_fact_for_view(view) else { +pub(crate) fn attach_buffer_view_facts(ctx: &mut FnCtx<'_>, view: &BufferViewSlot) { + if let Some(record) = ctx.native_rep_records.last_mut() { + record.buffer_view_pointer_state = Some(view.pointer_state.clone()); + record.native_owned_view = native_owned_fact_for_view(view); + } +} + +pub(crate) fn attach_buffer_view_pointer_state_for_expr(ctx: &mut FnCtx<'_>, expr: &Expr) { + let Expr::LocalGet(id) = expr else { + return; + }; + let Some(state) = ctx + .buffer_view_slots + .get(id) + .map(|view| view.pointer_state.clone()) + else { return; }; if let Some(record) = ctx.native_rep_records.last_mut() { - record.native_owned_view = Some(fact); + record.local_id = Some(*id); + record.buffer_view_pointer_state = Some(state); } } @@ -285,6 +330,7 @@ pub(crate) fn update_buffer_view_for_assignment( alias: AliasState::MayAlias, length_source: Some(LengthSource::Unknown), native_owned: None, + pointer_state: BufferViewPointerState::Stable, // Reassignment refresh: `Uint8ArrayNew` with a non-literal arg // can be the view form (`new Uint8Array(buffer)`), so the // inline-storage proof is not re-established here. diff --git a/crates/perry-codegen/src/expr/i32_fast_path.rs b/crates/perry-codegen/src/expr/i32_fast_path.rs index 0eafafd7d9..9f87bffff4 100644 --- a/crates/perry-codegen/src/expr/i32_fast_path.rs +++ b/crates/perry-codegen/src/expr/i32_fast_path.rs @@ -583,7 +583,8 @@ fn ta_int_elem_load_is_i32_provable(ctx: &FnCtx<'_>, object: &Expr, index: &Expr let Some(view) = ctx.buffer_view_slots.get(id) else { return false; }; - if view.index_unit != BufferIndexUnit::Element + if !view.pointer_state.is_stable() + || view.index_unit != BufferIndexUnit::Element || !view.alias.allows_noalias() || view.scope_idx.is_none() { diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 0a0964dd3d..e8d2fd73bf 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -31,12 +31,13 @@ use crate::type_analysis::{is_array_expr, is_numeric_expr, is_string_expr, recei use crate::types::{DOUBLE, I1, I16, I32, I64, I8}; use super::{ - array_kind_fact, buffer_access_materialization_reason, emit_typed_feedback_register_site, - expr_has_numeric_pointer_free_array_layout, int_range_expr, lower_buffer_load, lower_expr, - lower_expr_as_i32, lower_typed_array_load, materialize_js_value, raw_f64_layout_fact, - try_lower_flat_const_index_get, typed_feedback_emission_enabled, unbox_str_handle, - unbox_to_i64, BufferAccessSpec, FnCtx, PackedF64LoopFact, TypedFeedbackContract, - TypedFeedbackKind, + array_kind_fact, attach_buffer_view_pointer_state_for_expr, + buffer_access_materialization_reason, emit_typed_feedback_register_site, + expr_has_numeric_pointer_free_array_layout, int_range_expr, invalidate_buffer_view_pointer, + lower_buffer_load, lower_expr, lower_expr_as_i32, lower_typed_array_load, materialize_js_value, + raw_f64_layout_fact, try_lower_flat_const_index_get, typed_feedback_emission_enabled, + unbox_str_handle, unbox_to_i64, BufferAccessSpec, FnCtx, PackedF64LoopFact, + TypedFeedbackContract, TypedFeedbackKind, }; mod guarded_array; @@ -217,6 +218,40 @@ fn typed_array_index_needs_runtime_key(ctx: &FnCtx<'_>, object: &Expr, index: &E && !numeric_index_has_loop_array_index_proof(ctx, object, index) } +fn is_proven_canonical_numeric_string_literal(key: &[u8]) -> bool { + if matches!(key, b"-0" | b"NaN" | b"Infinity" | b"-Infinity") { + return true; + } + + let digits = key.strip_prefix(b"-").unwrap_or(key); + if digits.is_empty() + || (digits.len() > 1 && digits[0] == b'0') + || !digits.iter().all(u8::is_ascii_digit) + { + return false; + } + + // Decimal integers through Number.MAX_SAFE_INTEGER are exact, and this + // range is below the threshold where JS Number#toString switches to + // exponent notation. Their source spelling therefore proves + // CanonicalNumericIndexString without invoking runtime conversion. + digits + .iter() + .try_fold(0_u64, |value, digit| { + value.checked_mul(10)?.checked_add(u64::from(digit - b'0')) + }) + .is_some_and(|value| value <= 9_007_199_254_740_991) +} + +fn runtime_key_may_expose_typed_array_backing_buffer(index: &Expr) -> bool { + match index { + Expr::String(key) => !is_proven_canonical_numeric_string_literal(key.as_bytes()), + Expr::WtfString(key) => !is_proven_canonical_numeric_string_literal(key), + Expr::Integer(_) | Expr::Number(_) => false, + _ => true, + } +} + fn lower_array_index_get_via_runtime_key( ctx: &mut FnCtx<'_>, arr_box: &str, @@ -825,6 +860,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { return Ok(v); } if typed_array_index_needs_runtime_key(ctx, object.as_ref(), index.as_ref()) { + if runtime_key_may_expose_typed_array_backing_buffer(index) { + if let Expr::LocalGet(id) = object.as_ref() { + if ctx.buffer_view_slots.contains_key(id) { + invalidate_buffer_view_pointer( + ctx, + *id, + MaterializationReason::MutableAlias, + ); + } + } + } let arr_box = lower_expr(ctx, object)?; let key_box = lower_expr(ctx, index)?; let blk = ctx.block(); @@ -849,6 +895,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { false, vec!["typed_array_fallback=untracked_or_unproven".to_string()], ); + attach_buffer_view_pointer_state_for_expr(ctx, object); return Ok(result); } @@ -893,6 +940,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { false, vec!["typed_array_fallback=untracked_or_unproven".to_string()], ); + attach_buffer_view_pointer_state_for_expr(ctx, object); return Ok(result); } if is_uint8array_receiver(ctx, object) && is_numeric_expr(ctx, index) { diff --git a/crates/perry-codegen/src/expr/index_set.rs b/crates/perry-codegen/src/expr/index_set.rs index e5d7433c9b..21efadfb6c 100644 --- a/crates/perry-codegen/src/expr/index_set.rs +++ b/crates/perry-codegen/src/expr/index_set.rs @@ -42,9 +42,9 @@ use crate::types::{DOUBLE, I32, I64}; use super::index_set_typed_array::lower_inline_dyn_typed_array_set; use super::{ array_kind_fact, array_store_needs_layout_note, array_store_needs_write_barrier, - buffer_access_materialization_reason, emit_array_numeric_write_note_on_block, - emit_jsvalue_slot_store_on_block, emit_root_nanbox_store_on_block, - emit_typed_feedback_register_site, emit_write_barrier, + attach_buffer_view_pointer_state_for_expr, buffer_access_materialization_reason, + emit_array_numeric_write_note_on_block, emit_jsvalue_slot_store_on_block, + emit_root_nanbox_store_on_block, emit_typed_feedback_register_site, emit_write_barrier, expr_has_numeric_pointer_free_array_layout, int_range_expr, lower_buffer_store, lower_expr, lower_expr_as_i32, lower_expr_native, lower_index_set_fast, lower_typed_array_store, materialize_js_value, nanbox_pointer_inline, raw_f64_layout_fact, unbox_str_handle, @@ -881,6 +881,7 @@ pub(crate) fn lower( false, vec!["typed_array_fallback=untracked_or_unproven".to_string()], ); + attach_buffer_view_pointer_state_for_expr(ctx, object); return Ok(result); } @@ -911,6 +912,7 @@ pub(crate) fn lower( false, vec!["typed_array_fallback=untracked_or_unproven".to_string()], ); + attach_buffer_view_pointer_state_for_expr(ctx, object); return Ok(val_double); } if is_uint8array_receiver(ctx, object) && is_numeric_expr(ctx, index) { diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index dfd9306052..9d98fbba37 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -63,8 +63,9 @@ pub(crate) use buffer_access::{ lower_typed_array_store, BufferAccessSpec, }; pub(crate) use buffer_views::{ - alias_buffer_view_slot, attach_native_owned_view_fact, buffer_access_materialization_reason, - buffer_view_lowered_value, downgrade_buffer_alias, downgrade_buffer_aliases_in_expr, + alias_buffer_view_slot, attach_buffer_view_facts, attach_buffer_view_pointer_state_for_expr, + buffer_access_materialization_reason, buffer_view_lowered_value, downgrade_buffer_alias, + downgrade_buffer_aliases_in_expr, invalidate_buffer_view_pointer, invalidate_native_owned_views_for_dispose, native_arena_canonical_owner_id, record_native_arena_owner_assignment, update_buffer_view_for_assignment, }; diff --git a/crates/perry-codegen/src/expr/native_memory.rs b/crates/perry-codegen/src/expr/native_memory.rs index 9dbed12eab..53cff8b2f6 100644 --- a/crates/perry-codegen/src/expr/native_memory.rs +++ b/crates/perry-codegen/src/expr/native_memory.rs @@ -9,7 +9,7 @@ use crate::native_value::{ use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; use super::{ - attach_native_owned_view_fact, buffer_access_materialization_reason, buffer_view_lowered_value, + attach_buffer_view_facts, buffer_access_materialization_reason, buffer_view_lowered_value, effective_alias_state_for_access, lower_expr, lower_expr_native, unbox_to_i64, FnCtx, }; @@ -180,6 +180,9 @@ fn proven_view( return None; } let slot = ctx.buffer_view_slots.get(local_id)?.clone(); + if !slot.pointer_state.is_stable() { + return None; + } if slot.index_unit != BufferIndexUnit::Element { return None; } @@ -290,7 +293,7 @@ fn record_bulk_view( false, Vec::new(), ); - attach_native_owned_view_fact(ctx, view); + attach_buffer_view_facts(ctx, view); } fn record_runtime_fallback( diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index 69e5bafeab..31fb70ba24 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -95,7 +95,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if property == "buffer" { if let Expr::LocalGet(id) = object.as_ref() { if ctx.buffer_view_slots.contains_key(id) { - super::downgrade_buffer_alias( + super::invalidate_buffer_view_pointer( ctx, *id, crate::native_value::MaterializationReason::MutableAlias, diff --git a/crates/perry-codegen/src/expr/proven_view_access.rs b/crates/perry-codegen/src/expr/proven_view_access.rs index ce4e20e3c5..34becde97a 100644 --- a/crates/perry-codegen/src/expr/proven_view_access.rs +++ b/crates/perry-codegen/src/expr/proven_view_access.rs @@ -36,7 +36,9 @@ use anyhow::Result; use perry_hir::{BinaryOp, Expr}; -use super::{can_lower_expr_as_i32, lower_expr_as_i32, lower_expr_native, FnCtx}; +use super::{ + attach_buffer_view_facts, can_lower_expr_as_i32, lower_expr_as_i32, lower_expr_native, FnCtx, +}; use crate::nanbox::{double_literal, TAG_UNDEFINED}; use crate::native_value::{ BoundsState, BufferAccessMode, BufferElem, BufferIndexUnit, ExpectedNativeRep, LoweredValue, @@ -116,7 +118,8 @@ pub(crate) fn local_is_proven_int_store_view(ctx: &FnCtx<'_>, id: u32) -> bool { return false; } ctx.buffer_view_slots.get(&id).is_some_and(|view| { - view.storage_inline_proven + view.pointer_state.is_stable() + && view.storage_inline_proven && view.native_owned.is_none() && view.index_unit == BufferIndexUnit::Element && view.alias.allows_noalias() @@ -146,7 +149,8 @@ fn proven_view_for( return None; }; let view = ctx.buffer_view_slots.get(id)?.clone(); - if !view.storage_inline_proven + if !view.pointer_state.is_stable() + || !view.storage_inline_proven || view.native_owned.is_some() || view.index_unit != BufferIndexUnit::Element { @@ -302,6 +306,7 @@ pub(crate) fn try_lower_proven_view_checked_f64_load( false, vec!["proven_view=checked_inline; guards=none".to_string()], ); + attach_buffer_view_facts(ctx, &view); Ok(Some(result)) } @@ -438,5 +443,6 @@ pub(crate) fn try_lower_proven_view_checked_store( false, vec!["proven_view=checked_inline; guards=none".to_string()], ); + attach_buffer_view_facts(ctx, &view); Ok(Some(value_native)) } diff --git a/crates/perry-codegen/src/expr/record_value.rs b/crates/perry-codegen/src/expr/record_value.rs index f57445429a..442311849b 100644 --- a/crates/perry-codegen/src/expr/record_value.rs +++ b/crates/perry-codegen/src/expr/record_value.rs @@ -319,6 +319,7 @@ impl<'a> FnCtx<'a> { access_mode, buffer_access, native_owned_view: None, + buffer_view_pointer_state: None, materialization_reason, fallback_reason, native_value_state, diff --git a/crates/perry-codegen/src/native_value/artifact.rs b/crates/perry-codegen/src/native_value/artifact.rs index c5dfb07f80..fe3806bf99 100644 --- a/crates/perry-codegen/src/native_value/artifact.rs +++ b/crates/perry-codegen/src/native_value/artifact.rs @@ -270,6 +270,7 @@ pub(crate) struct NativeRepRecord { pub access_mode: Option, pub buffer_access: Option, pub native_owned_view: Option, + pub buffer_view_pointer_state: Option, pub materialization_reason: Option, pub fallback_reason: Option, pub native_value_state: NativeValueState, @@ -315,6 +316,7 @@ pub(crate) fn typed_clone_rejection_record( access_mode: None, buffer_access: None, native_owned_view: None, + buffer_view_pointer_state: None, materialization_reason: None, fallback_reason: None, native_value_state: NativeValueState::RegionLocal, @@ -609,7 +611,7 @@ pub(crate) fn write_native_rep_artifact_if_enabled( pid, wall_nonce, counter )); let artifact = NativeRepArtifact { - schema_version: 15, + schema_version: 16, module, records, pod_layouts: collect_pod_layouts(records), diff --git a/crates/perry-codegen/src/native_value/buffer.rs b/crates/perry-codegen/src/native_value/buffer.rs index 4b0d8dd818..f5a26c18f2 100644 --- a/crates/perry-codegen/src/native_value/buffer.rs +++ b/crates/perry-codegen/src/native_value/buffer.rs @@ -1,6 +1,6 @@ use serde::Serialize; -use super::rep::LoweredValue; +use super::{materialize::MaterializationReason, rep::LoweredValue}; #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -110,6 +110,24 @@ pub(crate) enum BufferAccessMode { DynamicFallback, } +/// Whether the data pointer cached for a tracked buffer view still names the +/// receiver's current storage. This is distinct from aliasing: a pointer may +/// safely alias another view, while exposing an inline typed array's `.buffer` +/// can make the cached pointer stale by rebinding the receiver to materialized +/// backing storage. +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +#[serde(tag = "state", rename_all = "snake_case")] +pub(crate) enum BufferViewPointerState { + Stable, + Invalidated { reason: MaterializationReason }, +} + +impl BufferViewPointerState { + pub(crate) fn is_stable(&self) -> bool { + matches!(self, Self::Stable) + } +} + #[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub(crate) struct BufferViewRep { @@ -183,6 +201,7 @@ pub(crate) struct BufferViewSlot { pub alias: AliasState, pub length_source: Option, pub native_owned: Option, + pub pointer_state: BufferViewPointerState, /// Representation-selection Phase 2: `true` when the receiver is PROVEN to /// be a freshly-constructed inline-storage (non-view) typed array / /// buffer — the construction form was a length or plain-array source, diff --git a/crates/perry-codegen/src/native_value/mod.rs b/crates/perry-codegen/src/native_value/mod.rs index 2af3fae4d7..68d5e7264f 100644 --- a/crates/perry-codegen/src/native_value/mod.rs +++ b/crates/perry-codegen/src/native_value/mod.rs @@ -12,8 +12,8 @@ pub(crate) use artifact::{ }; pub(crate) use buffer::{ AliasState, BoundedBufferIndex, BoundsProof, BoundsState, BufferAccessFacts, BufferAccessMode, - BufferAccessProof, BufferElem, BufferEndian, BufferIndexUnit, BufferViewSlot, - GuardedBufferIndex, LengthSource, NativeOwnedViewFact, NativeOwnedViewSlot, + BufferAccessProof, BufferElem, BufferEndian, BufferIndexUnit, BufferViewPointerState, + BufferViewSlot, GuardedBufferIndex, LengthSource, NativeOwnedViewFact, NativeOwnedViewSlot, }; pub(crate) use materialize::{ materialize_js_value, materialize_js_value_bits, materialize_js_value_without_record, diff --git a/crates/perry-codegen/src/native_value/verify.rs b/crates/perry-codegen/src/native_value/verify.rs index c10ede3486..30679a3d09 100644 --- a/crates/perry-codegen/src/native_value/verify.rs +++ b/crates/perry-codegen/src/native_value/verify.rs @@ -3,7 +3,7 @@ use anyhow::{bail, Result}; #[cfg(test)] use super::artifact::{NativeAbiTransitionOp, NativeAbiTransitionRecord}; use super::artifact::{NativeRepRecord, NativeValueState}; -use super::buffer::{AliasState, BoundsState, BufferAccessMode}; +use super::buffer::{AliasState, BoundsState, BufferAccessMode, BufferViewPointerState}; #[cfg(test)] use super::pod::recompute_layout_from_fields; use super::rep::NativeRep; @@ -231,6 +231,36 @@ pub(crate) fn verify_native_rep_records(records: &[NativeRepRecord]) -> Result<( record.function, record.block_label, record.consumer )); } + let uses_native_buffer_pointer = matches!( + record.access_mode.as_ref(), + Some(BufferAccessMode::UncheckedNative | BufferAccessMode::CheckedNative) + ); + if uses_native_buffer_pointer + && matches!( + record.buffer_view_pointer_state, + Some(BufferViewPointerState::Invalidated { .. }) + ) + { + errors.push(format!( + "{}:{} {} used an invalidated buffer-view data pointer", + record.function, record.block_label, record.consumer + )); + } + let uses_cached_buffer_view_pointer = matches!(record.native_rep, NativeRep::BufferView(_)) + || record.buffer_access.is_some() + || record + .notes + .iter() + .any(|note| note.starts_with("proven_view=checked_inline")); + if uses_native_buffer_pointer + && uses_cached_buffer_view_pointer + && record.buffer_view_pointer_state.is_none() + { + errors.push(format!( + "{}:{} {} native buffer-view access omitted pointer-lifetime evidence", + record.function, record.block_label, record.consumer + )); + } if matches!( record.access_mode.as_ref(), Some(BufferAccessMode::UncheckedNative) diff --git a/crates/perry-codegen/src/native_value/verify/tests.rs b/crates/perry-codegen/src/native_value/verify/tests.rs index f1aeccc5b4..740a9ab744 100644 --- a/crates/perry-codegen/src/native_value/verify/tests.rs +++ b/crates/perry-codegen/src/native_value/verify/tests.rs @@ -1,8 +1,8 @@ use super::{NativeAbiTransitionOp, NativeAbiTransitionRecord}; use crate::native_value::{ verify_native_rep_records, AliasState, BoundsProof, BoundsState, BufferAccessMode, - BufferViewRep, LoweredValue, MaterializationReason, NativeAbiDirection, NativeAbiTypeRecord, - NativeFactUse, NativeRep, NativeRepRecord, NativeValueState, SemanticKind, + BufferViewPointerState, BufferViewRep, LoweredValue, MaterializationReason, NativeAbiDirection, + NativeAbiTypeRecord, NativeFactUse, NativeRep, NativeRepRecord, NativeValueState, SemanticKind, }; use crate::types::{DOUBLE, F32, I32, I64, PTR}; @@ -33,6 +33,7 @@ fn record() -> NativeRepRecord { access_mode: None, buffer_access: None, native_owned_view: None, + buffer_view_pointer_state: None, materialization_reason: None, fallback_reason: None, native_value_state: NativeValueState::RegionLocal, @@ -465,6 +466,55 @@ fn accepts_unchecked_native_proven_and_guarded_bounds() { assert!(verify_native_rep_records(&[proven, guarded]).is_ok()); } +#[test] +fn rejects_native_access_through_invalidated_buffer_view_pointer() { + let mut r = record(); + r.access_mode = Some(BufferAccessMode::UncheckedNative); + r.bounds_state = Some(BoundsState::Proven { + proof: BoundsProof::ExplicitGuard, + }); + r.alias_state = Some(AliasState::NoAliasProven); + r.buffer_view_pointer_state = Some(BufferViewPointerState::Invalidated { + reason: MaterializationReason::MutableAlias, + }); + + let err = verify_native_rep_records(&[r]).expect_err("stale pointer must be rejected"); + assert!(err + .to_string() + .contains("invalidated buffer-view data pointer")); +} + +#[test] +fn rejects_scalar_cached_view_access_without_pointer_lifetime_evidence() { + let mut r = record(); + r.access_mode = Some(BufferAccessMode::CheckedNative); + r.bounds_state = Some(BoundsState::Proven { + proof: BoundsProof::ExplicitGuard, + }); + r.notes = vec!["proven_view=checked_inline; guards=none".to_string()]; + + let err = verify_native_rep_records(&[r]) + .expect_err("scalar access through a cached view must carry pointer-lifetime evidence"); + assert!(err + .to_string() + .contains("native buffer-view access omitted pointer-lifetime evidence")); +} + +#[test] +fn accepts_runtime_fallback_after_buffer_view_pointer_invalidation() { + let mut r = record(); + r.access_mode = Some(BufferAccessMode::DynamicFallback); + r.bounds_state = Some(BoundsState::Unknown); + r.materialization_reason = Some(MaterializationReason::MutableAlias); + r.fallback_reason = Some(MaterializationReason::MutableAlias); + r.native_value_state = NativeValueState::DynamicFallback; + r.buffer_view_pointer_state = Some(BufferViewPointerState::Invalidated { + reason: MaterializationReason::MutableAlias, + }); + + assert!(verify_native_rep_records(&[r]).is_ok()); +} + #[test] fn rejects_checked_native_without_real_bounds() { let mut r = record(); diff --git a/crates/perry-codegen/src/stmt/let_buffer_views.rs b/crates/perry-codegen/src/stmt/let_buffer_views.rs index 91c12c9f21..1be34ec9a9 100644 --- a/crates/perry-codegen/src/stmt/let_buffer_views.rs +++ b/crates/perry-codegen/src/stmt/let_buffer_views.rs @@ -4,7 +4,8 @@ use crate::expr::FnCtx; use crate::native_value::{ - AliasState, BufferElem, BufferIndexUnit, BufferViewSlot, LengthSource, NativeOwnedViewSlot, + AliasState, BufferElem, BufferIndexUnit, BufferViewPointerState, BufferViewSlot, LengthSource, + NativeOwnedViewSlot, }; use crate::types::{I32, I64, I8, PTR}; @@ -93,6 +94,7 @@ pub(super) fn register_noalias_buffer_view( alias: AliasState::NoAliasProven, length_source: Some(init.length_source), native_owned, + pointer_state: BufferViewPointerState::Stable, storage_inline_proven: init.storage_inline_proven, }, ); diff --git a/crates/perry-codegen/src/stmt/masked_window_region.rs b/crates/perry-codegen/src/stmt/masked_window_region.rs index 8b07904a92..a67c82d170 100644 --- a/crates/perry-codegen/src/stmt/masked_window_region.rs +++ b/crates/perry-codegen/src/stmt/masked_window_region.rs @@ -524,7 +524,8 @@ pub(super) fn try_match_masked_window_region( .buffer_view_slots .get(&access.array_id) .is_some_and(|view| { - view.storage_inline_proven + view.pointer_state.is_stable() + && view.storage_inline_proven && view.native_owned.is_none() && view.alias.allows_noalias() && view.scope_idx.is_some() diff --git a/crates/perry-codegen/tests/native_proof_buffer_views.rs b/crates/perry-codegen/tests/native_proof_buffer_views.rs index e93519c3c1..dbbdc4b18c 100644 --- a/crates/perry-codegen/tests/native_proof_buffer_views.rs +++ b/crates/perry-codegen/tests/native_proof_buffer_views.rs @@ -27,6 +27,9 @@ use native_proof_support::{ NativeRepsEnv, }; +#[path = "native_proof_buffer_views/pointer_lifetime.rs"] +mod pointer_lifetime; + fn empty_opts() -> CompileOptions { CompileOptions { target: None, diff --git a/crates/perry-codegen/tests/native_proof_buffer_views/pointer_lifetime.rs b/crates/perry-codegen/tests/native_proof_buffer_views/pointer_lifetime.rs new file mode 100644 index 0000000000..44fe5aa3d7 --- /dev/null +++ b/crates/perry-codegen/tests/native_proof_buffer_views/pointer_lifetime.rs @@ -0,0 +1,300 @@ +use super::*; + +#[test] +fn backing_buffer_exposure_records_pointer_invalidation_before_fallback() { + let body = vec![ + typed_array_let( + 1, + "words", + "Uint32Array", + perry_hir::TYPED_ARRAY_KIND_UINT32, + int(1), + ), + array_set(1, int(0), int(7)), + typed_array_let( + 2, + "bytes", + "Uint8Array", + perry_hir::TYPED_ARRAY_KIND_UINT8, + Expr::PropertyGet { + byte_offset: 0, + object: Box::new(local(1)), + property: "buffer".to_string(), + }, + ), + array_set(1, int(0), int(0x01020304)), + Stmt::Return(Some(index_get(2, int(0)))), + ]; + + let mut opts = empty_opts(); + opts.verify_native_regions = true; + let artifact = compile_artifact_json_for_module_with_opts( + module("buffer_pointer_lifetime_7220.ts", body), + opts, + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "TypedArraySet" + && record["local_id"] == 1 + && record["access_mode"] == "unchecked_native" + && record["buffer_view_pointer_state"]["state"] == "stable" + }), + "expected the pre-exposure access to carry stable-pointer evidence:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "TypedArraySet" + && record["local_id"] == 1 + && record["access_mode"] == "dynamic_fallback" + && record["materialization_reason"] == "mutable_alias" + && record["buffer_view_pointer_state"]["state"] == "invalidated" + && record["buffer_view_pointer_state"]["reason"] == "mutable_alias" + }), + "expected `.buffer` exposure to invalidate the cached pointer before fallback:\n{artifact:#}" + ); + assert!( + !records.iter().any(|record| { + record["local_id"] == 1 + && record["buffer_view_pointer_state"]["state"] == "invalidated" + && matches!( + record["access_mode"].as_str(), + Some("unchecked_native" | "checked_native") + ) + }), + "the invalidated words pointer must not reach a native access:\n{artifact:#}" + ); +} + +#[test] +fn computed_backing_buffer_exposure_invalidates_cached_pointer() { + let body = vec![ + typed_array_let( + 1, + "words", + "Uint32Array", + perry_hir::TYPED_ARRAY_KIND_UINT32, + int(1), + ), + Stmt::Expr(index_get(1, Expr::String("buffer".to_string()))), + array_set(1, int(0), int(0x01020304)), + Stmt::Return(Some(index_get(1, int(0)))), + ]; + + let mut opts = empty_opts(); + opts.verify_native_regions = true; + let artifact = compile_artifact_json_for_module_with_opts( + module("computed_buffer_pointer_lifetime_7220.ts", body), + opts, + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "TypedArrayGet" + && record["local_id"] == 1 + && record["access_mode"] == "dynamic_fallback" + && record["buffer_view_pointer_state"]["state"] == "invalidated" + }), + "computed `words[\"buffer\"]` must invalidate the cached pointer before fallback:\n\ + {artifact:#}" + ); + assert!( + !records.iter().any(|record| { + record["local_id"] == 1 + && record["buffer_view_pointer_state"]["state"] == "invalidated" + && matches!( + record["access_mode"].as_str(), + Some("unchecked_native" | "checked_native") + ) + }), + "the invalidated computed-property receiver must not reach native access:\n{artifact:#}" + ); +} + +#[test] +fn named_typed_array_getter_invalidates_cached_pointer() { + let body = vec![ + typed_array_let( + 1, + "words", + "Uint32Array", + perry_hir::TYPED_ARRAY_KIND_UINT32, + int(1), + ), + // A non-canonical named key uses ordinary [[Get]]. It can therefore + // invoke an accessor that reads `this.buffer` and rebinds storage. + Stmt::Expr(index_get(1, Expr::String("probe".to_string()))), + array_set(1, int(0), int(0x01020304)), + Stmt::Return(Some(index_get(1, int(0)))), + ]; + + let mut opts = empty_opts(); + opts.verify_native_regions = true; + let artifact = compile_artifact_json_for_module_with_opts( + module("named_getter_buffer_pointer_lifetime_7220.ts", body), + opts, + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "TypedArrayGet" + && record["local_id"] == 1 + && record["access_mode"] == "dynamic_fallback" + && record["buffer_view_pointer_state"]["state"] == "invalidated" + }), + "a named typed-array getter must invalidate the cached pointer before [[Get]]:\n\ + {artifact:#}" + ); + assert!( + !records.iter().any(|record| { + record["local_id"] == 1 + && record["buffer_view_pointer_state"]["state"] == "invalidated" + && matches!( + record["access_mode"].as_str(), + Some("unchecked_native" | "checked_native") + ) + }), + "the pointer invalidated by a named getter must not reach native access:\n{artifact:#}" + ); +} + +#[test] +fn canonical_numeric_string_get_preserves_cached_pointer() { + let body = vec![ + typed_array_let( + 1, + "words", + "Uint32Array", + perry_hir::TYPED_ARRAY_KIND_UINT32, + int(1), + ), + Stmt::Expr(index_get(1, Expr::String("0".to_string()))), + array_set(1, int(0), int(7)), + Stmt::Return(Some(index_get(1, int(0)))), + ]; + + let mut opts = empty_opts(); + opts.verify_native_regions = true; + let artifact = compile_artifact_json_for_module_with_opts( + module("numeric_string_buffer_pointer_lifetime_7220.ts", body), + opts, + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "TypedArrayGet" + && record["local_id"] == 1 + && record["access_mode"] == "dynamic_fallback" + && record["buffer_view_pointer_state"]["state"] == "stable" + }), + "canonical numeric strings cannot invoke a named accessor:\n{artifact:#}" + ); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "TypedArraySet" + && record["local_id"] == 1 + && matches!( + record["access_mode"].as_str(), + Some("unchecked_native" | "checked_native") + ) + && record["buffer_view_pointer_state"]["state"] == "stable" + }), + "a canonical numeric-string get should preserve later native access:\n{artifact:#}" + ); +} + +#[test] +fn backing_buffer_exposure_invalidates_copied_view_alias_group() { + let body = vec![ + typed_array_let( + 1, + "words", + "Uint32Array", + perry_hir::TYPED_ARRAY_KIND_UINT32, + int(1), + ), + Stmt::Let { + id: 2, + name: "alias".to_string(), + ty: Type::Named("Uint32Array".to_string()), + mutable: false, + init: Some(local(1)), + }, + Stmt::Expr(Expr::PropertyGet { + byte_offset: 0, + object: Box::new(local(2)), + property: "buffer".to_string(), + }), + array_set(1, int(0), int(0x01020304)), + Stmt::Return(Some(index_get(1, int(0)))), + ]; + + let mut opts = empty_opts(); + opts.verify_native_regions = true; + let artifact = compile_artifact_json_for_module_with_opts( + module("aliased_buffer_pointer_lifetime_7220.ts", body), + opts, + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "TypedArraySet" + && record["local_id"] == 1 + && record["access_mode"] == "dynamic_fallback" + && record["buffer_view_pointer_state"]["state"] == "invalidated" + }), + "exposing an alias's backing buffer must invalidate every cached pointer sharing its \ + data slot:\n{artifact:#}" + ); +} + +#[test] +fn native_memory_bulk_access_rejects_invalidated_cached_pointer() { + let body = vec![ + typed_array_let( + 1, + "words", + "Uint32Array", + perry_hir::TYPED_ARRAY_KIND_UINT32, + int(4), + ), + Stmt::Expr(Expr::PropertyGet { + byte_offset: 0, + object: Box::new(local(1)), + property: "buffer".to_string(), + }), + Stmt::Expr(Expr::NativeMemoryFillU32 { + view: Box::new(local(1)), + value: Box::new(int(0)), + }), + Stmt::Return(Some(index_get(1, int(0)))), + ]; + + let mut opts = empty_opts(); + opts.verify_native_regions = true; + let artifact = compile_artifact_json_for_module_with_opts( + module("native_memory_buffer_pointer_lifetime_7220.ts", body), + opts, + ); + let records = artifact["records"].as_array().unwrap(); + assert!( + records.iter().any(|record| { + record["expr_kind"] == "NativeMemoryFillU32" + && record["consumer"] == "NativeMemoryFillU32.runtime_fallback" + && record["access_mode"] == "dynamic_fallback" + }), + "bulk access through an invalidated cached pointer must use the runtime fallback:\n\ + {artifact:#}" + ); + assert!( + !records.iter().any(|record| { + record["expr_kind"] == "NativeMemoryFillU32" + && matches!( + record["consumer"].as_str(), + Some("NativeMemoryFillU32.memset_zero" | "NativeMemoryFillU32.store_loop") + ) + }), + "bulk access through an invalidated cached pointer must not emit native writes:\n\ + {artifact:#}" + ); +} diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index a7f22ee600..4d233d4ce0 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -700,7 +700,7 @@ fn artifact_schema_v6_records_consumed_native_facts_for_buffer_region() { ]; let artifact = compile_artifact_json("artifact_positive_buffer_region.ts", body); - assert_eq!(artifact["schema_version"], 15); + assert_eq!(artifact["schema_version"], 16); let records = artifact["records"].as_array().unwrap(); assert!( records.iter().any(|record| { @@ -733,7 +733,7 @@ fn artifact_schema_v6_records_rejected_facts_for_buffer_fallback() { ]; let artifact = compile_artifact_json("artifact_rejected_buffer_region.ts", body); - assert_eq!(artifact["schema_version"], 15); + assert_eq!(artifact["schema_version"], 16); let records = artifact["records"].as_array().unwrap(); assert!( records.iter().any(|record| { @@ -780,7 +780,7 @@ fn artifact_schema_v6_records_c_layout_pod_manifest() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_record.ts", body); - assert_eq!(artifact["schema_version"], 15); + assert_eq!(artifact["schema_version"], 16); assert_eq!(artifact["summary"]["pod_layout_count"], 1); assert_eq!(artifact["summary"]["pod_record_count"], 1); let layouts = artifact["pod_layouts"].as_array().unwrap(); @@ -1278,7 +1278,7 @@ fn artifact_schema_v6_records_pod_dynamic_write_fallback() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_dynamic_write.ts", body); - assert_eq!(artifact["schema_version"], 15); + assert_eq!(artifact["schema_version"], 16); assert!( artifact["records"] .as_array() @@ -1515,7 +1515,7 @@ fn artifact_schema_v8_rejects_inexact_pod_initializer_values() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_init_reject.ts", body); - assert_eq!(artifact["schema_version"], 15); + assert_eq!(artifact["schema_version"], 16); assert_eq!(artifact["summary"]["pod_layout_count"], 0); assert_eq!(artifact["summary"]["pod_record_count"], 0); assert!(artifact["pod_layouts"].as_array().unwrap().is_empty()); @@ -1567,7 +1567,7 @@ fn artifact_schema_v6_records_pod_pointerful_field_rejection() { ]; let artifact = compile_artifact_json("artifact_c_layout_pod_reject.ts", body); - assert_eq!(artifact["schema_version"], 15); + assert_eq!(artifact["schema_version"], 16); assert_eq!(artifact["summary"]["pod_layout_count"], 0); assert!(artifact["pod_layouts"].as_array().unwrap().is_empty()); assert!( diff --git a/test-files/test_gap_7220_typed_array_named_getter_pointer.ts b/test-files/test_gap_7220_typed_array_named_getter_pointer.ts new file mode 100644 index 0000000000..de778f75f2 --- /dev/null +++ b/test-files/test_gap_7220_typed_array_named_getter_pointer.ts @@ -0,0 +1,19 @@ +// A named typed-array property uses ordinary [[Get]]. Its getter may expose +// `.buffer`, which materializes backing storage and can rebind the view away +// from the codegen-cached element pointer. +let exposed: any = null; + +Object.defineProperty(Uint32Array.prototype, "probe", { + configurable: true, + get() { + exposed = this.buffer; + return "getter-ran"; + }, +}); + +const words = new Uint32Array(1); +console.log("named getter:", words["probe" as any]); +words[0] = 0x01020304; + +const bytes = new Uint8Array(exposed); +console.log("shared bytes:", bytes[0], bytes[1], bytes[2], bytes[3]);