mem: Finalize more (non-dom) types

Adds finalizers to various types. The two most interesting are ImageData (which
could have a large data field) and HTMLCollection which a page could create
many.

Various Crypto types are also finalized to make sure the key is freed. This is
particularly important for freeing any keys created with EVP_PKEY_new.
This commit is contained in:
Karl Seguin
2026-07-12 19:31:03 +08:00
parent 58d8aeb070
commit cd7b63cade
16 changed files with 207 additions and 40 deletions

View File

@@ -70,6 +70,19 @@
}
</script>
<script id=structured-clone>
{
const img = new ImageData(2, 2);
img.data[0] = 42;
const clone = structuredClone(img);
testing.expectEqual(2, clone.width);
testing.expectEqual(2, clone.height);
testing.expectEqual(42, clone.data[0]);
clone.data[0] = 7;
testing.expectEqual(42, img.data[0]);
}
</script>
<script id=too-large>
testing.expectError("IndexSizeError", () => new ImageData(2_147_483_648, 2_147_483_648));
</script>

View File

@@ -16,16 +16,22 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const crypto = @import("../../sys/libcrypto.zig");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Execution = js.Execution;
const Allocator = std.mem.Allocator;
/// Represents a cryptographic key obtained from one of the SubtleCrypto methods
/// generateKey(), deriveKey(), importKey(), or unwrapKey().
const CryptoKey = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator = undefined,
/// Algorithm being used.
_type: Type,
/// Whether this is a secret (symmetric), public, or private key. Surfaced as
@@ -37,8 +43,8 @@ _extractable: bool,
_usages: u8,
/// Raw bytes of key.
_key: []const u8,
/// Metadata needed to reconstruct the JS `.algorithm` dictionary. The strings
/// are expected to outlive the key (arena-allocated alongside it).
/// Metadata needed to reconstruct the JS `.algorithm` dictionary. `hash` is
/// duped into the key's arena by init; `name`/`named_curve` are static.
_algorithm: Algorithm,
/// Different algorithms may use different data structures;
/// this union can be used for such situations. Active field is understood
@@ -94,6 +100,39 @@ pub const Usages = struct {
// zig fmt: on
};
/// Copies `key` into an owned pooled arena, duping the caller-provided
/// slices (`_key`, `_algorithm.hash`). `_algorithm.name` and `_named_curve`
/// are expected to be static strings. Takes ownership of `_vary.pkey`.
pub fn init(exec: *const Execution, key: CryptoKey) !*CryptoKey {
const arena = try exec.getArena(.tiny, "CryptoKey");
errdefer exec.releaseArena(arena);
const self = try arena.create(CryptoKey);
self.* = key;
self._arena = arena;
self._key = try arena.dupe(u8, key._key);
if (key._algorithm.hash) |hash| {
self._algorithm.hash = try arena.dupe(u8, hash);
}
return self;
}
pub fn deinit(self: *CryptoKey, page: *Page) void {
switch (self._vary) {
.pkey => |pkey| crypto.EVP_PKEY_free(pkey),
.none, .digest => {},
}
page.releaseArena(self._arena);
}
pub fn releaseRef(self: *CryptoKey, page: *Page) void {
self._rc.release(self, page);
}
pub fn acquireRef(self: *CryptoKey) void {
self._rc.acquire();
}
pub fn canEncrypt(self: *const CryptoKey) bool {
return self._usages & Usages.encrypt != 0;
}

View File

@@ -16,7 +16,10 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
@@ -25,6 +28,7 @@ pub const FilterOpts = NodeFilter.FilterOpts;
const DOMNodeIterator = @This();
_rc: lp.RC(u8) = .{},
_root: *Node,
_what_to_show: u32,
_filter: NodeFilter,
@@ -43,6 +47,19 @@ pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, frame: *Frame)
});
}
pub fn deinit(self: *DOMNodeIterator, page: *Page) void {
self._filter.deinit();
page.factory.destroy(self);
}
pub fn releaseRef(self: *DOMNodeIterator, page: *Page) void {
self._rc.release(self, page);
}
pub fn acquireRef(self: *DOMNodeIterator) void {
self._rc.acquire();
}
pub fn getRoot(self: *const DOMNodeIterator) *Node {
return self._root;
}

View File

