DataDog / datadog-static-analyzer

Datadog Static Analyzer
https://docs.datadoghq.com/static_analysis/
Apache License 2.0
100 stars 12 forks source link

[STAL-1960] Implement ddsa JsRuntime scoped execution #398

Closed jasonforal closed 3 months ago

jasonforal commented 4 months ago

What problem are you trying to solve?

We need a high performance way to execute the JavaScript portion of a static analysis rule, so we are re-using the same v8 isolate across executions.

The ideal model for us is to create a new Context and Context::Scope for every rule execution. v8 Contexts (background info here) are relatively cheap to create, and as soon as they drop out of scope, all local handles are able to be garbage collected.

Each context has effectively its own state, and each context its own global environment. This is problematic for us, because when we bootstrap the deno_core runtime, we populate the v8 isolate's default context's global environment with DDSA-specific variables (see __bootstrap.js).

Because of this, we either need to share a global environment across contexts, or allocate and re-assign the global object with all of our needed values every time we create a new context. I previously didn't know v8 well enough to know how to achieve the former (hence why I re-used a v8 Context across executions and used a hacky closure-based solution to simulate different v8 Contexts).

This PR addresses this problem.

What is your solution?

Ideal solution

The ideal solution would be to pass in a handle to a pre-allocated global object ("global_object") when the v8 context is created. This is possible in the C++ API. We'd do something like the following:

auto ddsa_global = prev_ctx->Global();
prev_ctx->Exit();
prev_ctx->DetachGlobal();
auto new_ctx = v8::Context::New(/* ...isolate, ...template */, ddsa_global)

(See documentation for static Local< Context > New)

However, this is not possible with rusty_v8. As of writing, v8::Context::new will always pass in null for the "global_object" parameter.

rusty_v8 does have v8::Context::new_from_template, which would let us pass in an ObjectTemplate, however that isn't ideal for two reasons: 1) it would allocate an entirely new object and 2) ObjectTemplates can only store primitives or other ObjectTemplates/FunctionTemplates. 2 would be a significant thorn because the ddsa lib is designed around stateful "bridge" objects being available to all JavaScript executing, and hydrating their state from scratch would be complicated.

Thus, to get around rusty_v8's inability to pass in a pre-allocated object to be used as the global object, we simulate this behavior by manually setting the prototype of a context's global proxy object to our pre-allocated object. In order to re-use this object across contexts, we have to make it a v8::Global.

Wait, global this, global proxy that...uhh...what?

"Global"

Yes, this terminology is quite confusing. Quick summary:

So in sum, we are creating a "persistent" v8 object, and we configure each v8 Context to use this v8 object. Within the execution context, mutations to this v8 object can happen, but they never persist beyond the context. Executions are thus isolated from each other.

Benefits

By doing this, we no longer have to use a closure to encapsulate the rule's code. I suspect this will be a lot easier for v8's JIT to optimize.

Alternatives considered

What the reviewer should know