Commit Graph

8347 Commits

Author SHA1 Message Date
Karl Seguin
0fd73650fc Merge pull request #2801 from lightpanda-io/document-setDomain
webapi: Expose document.domain setter
2026-06-25 14:13:59 +08:00
Karl Seguin
5c902b6cfe crash: Fix handlescope corruption on iframe load
In trying to fuzz test a different issue, I ran into a reproducible case where
we end up with a broken handlescope stack. The issue requires a large number
of handlescopes created with aggressive GC, so hopefully it isn't something
that too many users have run into.

The issue is that a single HandleScope address is used to initialize two
HandleScopes. The fix could just be to create a 2nd HS variable (to get a 2nd
address), but the first initialization is unnecessary and can just be removed.

(Note that EventManager dispatch creates its own HandleScope, so the one
removed in frameCompletedLoading really did nothing)
2026-06-25 10:52:39 +08:00
Karl Seguin
f2eaeb1c9a uaf: Prevent double-free on BrowserContext deinit with pending interception
Adds a abortParked (to be used instead of abort for a parked transfer) which
guards against a double-free by setting the state to .completing BEFORE running
the requestFailed callback. Without this, requestFail could itself cause the
transfer to be cleared.

Also added a guard to try to catch double transfer deinit's in Debug. Because
the memory could get re-used between the first and second free, the lack of
failure doesn't prove there is no UAF. But it's cheap to do and debug only.
2026-06-24 20:09:00 +08:00
Karl Seguin
630826160e cli: fetch multiple urls
Modified the fetch command to support multiple URLs. Only allowed when --json
is specified (we can revisit this limitation if anyone asks for it, but for now
it's the only non-ambiguous way to get multiple results).

Probably not a big surprise, but the performance gain comes from being able to
concurrently fetch resources. On fast network, or with caching enabled, the
difference wouldn't be so significant:

```
time begin;
./lightpanda fetch "https://github.com/lightpanda-io/browser/" --json;
./lightpanda fetch "https://www.reddit.com/r/Zig/comments/1p0ur0d/i_started_learning_zig" --json;
end
10.28 secs
```

vs

```
time ./lightpanda fetch --json \
  "https://github.com/lightpanda-io/browser/" \
  "https://www.reddit.com/r/Zig/comments/1p0ur0d/i_started_learning_zig"
5.12 secs
```
2026-06-24 18:16:02 +08:00
Karl Seguin
39046492f1 design: Runner supports multi-pages
Follow up to https://github.com/lightpanda-io/browser/pull/2789 which adds
better multi-page support for runner. When waiting, callers have the choice
to wait for a specific frame, all current frames, or a given list of frames. The
last one, a given list of frames, is the most flexible, allowing callers to
provide an `until` per frame AND receive a per-frame result.

waitForSelector and waitForScript continue to be per frame (which is now
explicitly given).

Like 2789, while the changes aren't insignificant (Runner is doing important
work), a number of files were touched either purely because of changes in tests
or other superficial changes, e.g. `session.runner` is now infallible.
2026-06-24 17:44:58 +08:00
Pierre Tachoire
8ccbc4d730 Merge pull request #2790 from lightpanda-io/hash-change-event
webapi: add HashChangeEvent and Window hashchange event
2026-06-24 06:28:41 +00:00
Pierre Tachoire
72af3de5d0 Merge pull request #2805 from lightpanda-io/fix-homebrew-release
ci: fix homebrew release workflow name
2026-06-24 06:28:07 +00:00
Pierre Tachoire
4e97d1e2e9 ci: fix homebrew release workflow name 2026-06-24 08:13:36 +02:00
Karl Seguin
7b585df7ff Merge pull request #2789 from lightpanda-io/multi-page-sessions
design: multi-page session
2026-06-24 07:39:40 +08:00
Karl Seguin
6d1efd15e0 remove stale comment 2026-06-24 07:01:32 +08:00
Karl Seguin
9482a9af98 Cleanup sloppy tests, Fix potential UAF, smaller cleanup (PR feedback).
Cleanup sloppy tests, make sure pages are always closed after page tests - not
before and not never.

