Skip to content

fast_api: tighten FunctionTemplateBuilder::build_fast overload-list lifetime for V8 15.x #1989

@divybot

Description

@divybot

Context

Investigation in denoland/v8#21 traced the snapshot-serialization regression Deno hit upgrading to V8 14.9. The Deno workaround in denoland/deno#34226 ships a runtime upgrade pass to re-attach fast calls after deserialization.

V8 main has since reverted the storage shape in crrev.com/c/7828135 ([fastapi] Store v8::CFunction pointer directly in FunctionTemplateInfo, merged 2026-05-12 at refs/heads/main@{#107265}). It will land in denoland/v8 with the V8 15.x roll. Once it does, the deno_core workaround can be deleted — but only if rusty_v8 first satisfies the new V8-side embedder invariant.

The new V8 invariant

Quoting the crrev.com/c/7828135 commit message:

This change relies on the assumption that the CFunction object passed to FunctionTemplate::New outlives the FunctionTemplate itself. In practice, all embedders — including Chrome — already ensure this by holding the CFunction object in a static variable.

In V8 14.9 each v8::CFunction was deep-copied into a heap-allocated CFunctionWithSignature wrapped in a Managed, so the caller's CFunction could be a temporary. In 15.x, V8 stores a raw v8::CFunction* directly in FunctionTemplateInfo. If that pointer goes dangling the JIT will dereference freed memory on the fast path.

The V8 CL converts every d8-test.cc declaration to static CFunction … to illustrate what embedders are expected to do.

What rusty_v8 currently does

FunctionTemplateBuilder::build_fast:

pub fn build_fast<'i>(
    self,
    scope: &PinScope<'s, 'i>,
    overloads: &[CFunction],   // borrowed slice
) -> Local<'s, FunctionTemplate>

forwards overloads.as_ptr() straight into v8__FunctionTemplate__New. The signature accepts any &[CFunction], including a stack-local &[fast_function] — which is exactly what deno_core's op_ctx_template passes:

let template = builder.build_fast(scope, &[fast_function]);

In 14.9 this is fine (V8 copies). After the 15.x roll it would silently UAF.

What we need rusty_v8 to do

Tighten the API so the lifetime contract is enforceable at the type level before V8 15.x lands:

  • Option A (preferred): change build_fast's overload-list parameter to &'static [CFunction] (or a newtype that internally requires 'static). Forces callers to put the CFunctions in a static, matching how Chrome does it.
  • Option B: have rusty_v8 internally own the CFunction storage — e.g. allocate it on an isolate-scoped arena keyed by the resulting FunctionTemplate. More expensive and more API to maintain, but transparent to callers.

Either way the change is API-breaking, so it should land before denoland/v8 picks up V8 15.x.

v8::FunctionTemplate::SetCallHandler (used via Isolate-level FunctionTemplate::new with overloads) and any other entry points that take MemorySpan<const v8::CFunction> need the same treatment.

Out of scope

  • The deno_core op-binding workaround (will_snapshot / upgrade_snapshotted_ops_with_fast_calls) — that gets removed in deno_core after rusty_v8 lands the lifetime tightening and denoland/v8 rolls 15.x with crrev.com/c/7828135.
  • Backporting the V8 fix to 14.9-lkgr-denoland — not worth it; the workaround is in production and the API churn would be bigger than the benefit.

Pointers

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions