diff --git a/docs/validation/owner-document-url-reflection-904.md b/docs/validation/owner-document-url-reflection-904.md
new file mode 100644
index 000000000..1d7812e9e
--- /dev/null
+++ b/docs/validation/owner-document-url-reflection-904.md
@@ -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 `` 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.
diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc
index 0fc9dd098..e662ae78f 100644
--- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc
+++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc
@@ -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 location;
diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_resource_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_resource_tests.inc
index 338d4382e..172f4969d 100644
--- a/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_resource_tests.inc
+++ b/experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_resource_tests.inc
@@ -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,
diff --git a/tests/WebPlatformSubset/chrome/owner-document-url-reflection-reference.html b/tests/WebPlatformSubset/chrome/owner-document-url-reflection-reference.html
new file mode 100644
index 000000000..31c17a738
--- /dev/null
+++ b/tests/WebPlatformSubset/chrome/owner-document-url-reflection-reference.html
@@ -0,0 +1,24 @@
+
+
+Owner document URL reflection reference
+
+
+