[K9VULN-14008] Fix stale v8 script cache when using datadog-static-analyzer-server#901
[K9VULN-14008] Fix stale v8 script cache when using datadog-static-analyzer-server#901jasonforal merged 3 commits intomainfrom
datadog-static-analyzer-server#901Conversation
…undScript. Add helper to evict the cache if this hash doesn't match
…ting the v8::UnboundScript cache for the passed in JSRuntime.
There was a problem hiding this comment.
Pull request overview
Fixes stale v8 script caching in datadog-static-analyzer-server when rule names collide across updates and multiple thread-local JsRuntimes are in use, by making runtime-side script cache eviction dependent on rule code identity rather than a one-time “is_update” signal.
Changes:
- Add a per-script code fingerprint to the kernel
JsRuntimerule-script cache and introduceevict_script_if_stale(rule_name, rule_code). - Simplify server
RuleCacheAPI (removeCachedValue/is_update) and always ask eachJsRuntimeto evict stale scripts by code hash before execution. - Add/adjust tests to cover v8
UnboundScriptcache eviction behavior, including multi-runtime simulation.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
crates/static-analysis-kernel/src/analysis/ddsa_lib/runtime.rs |
Stores a rule-code fingerprint alongside cached compiled scripts and exposes stale-eviction API + unit test. |
crates/bins/src/bin/datadog_static_analyzer_server/rule_cache.rs |
Updates server rule caching flow to evict stale runtime scripts by code hash and expands test coverage (including multi-runtime case). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
🎯 Code Coverage (details) 🔗 Commit SHA: c7ebf93 | Docs | Datadog PR Page | Give us feedback! |
| /// Panics if the `rule_cache` has an existing borrow. | ||
| pub fn clear_rule_cache(&self, rule_name: &str) -> bool { | ||
| self.rule_cache.borrow_mut().remove(rule_name).is_some() | ||
| pub fn evict_script_if_stale(&self, rule_name: &str, rule_code: &str) -> bool { |
There was a problem hiding this comment.
This could avoid holding a mutable borrow when no eviction is needed:
pub fn evict_script_if_stale(&self, rule_name: &str, rule_code: &str) -> bool {
// First check with read-only access
if let Some(entry) = self.rule_cache.borrow().get(rule_name) {
if entry.code_hash == CompiledRule::get_code_hash(rule_code) {
return false; // No eviction needed, never took mut borrow
}
} else {
return false;
}
// Only take mut borrow if eviction is actually needed
self.rule_cache.borrow_mut().remove(rule_name).is_some()
}This reduces lock contention in the common case where rules haven't changed.
There was a problem hiding this comment.
There will never be lock contention, otherwise this would actually panic. We're using an Rc<RefCell<_>> here, not a mutex. It's governed by runtime borrow checking.
| let Some(entry) = cache.get(rule_name) else { | ||
| return false; | ||
| }; | ||
| if entry.code_hash != CompiledRule::get_code_hash(rule_code) { |
There was a problem hiding this comment.
get_code_hash(rule_code) is called on every request, even for cache hits. not sure if this is a concern from a performance perspective or if the overhead here is negligible. You could otherwise precompute it in RuleInternal during construction and pass it in but that might be overkill tbh.
There was a problem hiding this comment.
This is mentioned in the description
| } | ||
| compiled_rules.push(cached.rule); | ||
| let internal = cached.as_internal(); | ||
| let _ = runtime.evict_script_if_stale(&internal.name, &internal.code); |
There was a problem hiding this comment.
nit- the return value isn't used here. can we remove it?
There was a problem hiding this comment.
It's essentially a style question in this case. I just prefer to be explicit
What problem are you trying to solve?
When running
datadog-static-analyzer-serverwith the--use-rules-cacheCLI argument, there are two layers of cache, both keyed by the rule's name:RuleInternalon thedatadog-static-analyzer-server, which exists primary to cache the compiled tree-sitter query.JsRuntime.The cache is designed such that the caller is expected to to manage the
JsRuntime's v8 script cache. Currently, thedatadog-static-analyzer-serverexplicitly evicts the v8 script cache for a specific rule when the incoming JSON payload represents a separate rule:datadog-static-analyzer/crates/bins/src/bin/datadog_static_analyzer_server/rule_cache.rs
Lines 99 to 102 in b118e6e
This scenario essentially only occurs when working on a custom rule (where each edit to the JavaScript or tree-sitter query sends a brand new rule).
The issue is that there are multiple
JsRuntime-- one for each thread:datadog-static-analyzer/crates/bins/src/bin/datadog_static_analyzer_server/endpoints.rs
Lines 128 to 131 in b118e6e
But there is only a single
RuleInternalcache, shared across all threads:datadog-static-analyzer/crates/bins/src/bin/datadog-static-analyzer-server.rs
Line 14 in b118e6e
The issue is that while the
is_updateboolean works correctly, it is only ever triggered once. Because thecached_analysis_requestaccepts theJsRuntimeas an argument, this means that only a single runtime's v8 script cache is cleared. Thus, if the server is run with multiple threads, all other threads will continue to have a stale cache.What is your solution?
JsRuntimeitself. Withindatadog-static-analyzer-server, always hash and check equivalence of rule code.Alternatives considered
/analyzeendpoint and thread this through tocached_analysis_request, such that the server would only hash and check the v8 script cache if that query param is present. Then, the VS Code extension would modify only the custom rule editor to pass this query param. This would be a "correct" and surgical implementation that avoids unnecessary hashing.What the reviewer should know