Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/validation/owner-document-url-reflection-904.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Owner-document URL reflection — WebScene #904

On the merged WebScene `5b4739c8` installed Code OSS build, the nested
Markdown prelude belongs to a virtual HTTPS document and authors
`./fake.html?id=...`. Reading its iframe `src` from the main realm returned a
loopback `/fake.html` URL, while `ownerDocument.baseURI` remained virtual
HTTPS. This is a URL-property reflection defect; actual request navigation
uses a separate owner-root base path and was not shown to be wrong.

`get_element_url` now resolves URL-valued properties through the element's
owning cascade/document root instead of the caller's current document. Its
existing fallback remains for unavailable bases. The change shares the same
owner-root/base cache as resource navigation. It leaves the authored content
attribute and cross-origin access policy unchanged.

## Executed focused gates

- Production-style V8 native engine and source-test targets built in Release
with pinned V8 pointer compression and shared cage.
- `WEBSCENE_NATIVE_ENGINE_TEST_FILTER=iframe-navigation-lifecycle` passed.
The added regression reads connected `a.href` and `input.src` from a
same-origin parent realm before and after a nested `<base>` change and
checks that `getAttribute` stays authored. The existing gate also passed
100 iframe create/load/write/replace/remove cycles, 1 MiB update p95
3.73 ms (250 ms limit), flat heap before/after at 1,849,528 bytes,
cross-origin/opaque access denial, and history ordering.
- The checked-in `tests/WebPlatformSubset/chrome/owner-document-url-reflection-reference.html`
returned `PASS` under installed Chrome 152 with `--headless=new
--virtual-time-budget=3000 --dump-dom`. It checks the same nested `href`,
`src`, base mutation, and authored-attribute expectations.
- `git diff --check` passed.

The existing standalone cross-document-history WPT profile cannot currently
serve its nested fixture through this native runner, and a new `srcdoc` or
initial `about:blank` contract timed out. Those attempts were removed from
the PR; no WPT pass is claimed. The direct native source gate and checked-in
Chrome reference carry this focused regression. The broader WebScene #267
WPT/browser matrix and unchanged Markdown rendering remain open. The Chrome
CDP runner also failed to expose a debuggable page target on this host, so
the standalone headless DOM check was used for the browser oracle.
Original file line number Diff line number Diff line change
Expand Up @@ -5125,7 +5125,10 @@
|| node->tag == "video"));
if (iterator != node->attributes.end() && url_reflected) {
auto* self = current(info.GetIsolate());
auto base = self->current_base_address();
// URL reflection follows the element's owning document, even when
// a same-origin caller reads it from another browsing context.
auto base = self->effective_document_base(
self->css_cascade_root_for_node(*node));
if (base.empty()) {
auto local_context = info.GetIsolate()->GetCurrentContext();
v8::Local<v8::Value> location;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,61 @@ void test_same_origin_iframe_navigation_document_replacement()
"nested cross-document traversal admitted an unexpected resource request: before="
+ std::to_string(requests_before_cross_document_history)
+ " after=" + std::to_string(server.requests.load(std::memory_order_relaxed)));
execute(engine, R"JS(
globalThis.__ownerDocumentUrlReflection = {
complete:false, first:false, changed:false, srcFirst:false,
srcChanged:false, authored:false, error:null
};
(() => {
const frame = document.createElement('iframe');
frame.setAttribute('sandbox', 'allow-same-origin allow-scripts');
frame.src = './fake.html?id=markdown-preview';
document.body.appendChild(frame);
frame.contentWindow.addEventListener('DOMContentLoaded', event => {
try {
const child = event.target;
const link = child.createElement('a');
link.setAttribute('href', './relative.txt');
child.body.appendChild(link);
const input = child.createElement('input');
input.setAttribute('src', './relative.png');
child.body.appendChild(input);
__ownerDocumentUrlReflection.first =
link.href === new URL('./relative.txt', child.baseURI).href;
__ownerDocumentUrlReflection.srcFirst =
input.src === new URL('./relative.png', child.baseURI).href;
const base = child.createElement('base');
base.setAttribute('href', '/nested-owner/');
child.head.appendChild(base);
__ownerDocumentUrlReflection.changed =
link.href === new URL('./relative.txt', child.baseURI).href
&& link.href.endsWith('/nested-owner/relative.txt');
__ownerDocumentUrlReflection.srcChanged =
input.src === new URL('./relative.png', child.baseURI).href
&& input.src.endsWith('/nested-owner/relative.png');
__ownerDocumentUrlReflection.authored =
link.getAttribute('href') === './relative.txt'
&& input.getAttribute('src') === './relative.png';
} catch (error) {
__ownerDocumentUrlReflection.error = String(error && error.stack || error);
} finally {
frame.remove();
__ownerDocumentUrlReflection.complete = true;
}
});
})();
)JS", "iframe-owner-document-url-reflection.js");
require(evaluate_until_equals(engine, "__ownerDocumentUrlReflection.complete",
"iframe-owner-document-url-reflection-complete.js", "true", 4'000) == "true",
"nested owner-document URL reflection timed out");
require(evaluate(engine, R"JS((() => {
const s = __ownerDocumentUrlReflection;
return s.error === null && s.first && s.changed && s.srcFirst
&& s.srcChanged && s.authored;
})())JS", "iframe-owner-document-url-reflection-result.js") == "true",
"nested owner-document URL reflection diverged: "
+ evaluate(engine, "JSON.stringify(__ownerDocumentUrlReflection)",
"iframe-owner-document-url-reflection-detail.js"));
execute(engine, R"JS(
globalThis.__iframeLifecycleGate = {
complete:false, error:null, cycles:0, maximumLiveFrames:0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html>
<meta charset="utf-8">
<title>Owner document URL reflection reference</title>
<output id="result">PENDING</output>
<iframe id="child" srcdoc="<!doctype html><head><base href='https://example.test/first/'></head><body></body>"></iframe>
<script>
child.addEventListener('load', () => {
const doc = child.contentDocument;
const link = doc.createElement('a');
link.setAttribute('href', './relative.txt');
doc.body.appendChild(link);
const input = doc.createElement('input');
input.setAttribute('src', './relative.png');
doc.body.appendChild(input);
const first = link.href === 'https://example.test/first/relative.txt'
&& input.src === 'https://example.test/first/relative.png';
doc.querySelector('base').setAttribute('href', 'https://example.test/second/');
const second = link.href === 'https://example.test/second/relative.txt'
&& input.src === 'https://example.test/second/relative.png';
result.textContent = first && second && link.getAttribute('href') === './relative.txt'
&& input.getAttribute('src') === './relative.png'
? 'PASS' : `FAIL first=${first} second=${second} href=${link.href} src=${input.src}`;
});
</script>
Loading