From a45f9cb810bee8171f1bd61da0e119ac7b410739 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 25 Mar 2026 19:19:00 +0800 Subject: [PATCH] Set v8::Signature on FunctionTemplates This causes v8 to verify the receiver of a function, and prevents calling an accessor or function with the wrong receiver, e.g.: ``` const g = Object.getOwnPropertyDescriptor(Window.prototype, 'document').get; g.call(null); ``` A few other cleanups in this commit: 1 - Define any accessor with a getter as ReadOnly 2 - Ability to define an accessor with the DontDelete attribute (window.document and window.location) 3 - Replace v8__ObjectTemplate__SetAccessorProperty__DEFAULTX overloads with new v8__ObjectTemplate__SetAccessorProperty__Config 4 - Remove unnecessary @constCast for FunctionTemplate which can be const everywhere. --- build.zig.zon | 4 +- src/browser/js/Caller.zig | 1 + src/browser/js/Snapshot.zig | 63 ++++++++++++++++++++-------- src/browser/js/bridge.zig | 2 + src/browser/tests/window/window.html | 25 +++++++++++ src/browser/webapi/Window.zig | 4 +- 6 files changed, 78 insertions(+), 21 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index ad393bde..b81f8df6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.4.tar.gz", - .hash = "v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/910b5f52d3968873580a850bd5c5f3705fbc8eed.tar.gz", + .hash = "v8-0.0.0-xddH63R8BAC8DIpYQEw97NJ2u9VbGGJT7X-OlrDpycwx", }, // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 9ecd65b2..28da9f2a 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -505,6 +505,7 @@ pub const Function = struct { pub const Opts = struct { noop: bool = false, static: bool = false, + deletable: bool = true, dom_exception: bool = false, as_typed_array: bool = false, null_as_undefined: bool = false, diff --git a/src/browser/js/Snapshot.zig b/src/browser/js/Snapshot.zig index 7aa2125c..de4ac308 100644 --- a/src/browser/js/Snapshot.zig +++ b/src/browser/js/Snapshot.zig @@ -137,7 +137,7 @@ pub fn create() !Snapshot { defer v8.v8__HandleScope__DESTRUCT(&handle_scope); // Create templates (constructors only) FIRST - var templates: [JsApis.len]*v8.FunctionTemplate = undefined; + var templates: [JsApis.len]*const v8.FunctionTemplate = undefined; inline for (JsApis, 0..) |JsApi, i| { @setEvalBranchQuota(10_000); templates[i] = generateConstructor(JsApi, isolate); @@ -419,7 +419,7 @@ fn collectExternalReferences() [countExternalReferences()]isize { // via `new ClassName()` - but they could, for example, be created in // Zig and returned from a function call, which is why we need the // FunctionTemplate. -fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionTemplate { +fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *const v8.FunctionTemplate { const callback = blk: { if (@hasDecl(JsApi, "constructor")) { break :blk JsApi.constructor.func; @@ -429,7 +429,7 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT break :blk illegalConstructorCallback; }; - const template = @constCast(v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?); + const template = v8.v8__FunctionTemplate__New__DEFAULT2(isolate, callback).?; { const internal_field_count = comptime countInternalFields(JsApi); if (internal_field_count > 0) { @@ -482,10 +482,15 @@ pub fn countInternalFields(comptime JsApi: type) u8 { } // Attaches JsApi members to the prototype template (normal case) -fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.FunctionTemplate) void { +fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *const v8.FunctionTemplate) void { const instance = v8.v8__FunctionTemplate__InstanceTemplate(template); const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(template); + // Create a signature that validates the receiver is an instance of this template. + // This prevents crashes when JavaScript extracts a getter/method and calls it + // with the wrong `this` (e.g., documentGetter.call(null)). + const signature = v8.v8__Signature__New(isolate, template); + const declarations = @typeInfo(JsApi).@"struct".decls; var has_named_index_getter = false; @@ -497,23 +502,47 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio switch (definition) { bridge.Accessor => { const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); - const getter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.getter }).?); + const getter_signature = if (value.static) null else signature; + const getter_callback = v8.v8__FunctionTemplate__New__Config(isolate, &.{ + .callback = value.getter, + .signature = getter_signature, + }).?; + const setter_callback = if (value.setter) |setter| + v8.v8__FunctionTemplate__New__Config(isolate, &.{ + .callback = setter, + .signature = getter_signature, + }).? + else + null; + + var attribute: v8.PropertyAttribute = 0; if (value.setter == null) { - if (value.static) { - v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback); - } else { - v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback); - } + attribute |= v8.ReadOnly; + } + if (value.deletable == false) { + attribute |= v8.DontDelete; + } + + if (value.static) { + // Static accessors: use Template's SetAccessorProperty + v8.v8__Template__SetAccessorProperty(@ptrCast(template), js_name, getter_callback, setter_callback, attribute); } else { - if (comptime IS_DEBUG) { - std.debug.assert(value.static == false); - } - const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?); - v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback); + v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{ + .key = js_name, + .getter = getter_callback, + .setter = setter_callback, + .attribute = attribute, + }); } }, bridge.Function => { - const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func, .length = value.arity }).?); + // For non-static functions, use the signature to validate the receiver + const func_signature = if (value.static) null else signature; + const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ + .callback = value.func, + .length = value.arity, + .signature = func_signature, + }).?; const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len)); if (value.static) { v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None); @@ -551,7 +580,7 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio has_named_index_getter = true; }, bridge.Iterator => { - const function_template = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?); + const function_template = v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.func }).?; const js_name = if (value.async) v8.v8__Symbol__GetAsyncIterator(isolate) else diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index b7da3c13..fb49e01e 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -198,6 +198,7 @@ pub const Function = struct { pub const Accessor = struct { static: bool = false, + deletable: bool = true, cache: ?Caller.Function.Opts.Caching = null, getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null, setter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null, @@ -206,6 +207,7 @@ pub const Accessor = struct { var accessor = Accessor{ .cache = opts.cache, .static = opts.static, + .deletable = opts.deletable, }; if (@typeInfo(@TypeOf(getter)) != .null) { diff --git a/src/browser/tests/window/window.html b/src/browser/tests/window/window.html index 13cd40e8..9623f1ef 100644 --- a/src/browser/tests/window/window.html +++ b/src/browser/tests/window/window.html @@ -262,6 +262,31 @@ } + +