Fix a UAF in replaceRootImmediate..if a page is being retired, also retire its
replacement.

Give BrowserContext a Session.PageHandle rather than having a frame_id and re-
implementing PageHandle logic.
2026-06-24 07:01:31 +08:00
Karl Seguin
da6cd66e36 design: multi-page session
Replaces the 1 (+1 inflight) page design of session for an unlimited number of
pages. The main goal is to support a more efficient async goto (1). Without this
commit, async goto has two implementation: multiple browser, which is resource
intensive (thread + isolate per page), bolting it onto sub-frames (like iframe
or popups). The issue with the 2nd version (which I originally suggested) is
that most resources are tied to the Page. So even if an async goto "page" (which
would map to a Frame) is released, most things stay in memory, including the
DOM (page.factory) and the V8::Context.

This new approach adds multiple page support to sessions. The advantage is
pretty clear: the existing memory model (page-tied resources) becomes a strength
of the design, rather than a weakness.

This change is not tirivla, but the diff is inflated by 2 large mechanial
changes, so it isn't _that_ big either. That said, I'd divide this into four
parts.

1 - The old concept of _active/_pending is now baked into the Page itself. A
Page has a `replaces: ?*Page = null` and `replacement: ?*Page = null` field.

2 - Because of #1 above, the Session now just has an `pages: ArrayList(*Page)`.
As much as possible, single-page APIs, like `removePage` no longer exists. We're
trying to present a consistent multi-page API. See #3.

3 - References to *Page and *Frame have always been dangerous. For example,
`lightpanda.fetch` has a block to scope `frame`:

```zig
{
    const frame = try session.createPage();
    // frame isn't safe to use after navigate, it can be swapped out
    _ = try frame.navigate(...)
}
```

While we still hand out *Page and *Frame (user-beware), some of the more
important APIs now take a frame_id which Session can resolve. Furthermore,
createPage now returns a PageHandle which is a safe wrapper around a page/frame.

4 - Two large mechanical changes were made:
  a - Many tests were superficially changed to account for new naming or use
      a new test-helper to preserve the single-page illusion (because 99% of
      tests _are_ single-page)
  b - Many CDP changes where `bc.session.currentFrame()` -> `bc.mainFrame()`).
      This isn't to say CDP changes are meaningless, but it's mostly 1 change
      about how the "main" target/frame_id is tracked (by CDP itself, rather
      than the Session) that required a number of superficial changes to
      accomodate

`Runner` remains largely single-page focused. Runner and some MCP/Agent tools
continue to be tied to the "currentFrame" or "primaryPage". The Runner is
something I want to address in a follow-up PR, but we need to figure out what
it means to "wait" for multiple pages.
As much as possible, Session becomes multi-page native and has no concept of
a special/first/primary/main page. Users of session become responsible for
tracking pages of interest.

This change is not trivial, but the diff is inflated by 2 large mechanical
changes:
1 - Many tests

