mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-28 15:40:04 +00:00
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.
This commit is contained in:
@@ -5,8 +5,8 @@
|
|||||||
.minimum_zig_version = "0.15.2",
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.v8 = .{
|
.v8 = .{
|
||||||
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.4.tar.gz",
|
.url = "https://github.com/lightpanda-io/zig-v8-fork/archive/910b5f52d3968873580a850bd5c5f3705fbc8eed.tar.gz",
|
||||||
.hash = "v8-0.0.0-xddH6_F3BAAiFvKY6R1H-gkuQlk19BkDQ0--uZuTrSup",
|
.hash = "v8-0.0.0-xddH63R8BAC8DIpYQEw97NJ2u9VbGGJT7X-OlrDpycwx",
|
||||||
},
|
},
|
||||||
// .v8 = .{ .path = "../zig-v8-fork" },
|
// .v8 = .{ .path = "../zig-v8-fork" },
|
||||||
.brotli = .{
|
.brotli = .{
|
||||||
|
|||||||
@@ -505,6 +505,7 @@ pub const Function = struct {
|
|||||||
pub const Opts = struct {
|
pub const Opts = struct {
|
||||||
noop: bool = false,
|
noop: bool = false,
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
|
deletable: bool = true,
|
||||||
dom_exception: bool = false,
|
dom_exception: bool = false,
|
||||||
as_typed_array: bool = false,
|
as_typed_array: bool = false,
|
||||||
null_as_undefined: bool = false,
|
null_as_undefined: bool = false,
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ pub fn create() !Snapshot {
|
|||||||
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
defer v8.v8__HandleScope__DESTRUCT(&handle_scope);
|
||||||
|
|
||||||
// Create templates (constructors only) FIRST
|
// 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| {
|
inline for (JsApis, 0..) |JsApi, i| {
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
templates[i] = generateConstructor(JsApi, isolate);
|
templates[i] = generateConstructor(JsApi, isolate);
|
||||||
@@ -419,7 +419,7 @@ fn collectExternalReferences() [countExternalReferences()]isize {
|
|||||||
// via `new ClassName()` - but they could, for example, be created in
|
// via `new ClassName()` - but they could, for example, be created in
|
||||||
// Zig and returned from a function call, which is why we need the
|
// Zig and returned from a function call, which is why we need the
|
||||||
// FunctionTemplate.
|
// 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: {
|
const callback = blk: {
|
||||||
if (@hasDecl(JsApi, "constructor")) {
|
if (@hasDecl(JsApi, "constructor")) {
|
||||||
break :blk JsApi.constructor.func;
|
break :blk JsApi.constructor.func;
|
||||||
@@ -429,7 +429,7 @@ fn generateConstructor(comptime JsApi: type, isolate: *v8.Isolate) *v8.FunctionT
|
|||||||
break :blk illegalConstructorCallback;
|
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);
|
const internal_field_count = comptime countInternalFields(JsApi);
|
||||||
if (internal_field_count > 0) {
|
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)
|
// 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 instance = v8.v8__FunctionTemplate__InstanceTemplate(template);
|
||||||
const prototype = v8.v8__FunctionTemplate__PrototypeTemplate(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;
|
const declarations = @typeInfo(JsApi).@"struct".decls;
|
||||||
var has_named_index_getter = false;
|
var has_named_index_getter = false;
|
||||||
|
|
||||||
@@ -497,23 +502,47 @@ fn attachClass(comptime JsApi: type, isolate: *v8.Isolate, template: *v8.Functio
|
|||||||
switch (definition) {
|
switch (definition) {
|
||||||
bridge.Accessor => {
|
bridge.Accessor => {
|
||||||
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
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.setter == null) {
|
||||||
if (value.static) {
|
attribute |= v8.ReadOnly;
|
||||||
v8.v8__Template__SetAccessorProperty__DEFAULT(@ptrCast(template), js_name, getter_callback);
|
}
|
||||||
} else {
|
if (value.deletable == false) {
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT(prototype, js_name, getter_callback);
|
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 {
|
} else {
|
||||||
if (comptime IS_DEBUG) {
|
v8.v8__ObjectTemplate__SetAccessorProperty__Config(prototype, &.{
|
||||||
std.debug.assert(value.static == false);
|
.key = js_name,
|
||||||
}
|
.getter = getter_callback,
|
||||||
const setter_callback = @constCast(v8.v8__FunctionTemplate__New__Config(isolate, &.{ .callback = value.setter.? }).?);
|
.setter = setter_callback,
|
||||||
v8.v8__ObjectTemplate__SetAccessorProperty__DEFAULT2(prototype, js_name, getter_callback, setter_callback);
|
.attribute = attribute,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bridge.Function => {
|
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));
|
const js_name = v8.v8__String__NewFromUtf8(isolate, name.ptr, v8.kNormal, @intCast(name.len));
|
||||||
if (value.static) {
|
if (value.static) {
|
||||||
v8.v8__Template__Set(@ptrCast(template), js_name, @ptrCast(function_template), v8.None);
|
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;
|
has_named_index_getter = true;
|
||||||
},
|
},
|
||||||
bridge.Iterator => {
|
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)
|
const js_name = if (value.async)
|
||||||
v8.v8__Symbol__GetAsyncIterator(isolate)
|
v8.v8__Symbol__GetAsyncIterator(isolate)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ pub const Function = struct {
|
|||||||
|
|
||||||
pub const Accessor = struct {
|
pub const Accessor = struct {
|
||||||
static: bool = false,
|
static: bool = false,
|
||||||
|
deletable: bool = true,
|
||||||
cache: ?Caller.Function.Opts.Caching = null,
|
cache: ?Caller.Function.Opts.Caching = null,
|
||||||
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
getter: ?*const fn (?*const v8.FunctionCallbackInfo) callconv(.c) void = null,
|
||||||
setter: ?*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{
|
var accessor = Accessor{
|
||||||
.cache = opts.cache,
|
.cache = opts.cache,
|
||||||
.static = opts.static,
|
.static = opts.static,
|
||||||
|
.deletable = opts.deletable,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (@typeInfo(@TypeOf(getter)) != .null) {
|
if (@typeInfo(@TypeOf(getter)) != .null) {
|
||||||
|
|||||||
@@ -262,6 +262,31 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=cached_getter_wrong_this>
|
||||||
|
// Test that extracting a cached property getter and calling it with wrong `this`
|
||||||
|
// doesn't crash (V8 internal field out of bounds). V8's Signature validation
|
||||||
|
// should throw "Illegal invocation" for wrong receiver types.
|
||||||
|
const documentGetter = Object.getOwnPropertyDescriptor(Window.prototype, 'document').get;
|
||||||
|
|
||||||
|
// Verify we get an error with wrong this values
|
||||||
|
let errorCount = 0;
|
||||||
|
const testValues = [{}, null, undefined, 42, 'string', [], () => {}];
|
||||||
|
for (const val of testValues) {
|
||||||
|
try {
|
||||||
|
documentGetter.call(val);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes('Illegal invocation')) {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At least some should throw (null/undefined/primitives get coerced in sloppy mode)
|
||||||
|
testing.expectEqual(true, errorCount > 0);
|
||||||
|
|
||||||
|
// Calling with correct this should still work
|
||||||
|
testing.expectEqual(document, documentGetter.call(window));
|
||||||
|
</script>
|
||||||
|
|
||||||
<script id=unhandled_rejection>
|
<script id=unhandled_rejection>
|
||||||
{
|
{
|
||||||
let unhandledCalled = 0;
|
let unhandledCalled = 0;
|
||||||
|
|||||||
@@ -829,7 +829,7 @@ pub const JsApi = struct {
|
|||||||
pub var class_id: bridge.ClassId = undefined;
|
pub var class_id: bridge.ClassId = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 } });
|
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = .{ .internal = 1 }, .deletable = false });
|
||||||
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } });
|
pub const console = bridge.accessor(Window.getConsole, null, .{ .cache = .{ .internal = 2 } });
|
||||||
|
|
||||||
pub const top = bridge.accessor(Window.getTop, null, .{});
|
pub const top = bridge.accessor(Window.getTop, null, .{});
|
||||||
@@ -842,7 +842,7 @@ pub const JsApi = struct {
|
|||||||
pub const performance = bridge.accessor(Window.getPerformance, null, .{});
|
pub const performance = bridge.accessor(Window.getPerformance, null, .{});
|
||||||
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});
|
pub const localStorage = bridge.accessor(Window.getLocalStorage, null, .{});
|
||||||
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});
|
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{});
|
||||||
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{});
|
pub const location = bridge.accessor(Window.getLocation, Window.setLocation, .{ .deletable = false });
|
||||||
pub const history = bridge.accessor(Window.getHistory, null, .{});
|
pub const history = bridge.accessor(Window.getHistory, null, .{});
|
||||||
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
|
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
|
||||||
pub const crypto = bridge.accessor(Window.getCrypto, null, .{});
|
pub const crypto = bridge.accessor(Window.getCrypto, null, .{});
|
||||||
|
|||||||
Reference in New Issue
Block a user