@@ -16,7 +16,10 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const lp = @import("lightpanda");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Frame = @import("../Frame.zig");
const Node = @import("Node.zig");
@@ -25,6 +28,7 @@ pub const FilterOpts = NodeFilter.FilterOpts;
const DOMTreeWalker = @This();
_rc: lp.RC(u8) = .{},
_root: *Node,
_what_to_show: u32,
_filter: NodeFilter,
@@ -40,6 +44,19 @@ pub fn init(root: *Node, what_to_show: u32, filter: ?FilterOpts, frame: *Frame)
});
}
pub fn deinit(self: *DOMTreeWalker, page: *Page) void {
self._filter.deinit();
page.factory.destroy(self);
}
pub fn releaseRef(self: *DOMTreeWalker, page: *Page) void {
self._rc.release(self, page);
}
pub fn acquireRef(self: *DOMTreeWalker) void {
self._rc.acquire();
}
pub fn getRoot(self: *const DOMTreeWalker) *Node {
return self._root;
}

View File

@@ -188,8 +188,8 @@ pub fn getEmbeds(self: *HTMLDocument, frame: *Frame) !collections.NodeLive(.tag)
return collections.NodeLive(.tag).init(self.asNode(), .embed, frame);
}
pub fn getApplets(_: *const HTMLDocument) collections.HTMLCollection {
return .{ ._data = .empty };
pub fn getApplets(_: *const HTMLDocument, frame: *Frame) !*collections.HTMLCollection {
return frame._factory.create(collections.HTMLCollection{ ._data = .empty });
}
pub fn getCurrentScript(self: *const HTMLDocument) ?*Element.Html.Script {

View File

@@ -27,6 +27,8 @@ const Execution = js.Execution;
/// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData
const ImageData = @This();
_rc: lp.RC(u8) = .{},
_width: u32,
_height: u32,
_data: js.ArrayBufferRef(.uint8_clamped).Global,
@@ -74,9 +76,14 @@ pub fn init(
}
var size, var overflown = @mulWithOverflow(width, height);
if (overflown == 1) return error.IndexSizeError;
if (overflown == 1) {
return error.IndexSizeError;
}
size, overflown = @mulWithOverflow(size, 4);
if (overflown == 1) return error.IndexSizeError;
if (overflown == 1) {
return error.IndexSizeError;
}
return exec._factory.create(ImageData{
._width = width,
@@ -85,6 +92,19 @@ pub fn init(
});
}
pub fn deinit(self: *ImageData, page: *Page) void {
self._data.release();
page.factory.destroy(self);
}
pub fn releaseRef(self: *ImageData, page: *Page) void {
self._rc.release(self, page);
}
pub fn acquireRef(self: *ImageData) void {
self._rc.acquire();
}
pub fn structuredSerialize(self: *const ImageData, writer: *js.StructuredWriter) !void {
writer.writeUint32(self._width);
writer.writeUint32(self._height);

View File

@@ -44,6 +44,12 @@ pub fn init(opts_: ?FilterOpts) !NodeFilter {
};
}
pub fn deinit(self: *const NodeFilter) void {
if (self._func) |func| {
func.release();
}
}
// Constants
pub const FILTER_ACCEPT: i32 = 1;
pub const FILTER_REJECT: i32 = 2;

View File

@@ -205,13 +205,12 @@ pub fn importKey(
const mask = common.usageMask(&.{ "deriveKey", "deriveBits" }, key_usages) catch |err| {
return local.rejectPromise(.{ .dom_exception = .{ .err = err } });
};
const key = try exec.arena.dupe(u8, raw);
const crypto_key = try exec._factory.create(CryptoKey{
const crypto_key = try CryptoKey.init(exec, .{
._type = .derive,
._kind = .secret,
._extractable = extractable,
._usages = mask,
._key = key,
._key = raw,
._algorithm = .{ .name = derive_name },
});
return local.resolvePromise(crypto_key);

View File

@@ -17,7 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Element = @import("../Element.zig");
const TreeWalker = @import("../TreeWalker.zig");
@@ -55,6 +58,19 @@ _data: union(Mode) {
form: NodeLive(.form),
empty: void,
},
_rc: lp.RC(u8) = .{},
pub fn deinit(self: *HTMLCollection, page: *Page) void {
page.factory.destroy(self);
}
pub fn releaseRef(self: *HTMLCollection, page: *Page) void {
self._rc.release(self, page);
}
pub fn acquireRef(self: *HTMLCollection) void {
self._rc.acquire();
}
pub fn length(self: *HTMLCollection, frame: *const Frame) u32 {
return switch (self._data) {
@@ -119,6 +135,14 @@ pub const Iterator = GenericIterator(struct {
empty: void,
},
pub fn acquireRef(self: *@This()) void {
self.list.acquireRef();
}
pub fn releaseRef(self: *@This(), page: *Page) void {
self.list.releaseRef(page);
}
pub fn next(self: *@This(), _: *const Execution) ?*Element {
return switch (self.list._data) {
.tag => |*impl| impl.nextTw(&self.tw.tag),

View File

@@ -18,6 +18,7 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Element = @import("../Element.zig");
@@ -31,10 +32,22 @@ const HTMLFormControlsCollection = @This();
_proto: *HTMLCollection,
pub const NamedItemResult = union(enum) {
element: *Element,
radio_node_list: *RadioNodeList,
};
// The refcount lives on the proto, but anchoring the finalizer here lets
// deinit reclaim this struct's slot along with the proto's.
pub fn deinit(self: *HTMLFormControlsCollection, page: *Page) void {
self._proto.deinit(page);
// Not destroy(): the proto is a separate slab allocation, not a
// contiguous factory chain.
page.factory.destroyStandalone(self);
}
pub fn acquireRef(self: *HTMLFormControlsCollection) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *HTMLFormControlsCollection, page: *Page) void {
self._proto._rc.release(self, page);
}
pub fn length(self: *HTMLFormControlsCollection, frame: *Frame) u32 {
return self._proto.length(frame);
@@ -44,6 +57,11 @@ pub fn getAtIndex(self: *HTMLFormControlsCollection, index: usize, frame: *Frame
return self._proto.getAtIndex(index, frame);
}
pub const NamedItemResult = union(enum) {
element: *Element,
radio_node_list: *RadioNodeList,
};
pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, frame: *Frame) !?NamedItemResult {
if (name.len == 0) {
return null;
@@ -87,6 +105,10 @@ pub fn namedItem(self: *HTMLFormControlsCollection, name: []const u8, frame: *Fr
radio_node_list._proto = try frame._factory.create(NodeList{ ._data = .{ .radio_node_list = radio_node_list } });
// The RadioNodeList outlives this call; its NodeList releases
// the ref in deinit.
self.acquireRef();
return .{ .radio_node_list = radio_node_list };
}
}

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Node = @import("../Node.zig");
const Element = @import("../Element.zig");
@@ -27,6 +28,23 @@ const HTMLOptionsCollection = @This();
_proto: *HTMLCollection,
_select: *@import("../element/html/Select.zig"),
// The refcount lives on the proto, but anchoring the finalizer here lets
// deinit reclaim this struct's slot along with the proto's.
pub fn deinit(self: *HTMLOptionsCollection, page: *Page) void {
self._proto.deinit(page);
// Not destroy(): the proto is a separate slab allocation, not a
// contiguous factory chain.
page.factory.destroyStandalone(self);
}
pub fn acquireRef(self: *HTMLOptionsCollection) void {
self._proto.acquireRef();
}
pub fn releaseRef(self: *HTMLOptionsCollection, page: *Page) void {
self._proto._rc.release(self, page);
}
// Forward length to HTMLCollection
pub fn length(self: *HTMLOptionsCollection, frame: *Frame) u32 {
return self._proto.length(frame);

View File

@@ -45,7 +45,8 @@ pub fn deinit(self: *NodeList, page: *Page) void {
switch (self._data) {
.child_nodes => |cn| cn.deinit(page),
.selector_list => |list| list.deinit(page),
else => {},
.radio_node_list => |rnl| rnl._form_collection.releaseRef(page),
.name => {},
}
}

View File

@@ -93,12 +93,12 @@ pub fn generate(
const allowed = allowedUsages(params.name).?;
const mask = common.usageMask(allowed, key_usages) catch unreachable;
const key = try exec.arena.alloc(u8, params.length / 8);
const key = try exec.local_arena.alloc(u8, params.length / 8);
const res = crypto.RAND_bytes(key.ptr, key.len);
lp.assert(res == 1, "AES.generate", .{ .res = res });
const crypto_key = try exec._factory.create(CryptoKey{
const crypto_key = try CryptoKey.init(exec, .{
._type = .aes,
._kind = .secret,
._extractable = extractable,
@@ -133,13 +133,12 @@ pub fn import(
return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
}
const key = try exec.arena.dupe(u8, raw);
const crypto_key = try exec._factory.create(CryptoKey{
const crypto_key = try CryptoKey.init(exec, .{
._type = .aes,
._kind = .secret,
._extractable = extractable,
._usages = mask,
._key = key,
._key = raw,
._algorithm = .{ .name = canonical },
});

View File

@@ -127,7 +127,7 @@ pub fn generate(
errdefer crypto.EVP_PKEY_free(public_pkey);
if (crypto.EVP_PKEY_set1_EC_KEY(public_pkey, pub_ec) != 1) return error.OutOfMemory;
const private = try exec._factory.create(CryptoKey{
const private = try CryptoKey.init(exec, .{
._type = .ec,
._kind = .private,
._extractable = extractable,
@@ -136,9 +136,8 @@ pub fn generate(
._algorithm = .{ .name = name, .named_curve = curve },
._vary = .{ .pkey = private_pkey },
});
errdefer exec._factory.destroy(private);
const public = try exec._factory.create(CryptoKey{
const public = try CryptoKey.init(exec, .{
._type = .ec,
._kind = .public,
// Public keys are always extractable.
@@ -192,7 +191,7 @@ pub fn import(
return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
}
const crypto_key = try exec._factory.create(CryptoKey{
const crypto_key = try CryptoKey.init(exec, .{
._type = .ec,
._kind = if (is_private) .private else .public,
._extractable = extractable,

View File

@@ -81,18 +81,18 @@ pub fn init(
}
// Should we reject this in promise too?
const key = try exec.arena.alloc(u8, block_size);
const key = try exec.local_arena.alloc(u8, block_size);
// HMAC is simply CSPRNG.
const res = crypto.RAND_bytes(key.ptr, key.len);
lp.assert(res == 1, "HMAC.init", .{ .res = res });
const crypto_key = try exec._factory.create(CryptoKey{
const crypto_key = try CryptoKey.init(exec, .{
._type = .hmac,
._extractable = extractable,
._usages = mask,
._key = key,
._algorithm = .{ .name = "HMAC", .hash = try exec.arena.dupe(u8, hash_name) },
._algorithm = .{ .name = "HMAC", .hash = hash_name },
._vary = .{ .digest = digest },
});
@@ -122,16 +122,13 @@ pub fn import(
return local.rejectPromise(.{ .dom_exception = .{ .err = error.DataError } });
}
const key = try exec.arena.dupe(u8, raw);
errdefer exec.arena.free(key);
const crypto_key = try exec._factory.create(CryptoKey{
const crypto_key = try CryptoKey.init(exec, .{
._type = .hmac,
._kind = .secret,
._extractable = extractable,
._usages = mask,
._key = key,
._algorithm = .{ .name = "HMAC", .hash = try exec.arena.dupe(u8, hash_name) },
._key = raw,
._algorithm = .{ .name = "HMAC", .hash = hash_name },
._vary = .{ .digest = digest },
});

View File

@@ -60,11 +60,8 @@ pub fn init(
});
}
const public_value = try exec.arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);
errdefer exec.arena.free(public_value);
const private_key = try exec.arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);
errdefer exec.arena.free(private_key);
const public_value = try exec.local_arena.alloc(u8, crypto.X25519_PUBLIC_VALUE_LEN);
const private_key = try exec.local_arena.alloc(u8, crypto.X25519_PRIVATE_KEY_LEN);
// There's no info about whether this can fail; so I assume it cannot.
crypto.X25519_keypair(@ptrCast(public_value), @ptrCast(private_key));
@@ -91,7 +88,7 @@ pub fn init(
private_key.len,
) orelse return error.OutOfMemory;
const private = try exec._factory.create(CryptoKey{
const private = try CryptoKey.init(exec, .{
._type = .x25519,
._kind = .private,
._extractable = extractable,
@@ -100,9 +97,8 @@ pub fn init(
._algorithm = .{ .name = "X25519" },
._vary = .{ .pkey = private_pkey },
});
errdefer exec._factory.destroy(private);
const public = try exec._factory.create(CryptoKey{
const public = try CryptoKey.init(exec, .{
._type = .x25519,
._kind = .public,
// Public keys are always extractable.