(1) https://github.com/lightpanda-io/browser/pull/2759
2026-06-24 07:01:29 +08:00
Karl Seguin
48cb0848cc Merge pull request #2799 from lightpanda-io/moveBefore
webapi: moveBefore
2026-06-24 06:53:46 +08:00
Karl Seguin
068ad7b0b1 Merge pull request #2796 from lightpanda-io/strip-mode-invisible
cli: add strip-mode invisible
2026-06-24 06:53:17 +08:00
Karl Seguin
48ed689c20 Merge pull request #2795 from lightpanda-io/innerText-visibility
webapi: innerText  uses StyleManager's visibility
2026-06-24 06:51:14 +08:00
Pierre Tachoire
846d6fd816 fix: single source of truth for hashchange fragment decision
History.goInner re-derived the fragment-vs-document decision from the raw
entry._url, while navigateInner computes it from the resolved URL. These
diverge when an entry stores a non-canonical URL, queuing a spurious
hashchange (or missing one). Let navigateInner own the decision and queue
the event for all same-document navigations; drop goInner's duplicate.
2026-06-23 18:03:04 +02:00
Pierre Tachoire
b989ea6b47 remove useless comment 2026-06-23 17:42:27 +02:00
Pierre Tachoire
c450247b31 fix UAF: copy urls into event's arena 2026-06-23 17:31:12 +02:00
Pierre Tachoire
5b97450cfa Merge pull request #2802 from lightpanda-io/feat/cdp-viewport-browser-level
cdp: return browser's viewport on getLayoutMetrics
2026-06-23 15:28:32 +00:00
Pierre Tachoire
82cd61e75c cdp: return browser's viewport on getLayoutMetrics 2026-06-23 15:22:16 +02:00
Pierre Tachoire
55568907ba Merge pull request #2791 from rohitsux/feat/cdp-viewport-browser-level
refactor(cdp): store viewport override at the browser level
2026-06-23 13:14:39 +00:00
Karl Seguin
b6d63a9c91 webapi: Expose document.domain setter
This is a deprecated API, but it's used in various WPT tests. I don't expect
WPT tests to necessarily improve, but they should get further than just failing
on the setter.

This also includes a change where BroadcastChannel now uses the origin string
rather than the js.Origin identity because BroadcastChannel should be based
on the actual frame's origin...which used to be captured in Context.origin but
which the new document.domain can change.
2026-06-23 20:02:49 +08:00
Pierre Tachoire
53962b675e Merge pull request #2800 from lightpanda-io/trigger-homebrew-formula
ci: trigger homebrew-browser formula update after release
2026-06-23 11:35:13 +00:00
Pierre Tachoire
4314148b70 ci: trigger homebrew-browser formula update after release 2026-06-23 11:58:22 +02:00
Karl Seguin
1e06ec918e webapi: innerText visibility fix
When "rendering" innerText, we don't care about the visibility of the entire
tree, we just care about the visibilty of the element itself and then all of
it's children. Or, put differently:

```
<div style=display:none>
  <div id=x>spice</div>
</div>
<script>
console.log(document.getElementById('x').innerText)
</script>
```

Should still print "spice"..even though 'x' is inside a hidden element. The
original version used the StyleManager's isHidden which fails this test - it
scans all the parents. This new version just considers the visibility of each
element being displayed. This is faster and more correct
2026-06-23 09:57:44 +08:00
Karl Seguin
bedde28dde add unit tests for dump 2026-06-23 09:31:33 +08:00
Karl Seguin
c14d2b4eef update help text 2026-06-23 08:17:09 +08:00
Karl Seguin
ad9bbef2b8 webapi: moveBefore
Add Element/DocuemntFragment/Document moveBefore. The API is relatively
complicated compared to other node-moving APIs, so there are still some edge
cases. But this implementation should be at a level where having the API fixes
more things than it breaks.
2026-06-23 08:12:29 +08:00
Navid EMAD
2ed49ecb8d css: apply @layer block rules to the cascade
Rules wrapped in @layer blocks (named, dotted sub-layer names, or
anonymous) were dropped from the cascade: StyleManager.parseSheet applied
top-level style rules and recursed into @media only, so @layer fell into
the silent else arm on both the _css_rules path and the <style> text path.
@layer blocks now flatten into the cascade like a matching @media body,
recursing into nested @media/@layer in both directions under the shared
MAX_AT_RULE_NESTING depth cap. The statement form (@layer a, b;) declares
ordering only and stays inert. Layer-priority ordering (css-cascade-5
section 6.4) is deliberately out of scope for this change: flattening with
the existing specificity + source-order tie-breaking fixes the common
failure mode of layered rules vanishing entirely (e.g. Tailwind v4 emits
its whole compiled output inside @layer, so .hidden { display: none }
never applied and checkVisibility()/getComputedStyle() reported hidden
elements as visible).

