use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use boa_engine::property::PropertyDescriptor;
use boa_engine::property::PropertyKey;
use boa_engine::{Context, JsObject, JsValue, js_string};
struct RootSnapshot {
name: &'static str,
object: JsObject,
keys: Vec<PropertyKey>,
identity: Vec<Vec<JsValue>>,
descriptors_hash: u64,
proto: Option<JsObject>,
}
impl RootSnapshot {
fn capture(name: &'static str, object: JsObject, ctx: &mut Context) -> Self {
let keys = object.own_property_keys(ctx).unwrap_or_default();
let identity = keys
.iter()
.map(|key| {
object
.borrow()
.properties()
.get(key)
.map(|desc| identity_values(&desc))
.unwrap_or_default()
})
.collect::<Vec<_>>();
let descriptors_hash = descriptors_hash(&object, &keys, &identity);
let proto = object.prototype();
Self {
name,
object,
keys,
identity,
descriptors_hash,
proto,
}
}
fn matches(&self, ctx: &mut Context) -> bool {
let Ok(keys) = self.object.own_property_keys(ctx) else {
return false;
};
if !key_sets_equal(&keys, &self.keys) {
return false;
}
if !protos_equal(self.object.prototype(), self.proto.as_ref()) {
return false;
}
descriptors_hash(&self.object, &self.keys, &self.identity) == self.descriptors_hash
}
}
pub(super) struct IntegrityBaseline {
global_keys: Vec<PropertyKey>,
roots: Vec<RootSnapshot>,
}
impl IntegrityBaseline {
pub(super) fn empty() -> Self {
Self {
global_keys: Vec::new(),
roots: Vec::new(),
}
}
pub(super) fn capture(ctx: &mut Context) -> Self {
let global = ctx.global_object();
let global_keys = global.own_property_keys(ctx).unwrap_or_default();
let eval_object = global
.get(js_string!("eval"), ctx)
.ok()
.and_then(|v| v.as_object());
let mut roots = Vec::with_capacity(4);
if let Some(eval_object) = eval_object {
roots.push(RootSnapshot::capture("eval", eval_object, ctx));
}
let constructors = ctx.intrinsics().constructors();
for (name, proto) in [
("Object.prototype", constructors.object().prototype()),
("Array.prototype", constructors.array().prototype()),
("Function.prototype", constructors.function().prototype()),
] {
roots.push(RootSnapshot::capture(name, proto, ctx));
}
Self { global_keys, roots }
}
pub(super) fn eval_object(&self) -> Option<&JsObject> {
self.roots
.iter()
.find(|root| root.name == "eval")
.map(|root| &root.object)
}
pub(super) fn contains_global_key(&self, key: &PropertyKey) -> bool {
self.global_keys.contains(key)
}
pub(super) fn verify(&self, ctx: &mut Context) -> bool {
let global = ctx.global_object();
let keys = global.own_property_keys(ctx).unwrap_or_default();
if !key_sets_equal(&keys, &self.global_keys) {
return false;
}
let Some(eval_object) = self.eval_object() else {
return false;
};
let eval_object = eval_object.clone();
match global.get(js_string!("eval"), ctx) {
Ok(value) if value.strict_equals(&JsValue::from(eval_object)) => {}
_ => return false,
}
for root in &self.roots {
if !root.matches(ctx) {
tracing::debug!(root = root.name, "JS realm integrity drift detected");
return false;
}
}
true
}
}
fn key_sets_equal(current: &[PropertyKey], baseline: &[PropertyKey]) -> bool {
let current: HashSet<&PropertyKey> = current.iter().collect();
let baseline: HashSet<&PropertyKey> = baseline.iter().collect();
current == baseline
}
fn protos_equal(current: Option<JsObject>, baseline: Option<&JsObject>) -> bool {
match (current, baseline) {
(Some(current), Some(baseline)) => JsObject::equals(¤t, baseline),
(None, None) => true,
_ => false,
}
}
fn identity_values(desc: &PropertyDescriptor) -> Vec<JsValue> {
let mut out = Vec::new();
if desc.is_data_descriptor() {
if let Some(value) = desc.value() {
out.push(value.clone());
}
} else if desc.is_accessor_descriptor() {
if let Some(get) = desc.get() {
out.push(get.clone());
}
if let Some(set) = desc.set() {
out.push(set.clone());
}
}
out
}
fn descriptors_hash(
object: &JsObject,
keys: &[PropertyKey],
baseline_identity: &[Vec<JsValue>],
) -> u64 {
let mut hasher = DefaultHasher::new();
for (i, key) in keys.iter().enumerate() {
key.hash(&mut hasher);
let desc = object.borrow().properties().get(key);
let Some(desc) = desc else {
false.hash(&mut hasher);
continue;
};
true.hash(&mut hasher);
let kind = if desc.is_data_descriptor() {
0u8
} else if desc.is_accessor_descriptor() {
1u8
} else {
2u8
};
kind.hash(&mut hasher);
desc.enumerable().hash(&mut hasher);
desc.configurable().hash(&mut hasher);
desc.writable().hash(&mut hasher);
let ids = identity_values(&desc);
ids.len().hash(&mut hasher);
for (j, value) in ids.iter().enumerate() {
let same = baseline_identity
.get(i)
.and_then(|row| row.get(j))
.is_some_and(|baseline| value.strict_equals(baseline));
same.hash(&mut hasher);
}
}
hasher.finish()
}