Skip to main content

What It's Like Fixing A Crash in the Ladybird Browser

How I tracked down and fixed a session history bug in Ladybird that crashed the browser when navigating back on single-page apps like Modrinth.

  • C++
  • Browsers
  • Open Source
  • Ladybird

By Jenn Barosa

Ladybird is an independent open-source web browser being built from scratch with its own engine, rendering pipeline, and JS runtime. It doesn't use Chromium, WebKit, or Gecko at all. One of the few serious attempts at a new browser engine in a long time.

I made my first contribution to Ladybird in March 2026, fixing a crash in the session history code. The fix was mentioned in the Ladybird newsletter.

Finding the Bug

There was an issue (#8269) where Ladybird crashes on Modrinth when you hit the back button. Go to modrinth.com, click settings, press back, and the whole browser dies.

After messing around with it for a bit I realized it wasn't a Modrinth thing at all. Any single page js app that did a replaceState followed quickly by a pushState would crash on back navigation. So it had to be something in how Ladybird handles history.

Reading the Code

Working in a browser engine is a different experience. You can't really just read the code like you normally would. Everything is implementing the HTML spec, so you end up with TraversableNavigable.cpp open on one side and the spec on the other, trying to match function names to spec algorithm steps.

The session history code is a lot to take in. There's navigations happening, history traversals queued up, documents loading and unloading, and all of it is async and touching the same state. The spec tries to define how all of that interleaves but some of the edge cases are genuinely unclear.

The Crash

I eventually traced it down to what happens when same-document navigations get superseded. If you do a replaceState and then a pushState quickly, the first navigation gets superseded by the second one. The finalization code for the superseded navigation does an early return since that navigation was abandoned, which makes sense. But the session history replacement that was supposed to happen as part of that navigation just never runs. So the session history and the Navigation API entry list get out of sync. One thinks there's N entries, the other thinks there's N+1. Hit back and the traversal code tries to grab an entry that only exists in one of the two lists.

The Fix

Ended up being nine lines in TraversableNavigable.cpp. When a same-document navigation gets superseded, the session history entry still needs to get updated before the early return. Added that so both lists stay in sync whether the navigation finishes or gets abandoned.

I also wrote a test case for it. Ladybird's test suite has this setup where it spins up a local HTTP server and serves HTML test pages to the browser, then compares the output against expected text files. So I wrote a simple HTML page that does replaceState then pushState, then navigates back, and an expected output file that checks the back navigation doesn't crash and you end up on the right history entry. I also needed to add my test to TestConfig.ini so the test runner knows to spin up the HTTP server for it since the history API needs a real page load, not just a local file.

Getting It Merged

Making a PR is great but actually getting it merged can be something else. A lot of times maintainers are way too busy and there's way too many PRs to review. It took about a week or so of sending the PR many times in their Discord channel for somebody to take a look and review, but thankfully, it was approved without any further comments.

Spec Stuff

Turns out the bug is related to an open spec issue about how superseded navigations should interact with session history. The spec doesn't really nail down what should happen to history entries when a navigation gets abandoned mid-flight. Ladybird's behavior before my fix was technically a valid interpretation of the spec, it just happened to crash.