Skip to content

Commit 9bd8970

Browse files
committed
Store the hash of the original code that was compiled into a v8::UnboundScript. Add helper to evict the cache if this hash doesn't match
1 parent b118e6e commit 9bd8970

1 file changed

Lines changed: 58 additions & 0 deletions

File tree

  • crates/static-analysis-kernel/src/analysis/ddsa_lib

crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,23 @@ impl JsRuntime {
223223
self.rule_cache.borrow_mut().remove(rule_name).is_some()
224224
}
225225

226+
/// Drops the cached [v8::UnboundScript] for the given `rule_name` if its `rule_code` doesn't match the provided.
227+
/// Returns `true` if an entry was evicted, or `false` if not.
228+
///
229+
/// # Panics
230+
/// Panics if the `rule_cache` has an existing borrow.
231+
pub fn evict_script_if_stale(&self, rule_name: &str, rule_code: &str) -> bool {
232+
let mut cache = self.rule_cache.borrow_mut();
233+
let Some(entry) = cache.get(rule_name) else {
234+
return false;
235+
};
236+
if entry.code_hash != CompiledRule::get_code_hash(rule_code) {
237+
cache.remove(rule_name);
238+
return true;
239+
}
240+
false
241+
}
242+
226243
#[allow(clippy::too_many_arguments)]
227244
fn execute_rule_internal(
228245
&mut self,
@@ -482,6 +499,8 @@ pub struct CompiledRule {
482499
script: v8::Global<v8::UnboundScript>,
483500
/// The number of lines in the rule code before it was interpolated into the rule template.
484501
original_line_count: usize,
502+
/// A fingerprint of the rule code (before interpolation) that produced this script.
503+
code_hash: [u8; 16],
485504
}
486505

487506
impl CompiledRule {
@@ -509,12 +528,20 @@ impl CompiledRule {
509528
);
510529
let script = compile_script(scope, &rule_script, Some(&origin))?;
511530
let original_line_count = rule_code.lines().count();
531+
let code_hash = Self::get_code_hash(rule_code);
512532
Ok(Self {
513533
script,
514534
original_line_count,
535+
code_hash,
515536
})
516537
}
517538

539+
fn get_code_hash(rule_code: &str) -> [u8; 16] {
540+
use sha2::Digest;
541+
let digest = sha2::Sha256::digest(rule_code.as_bytes());
542+
digest[..16].try_into().expect("slice len should be 16")
543+
}
544+
518545
/// Mutates the provided stack trace to remove lines that reference code outside the template.
519546
pub fn filter_stack_trace(&self, stack_trace_frames: &mut Vec<JsCallSite>) {
520547
stack_trace_frames.retain(|cs| {
@@ -1689,4 +1716,35 @@ function visit(captures) {
16891716
vec![" at someFunction (rule:2:5)", " at visit (rule:6:5)"]
16901717
);
16911718
}
1719+
1720+
/// [`JsRuntime::evict_script_if_stale`] removes a cached rule if the code hash doesn't match.
1721+
#[test]
1722+
fn evict_script_if_stale() {
1723+
const RULE_NAME: &str = "ruleset/rule-name";
1724+
const CODE_V1: &str = "function visit(captures) { console.log('rule_v1'); }";
1725+
const CODE_V2: &str = "function visit(captures) { console.log('rule_v2'); }";
1726+
1727+
let mut rt = cfg_test_v8().new_runtime();
1728+
1729+
let compiled = CompiledRule::new(&mut rt.v8_handle_scope(), CODE_V1).unwrap();
1730+
rt.rule_cache
1731+
.borrow_mut()
1732+
.insert(RULE_NAME.to_string(), compiled);
1733+
assert_eq!(
1734+
rt.rule_cache.borrow().get(RULE_NAME).unwrap().code_hash,
1735+
CompiledRule::get_code_hash(CODE_V1)
1736+
);
1737+
1738+
let did_evict = rt.evict_script_if_stale(RULE_NAME, CODE_V1);
1739+
assert!(!did_evict);
1740+
assert!(rt.rule_cache.borrow().contains_key(RULE_NAME));
1741+
1742+
let did_evict = rt.evict_script_if_stale("ruleset/rule-without-a-cached-script", CODE_V2);
1743+
assert!(!did_evict);
1744+
assert!(rt.rule_cache.borrow().contains_key(RULE_NAME));
1745+
1746+
let did_evict = rt.evict_script_if_stale(RULE_NAME, CODE_V2);
1747+
assert!(did_evict);
1748+
assert!(!rt.rule_cache.borrow().contains_key(RULE_NAME));
1749+
}
16921750
}

0 commit comments

Comments
 (0)