cssRules-inserted layer rules (insertRule/replaceSync) get their own
CSSRule.Type variant so the cascade pass can recognize them; their
JS-visible type stays 0 per CSSOM section 6.4.1 (no legacy code assigned),
matching Chrome's CSSLayerBlockRule.

Closes #2718
2026-06-23 00:22:17 +02:00
Pierre Tachoire
b78940d7f6 remove useless url duplications 2026-06-22 22:09:21 +02:00
Pierre Tachoire
84098c0189 webapi: add HashChangeEvent and Window hashchange event
Implement the HashChangeEvent interface and fire a `hashchange` event on
the window whenever the URL fragment changes without a document reload,
mirroring the existing PopStateEvent/onpopstate wiring.

Per spec, hashchange is fired by queuing a task rather than synchronously,
so a listener registered after the fragment change still observes it and
each fragment change fires its own event. The task is queued via a
factory-allocated callback (the same async-dispatch pattern as MessagePort
and BroadcastChannel), capturing the old/new URLs since the frame's URL
keeps changing synchronously before the task runs.
2026-06-22 22:09:16 +02:00
Pierre Tachoire
2664670947 Merge pull request #2798 from lightpanda-io/release-docker
call browser-docker workflow after installation
0.3.3
2026-06-22 16:14:08 +00:00
Pierre Tachoire
ff6fc57d3d call browser-docker workflow after installation 2026-06-22 18:00:48 +02:00
Pierre Tachoire
4a71e0a2ad fix: work around libcurl Brotli trailing-byte rejection (CURLE_WRITE_ERROR)
Some CDNs (e.g. CloudFront/Brave Search) emit Brotli streams with 1+
trailing bytes after the logical end. The Brotli decoder succeeds, but
libcurl's brotli_do_write() (content_encoding.c:439) rejects unconsumed
input bytes as CURLE_WRITE_ERROR — even though all decompressed data was
delivered to the write callback.

Browsers accept such responses, so match that: when CURLE_WRITE_ERROR
arrives but our callback never errored and bytes were received, downgrade
to success.
2026-06-22 15:51:37 +02:00
Pierre Tachoire
92da6f90ff Merge pull request #2792 from lightpanda-io/preload-shutdown-callback
fail pending module/preload fetches killed on teardown
2026-06-22 12:12:25 +00:00
Karl Seguin
23f5b8414e cli: add strip-mode invisible
Adds a new strip-mode "invisible" which consults the StyleManager to remove
hidden elements. However, unlike the general isHidden check in StyleManager
which is spec-compliant and will automatically hide a number of tags, e.g.
"meta", this check is explicitly against explicit styles. This seems more
practical to me, while it might be useful to remove some tags by default, e.g.
template, some tags like head, title and meta are too important to strip out.
2026-06-22 18:57:44 +08:00
Karl Seguin
23e72cf573 webapi: innerText uses StyleManager's visibility
innerText now uses the StyleManager's visibility check (isHidden) to skip
rendering children which are hidden.

Should allow us to close
https://github.com/lightpanda-io/browser/pull/2741 and
https://github.com/lightpanda-io/browser/issues/2734

