Merge pull request #2913 from lightpanda-io/attribute-lookup-fix

fix: broken attribute lookup cross frame
This commit is contained in:
Karl Seguin
2026-07-10 19:05:08 +08:00
committed by GitHub
3 changed files with 67 additions and 35 deletions

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<head></head>
<body>
<script src="../testing.js"></script>
<!--
Attribute lookups must work across realms. Names not in the static intern
table (data-*, custom attributes) are canonicalized per-frame, so a parent
reading an iframe element's attributes (or vice versa) can't rely on
pointer identity — it must fall back to comparing bytes. Regression: parent
reads of iframe data-* returned null (WPT encoding/*).
-->
<iframe id="idf" src="support/cross_realm_attributes.html"></iframe>
<script id="cross_realm_reads">
testing.onload(() => {
const el = document.getElementById('idf').contentDocument.getElementById('s1');
testing.expectEqual('5F', el.getAttribute('data-cp'));
testing.expectEqual('5F', el.dataset.cp);
testing.expectEqual('A1B2', el.dataset.bytes);
testing.expectEqual('ns1', el.getAttribute('nonstandardattr'));
testing.expectTrue(el.hasAttribute('nonstandardattr'));
testing.expectFalse(el.hasAttribute('data-nope'));
testing.expectEqual(null, el.getAttribute('data-nope'));
});
</script>
<script id="cross_realm_writes">
testing.onload(() => {
const el = document.getElementById('idf').contentDocument.getElementById('s1');
const count = el.attributes.length;
// Updating an existing attribute from the parent realm must find the
// entry (not append a duplicate).
el.setAttribute('data-cp', '60');
testing.expectEqual('60', el.getAttribute('data-cp'));
testing.expectEqual(count, el.attributes.length);
// A new name canonicalized by the parent realm must be readable from
// the iframe's own realm (and back).
el.setAttribute('data-parentset', 'pv');
const iwin = document.getElementById('idf').contentWindow;
testing.expectEqual('pv', iwin.readAttr('data-parentset'));
testing.expectEqual('60', iwin.readAttr('data-cp'));
el.removeAttribute('data-parentset');
testing.expectFalse(el.hasAttribute('data-parentset'));
testing.expectEqual(null, iwin.readAttr('data-parentset'));
});
</script>
</body>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<body>
<span id="s1" data-cp="5F" data-bytes="A1B2" nonstandardattr="ns1">x</span>
<script>
// Called by the parent test: reads execute in this frame's realm.
function readAttr(name) {
return document.getElementById('s1').getAttribute(name);
}
</script>
</body>

View File

@@ -385,29 +385,12 @@ pub const List = struct {
const normalized =
if (self.normalize) try normalizeNameForLookup(name, frame) else name;
// A name that was never canonicalized can't be in any list.
const canonical = lookupCanonicalName(normalized.str(), frame) orelse {
return .{ .normalized = normalized, .entry = null };
};
return .{
.normalized = normalized,
.entry = self.getEntryWithCanonicalName(canonical.ptr),
.entry = self.getEntryWithNormalizedName(normalized),
};
}
// This is one of the wins of the canonical names...we can compare strings
// as a single pointer comparison. By applying the same canonicalization to
// the lists attributes name and to an input, we get the same pointer for
// a given string.
fn getEntryWithCanonicalName(self: *const List, name_ptr: [*]const u8) ?*Entry {
for (self._entries[0..self._len]) |*e| {
if (e._name_ptr == name_ptr) {
return e;
}
}
return null;
}
fn getEntryWithNormalizedName(self: *const List, name: String) ?*Entry {
const name_str = name.str();
for (self._entries[0..self._len]) |*e| {
@@ -514,10 +497,10 @@ pub fn validateAttributeName(name: String) !void {
}
// Every stored entry name either comes from the static String.intern or from
// the frame._attribute_names. This doesn't just avoid extra dupes/allocations,
// it gives us comparable pointer.
// For the lifetime of a frame:
// canonicalizeName("attrx").ptr == canonicalizeName("attrx").ptr
// the frame._attribute_names. Beyond avoiding extra dupes/allocations, this
// gives a stable pointer for the frame's lifetime, which List.LookupKey
// relies on for identity. The pointer is NOT comparable across frames (each
// frame has its own pool), which is why lookups byte-compare.
fn canonicalizeName(name: []const u8, frame: *Frame) ![]const u8 {
if (String.intern(name)) |static| {
return static;
@@ -529,19 +512,6 @@ fn canonicalizeName(name: []const u8, frame: *Frame) ![]const u8 {
return gop.key_ptr.*;
}
// Let's say you want to find an attribute name "attrx". We could iterate an
// attribute list to do the string comparison. OR, we could run "attrx" through
// the same canonicalization as we applied to the attribute's names. If we don't
// find a canonical value, then it's then this attribute was never canonicalized
// before (and thus, we can shortcircuit a search). If we DO canonicalize it
// the we get do a pointer comparison rather than a full string comparison.
fn lookupCanonicalName(name: []const u8, frame: *const Frame) ?[]const u8 {
if (String.intern(name)) |static| {
return static;
}
return frame._attribute_names.getKey(name);
}
fn normalizeNameForLookup(name: String, frame: *Frame) !String {
if (!needsLowerCasing(name.str())) {
return name;