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
13 changes: 13 additions & 0 deletions changelog.d/7849-buffer-view-pointer-lifetime.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 5 additions & 1 deletion crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1026,6 +1029,7 @@ pub(super) fn compile_function(
None => LengthSource::Unknown,
}),
native_owned: None,
pointer_state: BufferViewPointerState::Stable,
storage_inline_proven: true,
},
);
Expand Down
17 changes: 10 additions & 7 deletions crates/perry-codegen/src/expr/buffer_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

pub(crate) fn lower_buffer_load(
Expand Down Expand Up @@ -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));
Expand All @@ -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))
}
Expand Down Expand Up @@ -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 }))
}
Expand Down Expand Up @@ -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))
}

Expand Down Expand Up @@ -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 }))
}
56 changes: 51 additions & 5 deletions crates/perry-codegen/src/expr/buffer_views.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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::<Vec<_>>()
} 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());
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
fn owner_alias_invalidation_reason(reason: &MaterializationReason) -> MaterializationReason {
match reason {
MaterializationReason::UnknownCallEscape => MaterializationReason::MissingOwnerRoot,
Expand Down Expand Up @@ -233,12 +263,27 @@ pub(crate) fn native_owned_fact_for_view(view: &BufferViewSlot) -> Option<Native
.map(|native| native.fact(view.element_width_bytes, alias_group))
}

pub(crate) fn attach_native_owned_view_fact(ctx: &mut FnCtx<'_>, 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);
}
}

Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-codegen/src/expr/i32_fast_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
60 changes: 54 additions & 6 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
fn lower_array_index_get_via_runtime_key(
ctx: &mut FnCtx<'_>,
arr_box: &str,
Expand Down Expand Up @@ -825,6 +860,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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();
Expand All @@ -849,6 +895,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
false,
vec!["typed_array_fallback=untracked_or_unproven".to_string()],
);
attach_buffer_view_pointer_state_for_expr(ctx, object);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return Ok(result);
}

Expand Down Expand Up @@ -893,6 +940,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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) {
Expand Down
8 changes: 5 additions & 3 deletions crates/perry-codegen/src/expr/index_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
7 changes: 5 additions & 2 deletions crates/perry-codegen/src/expr/native_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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,
Expand Down
Loading
Loading