Note that ~1000 WPT test still fail because of other style-driven behavior, most
significantly is `white-space`. The isHidden change only fixes a couple WPT
cases, but I think, for end users, it's the more important change (and it's also
the simpler for us to implement). Having wrong spacing is usually less of a
problem than showing the wrong content.
2026-06-22 18:26:31 +08:00
Pierre Tachoire
e61d618a35 Merge pull request #2793 from lightpanda-io/fix-log-message
cdp: fix log msg too long
2026-06-22 10:20:32 +00:00
Pierre Tachoire
dc4ddf6913 cdp: fix log msg too long 2026-06-22 11:57:30 +02:00
Pierre Tachoire
b9823b8919 fail pending module/preload fetches killed on teardown
A queued module/preload transfer aborted during a page-swap teardown fired
shutdown_callback (unset on these requests), not error_callback, leaving its
entry stuck `.loading` and hanging waitForImport/waitForPreload. Register a
JS-free shutdown_callback that moves the entry off `.loading` so the waiter
unblocks.
2026-06-22 11:42:46 +02:00
Rohit
81f848d5d1 refactor(cdp): store viewport override at the browser level
Follow-up to #2664: move the Emulation.setDeviceMetricsOverride viewport
override from the Page to the Browser so it persists across page navigations
for the whole CDP connection, instead of being lost when the page is replaced.
Page.getViewport now delegates to Browser.getViewport, so every viewport
consumer keeps reading through a single path unchanged.
2026-06-22 14:20:15 +05:30
Pierre Tachoire
f61d16aa49 Merge pull request #2664 from rohitsux/feat/cdp-set-device-metrics
feat(cdp): honor viewport in Emulation.setDeviceMetricsOverride
2026-06-22 07:12:23 +00:00
Karl Seguin
39f9792bac Merge pull request #2785 from lightpanda-io/innerText
webapi: Improve innerText, add outerText
2026-06-21 08:23:55 +08:00
Karl Seguin
04e1b13af2 Merge pull request #2788 from lightpanda-io/popup_close_lifetime
uaf: Extend closed popup's lifetime from Frame to Page
2026-06-21 08:23:40 +08:00
Rohit
8580f92488 feat(cdp): honor viewport in Emulation.setDeviceMetricsOverride
Now that #2694 centralized the default viewport, store an optional override
on the Page and have every viewport consumer read it through
Page.getViewport(): window.innerWidth/innerHeight, screen.width/height/
availWidth, visualViewport.width/height, Page.getLayoutMetrics, the
IntersectionObserver root rect, and matchMedia / the StyleManager media
cascade. This keeps the viewport consistent across all of them instead of
the split-brain a single-property override would create.

Emulation.setDeviceMetricsOverride writes the override (a 0 width or height
keeps that dimension, per the CDP default); clearDeviceMetricsOverride resets
to the default. Not-yet-emulated params (deviceScaleFactor, mobile, scale,
screenWidth/Height) are accepted but logged as not_implemented.
2026-06-20 07:18:12 +05:30
Karl Seguin
6aa4d50b33 webapi: Improve innerText, add outerText
Aimed at improving correctness of the innerText getter and setter. Driven by a
number of WPT tests, largely in html/dom/elements/the-innertext-and-outertext-properties/

Meant as a replacement for (1). Though, unlike that PR, this commit does not
consider CSS at all (todo in a follow up PR). The change just to get correct
space handling is big enough on its own. The short version is that, as we
traverse the children, we need to keep various state in order to (for example)
avoid double whitespace. Table cells and rows need their own special handling
(tabs and newlines inserted between items), pre needs to preserve whitespace,
etc.

This also adds outerText which, for the getter, is identical to innerText and
for the setter, can reuse Element.replaceWith, except for special whitespace
handling at its edges.

(1) https://github.com/lightpanda-io/browser/pull/2741
2026-06-20 08:37:41 +08:00
Karl Seguin
e9bbf95cf5 Merge pull request #2787 from lightpanda-io/frame_breakup
cleanup: Break various frame behavior into their own little utility f…
2026-06-20 07:09:00 +08:00
Pierre Tachoire
5d62fdc0fc Merge pull request #2773 from lightpanda-io/internal-request-flag
Internal Flag on Request
2026-06-19 15:18:02 +00:00
Pierre Tachoire
a808386c76 Merge pull request #2722 from Ar-maan05/feat-cdp-download-behavior-2701
feat(cdp): implement Browser.setDownloadBehavior file downloads
2026-06-19 12:57:42 +00:00
Pierre Tachoire
0f5ee79a1b Download events mustn't send session id 2026-06-19 14:31:40 +02:00