A browser is more than a window capable of rendering HTML.
At first, the essential workflow appears simple: create a native window, place a WebView inside it, add an address bar, and load a URL. Once that works, the application already resembles a browser.
But resemblance is not architecture.
The difficult problems begin when remote pages, native resources, persistent sessions, internal protocols, downloads, tabs, and privileged tools must coexist inside the same process.
That distinction became increasingly clear while I was developing Grimley Browser, an experimental browser written in Rust.
From WebView to browser architecture
Grimley Browser uses Tao for native window and event management and Wry for platform WebViews. On Linux, the rendering backend is provided by WebKitGTK.
These technologies make it possible to render web content inside a native desktop application, but they do not automatically provide the architecture of a complete browser.
A real browser-like application still needs to answer several questions:
How are tabs identified and managed?
What happens to navigation state when a tab is closed?
How should sessions be restored?
Which pages may communicate with native code?
How are downloads constrained?
Can an internal tool trust content coming from a remote page?
What happens when callbacks arrive after their tab has already been destroyed?
Grimley Browser started as an exploration of these problems. Its current functionality includes tab management, session persistence, internal pages, navigation history, an experimental Shield layer, and a PDF workspace with local annotations.
The PDF workspace is where the most important architectural problem became visible.
Remote content and privileged interfaces are not equivalent
A normal website should be treated as untrusted content.
Even when a page is delivered over HTTPS, that does not mean it should receive access to commands capable of modifying local files, reading another tab’s documents, or invoking privileged application services.
An internal PDF workspace has different requirements. It needs controlled access to a downloaded document, annotation storage, document identity, and native persistence.
Initially, these two worlds were not separated strongly enough.
The application had an IPC handler available in content WebViews. Structural validation could confirm that a message looked like an annotation command, but it did not fully answer the more important question:
Was this page authorized to send that command?
Validation and authorization are not the same thing.
A valid message can still come from the wrong origin, the wrong tab, or the wrong document.
The architecture was changed so that ordinary remote pages use unprivileged WebViews. Privileged PDF services are available only inside dedicated internal workspaces. Commands are evaluated using the workspace origin, tab identifier, associated document, permitted operation, and message limits.
The intended boundary is now explicit:
Remote page
→ unprivileged WebView
→ no native PDF commandsThe internal flow is different:
Trusted PDF workspace
→ validated IPC message
→ tab and document authorization
→ restricted native serviceThis does not make the application universally secure. It does, however, make the trust model visible and testable.
When data accidentally becomes code
The audit also revealed a dangerous pattern in the PDF annotation flow.
Annotation data had previously been inserted into generated HTML through a script block. Escaping quotes and backslashes was not sufficient because content containing a closing script tag could terminate the original block.
At that point, data could be interpreted as executable markup.
The solution was not to add another replacement rule for a specific malicious string. The safer solution was to remove the interpolation pattern itself.
Annotations are now delivered through a restricted internal JSON endpoint using the application/json content type. The workspace reads structured data without embedding it as executable JavaScript.
Regression tests cover closing script tags, quotation marks, backslashes, line breaks, Unicode, control characters, and content resembling HTML.
One of the test values was:
</script><script>alert("grimley")</script>During graphical validation, that value remained literal text in both the interface and stored JSON. No script dialog was observed.
The broader lesson is simple: if a value is data, the architecture should keep it as data throughout the entire path.
A dependency can change the threat model
The original PDF workspace loaded PDF.js 3.11.174.
That version was affected by CVE-2024-4367 under configurations where evaluated code remained supported. The project was updated to PDF.js 5.4.624, with the library and worker packaged locally rather than loaded from a public CDN.
The workspace also configures PDF loading with evaluated code disabled.
Keeping the assets local reduces reliance on a remote runtime dependency and makes the exact version used by the application explicit. It does not eliminate every possible PDF risk: compressed, malformed, or unusually complex documents may still consume substantial resources.
For that reason, the native download flow also applies connection and operation timeouts, a 20 MiB document limit, HTTP status validation, MIME and file-signature checks, controlled redirects, and a bounded in-memory cache.
Security rarely comes from one perfect control. It usually comes from several limited controls that constrain different failure modes.
Tabs are native resources, not only interface elements
Another important lesson came from tab lifecycle management.
In a traditional web interface, removing a tab component may simply mean removing an element from the document. In a native WebView application, a tab may own operating-system resources, callbacks, event channels, persistent state, and child windows.
During graphical testing, the application encountered an X11 BadWindow failure while switching tabs. Its exact native cause was not conclusively proven, but the investigation exposed weaknesses in the lifecycle model.
The tab architecture was revised to use stable identifiers and explicit lifecycle handling. Commands and callbacks directed to tabs that are closing or already closed are rejected. WebViews are destroyed before the main window, and the event loop is explicitly awakened when IPC commands arrive.
Session restoration also received special attention.
Previously, an entry stored as a web tab could create a remote WebView before its PDF URL was intercepted. That sequence produced an additional PDF workspace.
PDF URLs are now classified before WebView creation, and the conversion process is idempotent. Restoring one session entry should produce exactly one tab.
Tests cover web pages, PDF tabs, mixed sessions, multiple PDFs, PDF URLs with query strings and fragments, and sessions where the active tab is a PDF.
Automated tests were necessary—and insufficient
After the security and lifecycle changes, the project reached:
59 passing Rust tests;
7 passing JavaScript security tests;
a clean Rust build;
successful formatting verification;
Clippy passing with warnings treated as errors.
Those results were important, but they were not enough.
Earlier in the process, automated tests and compilation passed while graphical execution still revealed a black window, PDF layout problems, duplicated workspaces, and a native X11 failure.
The final validation therefore included a reproducible graphical smoke test on Fedora KDE through X11/XWayland.
Ten complete cycles and 21 application launches exercised:
internal pages;
local web content;
PDF rendering;
repeated tab switching;
different tab-closing orders;
application shutdown;
session restoration;
annotation creation, editing, deletion, and persistence.
All ten final cycles completed without a failure.
This provides evidence for the tested environment. It does not prove that the same result will hold across every Linux distribution, graphics driver, operating system, or WebView backend.
Current limitations
Grimley Browser remains an Early-stage project.
Native Wayland operation is not currently supported by its Linux integration. On the tested Fedora KDE environment, execution required X11/XWayland and disabling the WebKit DMABUF renderer for that process.
The browser also does not yet provide several features expected from an everyday browser, including bookmarks, complete address-bar search, a full download manager, comprehensive shortcuts, extensions, or mature privacy controls.
The experimental Shield layer should not be described as complete advertisement or tracker blocking. Authenticated PDF documents may also fail when their download depends on cookies held by a different WebView session.
These limitations are not footnotes to hide. They define the current boundary of the project.
What building Grimley Browser taught me
The most important lesson was not how to render a website inside a Rust application.
It was that every bridge between remote content and native code creates a trust decision.
A message can be structurally valid and still be unauthorized. A test suite can be green while the graphical system remains unstable. A hidden WebView can still own resources. A restored URL can create two workspaces if classification happens too late. A string can become executable code when data and scripts share the same transport.
Building browser software means designing boundaries: between tabs, documents, origins, processes, persistent state, and native services.
A WebView renders the page.
The architecture decides what that page is allowed to influence.