The issue
Firefox stopped responding on my Mac, so I force-quit it. When I opened it again and chose Restore Session, Firefox appeared to hang. No windows came back.
This was a bad session to lose. I knew it covered several pieces of work and a lot of tabs, although I didn’t yet know the exact count.
I resisted the usual cycle of quitting and reopening the application. Firefox updates its live recovery files during startup, and I didn’t want a fresh one-tab session to replace the files I was trying to recover. I went to the profile directory first.
The active profile was listed in:
~/Library/Application Support/Firefox/profiles.ini
Its sessionstore-backups directory contained several Mozilla LZ4 files:
recovery.jsonlz4
recovery.baklz4
previous.jsonlz4
upgrade.jsonlz4-*
Their sizes immediately revealed something useful. The current recovery.jsonlz4 and recovery.baklz4 files were only about 1 KB. The older previous.jsonlz4 was 5.17 MB. The tiny files represented Firefox’s new recovery page, while the larger file was a plausible copy of the missing session.
Before touching any of them, I copied the recovery directory, profile index, and preferences to a timestamped folder on the Desktop:
FIREFOX_ROOT="$HOME/Library/Application Support/Firefox"
PROFILE="$FIREFOX_ROOT/Profiles/4aehpbj7.default-release"
BACKUP="$HOME/Desktop/Firefox Session Backup $(date +%Y-%m-%d_%H-%M-%S)"
mkdir -p "$BACKUP"
cp -p "$FIREFOX_ROOT/profiles.ini" "$BACKUP/"
cp -pR "$PROFILE/sessionstore-backups" "$BACKUP/"
cp -p "$PROFILE/prefs.js" "$BACKUP/"
Firefox session files contain URLs, page titles, form state and browsing history. They should be handled as private data and not attached to public issues or uploaded to an online decoder.
Firefox was still running at this point. It had a parent process and several content processes, yet WindowServer reported no visible Firefox windows. Sampling the parent process showed its main thread waiting in the normal AppKit event loop. It was alive, but there was nothing I could interact with.
Troubleshoot Mode eventually gave me a visible about:sessionrestore page. It said Firefox was having trouble getting the pages back, and the expandable list of previous tabs was empty. At least I now had a window. I still had no restore.
The cause
Firefox had created a pending crash report for the original failure. Its metadata contained the most useful clue:
GraphicsCriticalError: Managed to allocate after flush.
Killing GPU process due to IPC reply timeout.
ipc_channel_error: GPUProcessKill
MacMemoryPressure: Warning
AvailablePhysicalMemory: 265420800
The GPU process had stopped replying while macOS memory pressure was at Warning. Firefox killed the process after the IPC timeout, then the original browser instance exited.
The relaunch had its own failure. As AppKit tried to restore the Firefox windows, the macOS unified log repeatedly recorded:
Unable to find className=(null)
That lined up with what I could see from the process list: Firefox was running without an accessible window. When a recovery page finally appeared, it could not populate the previous-window table.
I still didn’t know whether the 5.17 MB file was usable. Mozilla’s jsonlz4 format starts with a mozLz40\0 header followed by LZ4-compressed JSON. I decoded previous.jsonlz4 locally with the system liblz4 library and parsed the result as JSON. The validation script printed counts only, so URLs and page titles stayed out of the terminal output.
The file parsed cleanly:
10 open windows
339 open tabs
5 closed windows
5 tabs in closed windows
748 history entries
That was the first point where I knew the tabs were still there. The crash report explained why Firefox had stopped. The AppKit errors explained the windowless relaunch. Neither had damaged the large session file.
Before asking Firefox to read the session again, I made a browser-independent fallback. I used Python’s lz4 package to decode the file locally:
python3 -m venv "$HOME/.firefox-session-tools"
"$HOME/.firefox-session-tools/bin/python" -m pip install lz4
I saved the following script as export-firefox-session.py:
#!/usr/bin/env python3
import html
import json
import os
import sys
from pathlib import Path
import lz4.block
MAGIC = b"mozLz40\0"
os.umask(0o077)
source = Path(sys.argv[1]).expanduser()
destination = Path(sys.argv[2]).expanduser()
payload = source.read_bytes()
if not payload.startswith(MAGIC):
raise SystemExit(f"{source} is not a Firefox jsonlz4 file")
session = json.loads(lz4.block.decompress(payload[len(MAGIC) :]))
sections = []
tab_count = 0
for window_number, window in enumerate(session.get("windows", []), start=1):
links = []
for tab in window.get("tabs", []):
entries = tab.get("entries", [])
if not entries:
continue
# Firefox stores the selected history entry as a 1-based index.
entry_number = tab.get("index", len(entries))
entry_number = min(max(entry_number, 1), len(entries))
entry = entries[entry_number - 1]
url = entry.get("url")
if not url:
continue
title = entry.get("title") or url
safe_url = html.escape(url, quote=True)
safe_title = html.escape(title)
links.append(f'<li><a href="{safe_url}">{safe_title}</a></li>')
tab_count += 1
sections.append(
f"<section><h2>Window {window_number}</h2><ol>"
f"{''.join(links)}</ol></section>"
)
document = f"""<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Recovered Firefox tabs</title>
<h1>Recovered Firefox tabs</h1>
{''.join(sections)}
</html>
"""
destination.write_text(document, encoding="utf-8")
destination.chmod(0o600)
print(f"Wrote {tab_count} links to {destination}")
The important part is that a Firefox tab is not stored as one URL. Each tab has
an entries array containing its back-and-forward history. The tab’s index
field is a 1-based pointer to the entry that was current when Firefox saved the
session. For example, an index of 3 selects the third history entry. It does
not default to the last entry in the list.
The exporter therefore does the following for every item in the top-level
windows array:
- loops through that window’s open
tabs; - clamps the saved index to the available history range and converts it to Python’s zero-based indexing;
- reads the URL and title from only that selected entry;
- uses the URL as the visible text if Firefox did not save a title;
- HTML-escapes both values before creating the link; and
- writes those links beneath a heading for that window.
That distinction matters. The session contained 748 history entries, but the
fallback contained 339 links: one current page for each open tab. It did not
export every Back-button entry, the five closed windows, form contents, cookies,
or the rest of the session state. The order of windows and tabs in the JSON
became the order of headings and links in the HTML file.
Nothing was uploaded and the script made no network requests. It read the
backed-up jsonlz4 file, decompressed and parsed it locally, then created a
standalone index of ordinary links. Opening the HTML file displayed the saved
titles; clicking a link would then ask the browser to load that URL normally.
The restrictive umask and final 0600 mode kept the generated file readable
only by my account.
I ran it against the untouched backup rather than the live profile:
PYTHON="$HOME/.firefox-session-tools/bin/python"
BACKUP="$HOME/Desktop/Firefox Session Backup 2026-08-03_14-55-59"
"$PYTHON" export-firefox-session.py \
"$BACKUP/sessionstore-backups/previous.jsonlz4" \
"$BACKUP/firefox-tabs.html"
The output was a local HTML file with 339 links grouped under ten window headings. It contains private browsing data, just like the original session file, so I kept it in the backup folder. If Firefox refused to restore the session, I could open that file and recover the pages manually.
The fix
With the original files backed up and the HTML export available, I stopped the windowless Firefox process. A zero-byte .parentlock remained in the profile. lsof showed that no process held it, so I removed the stale lock.
My first restore attempt used Troubleshoot Mode, which temporarily disables extensions and hardware acceleration:
open -na /Applications/Firefox.app --args -safe-mode
The recovery page became visible, but its tab table was still empty. I tried Troubleshoot Mode once more with -restore-last-session; it opened a new one-tab session. Troubleshoot Mode wasn’t going to restore this session.
For the next attempt, I wanted a normal Firefox launch with hardware acceleration disabled and tabs left unloaded. With Firefox fully stopped, I created user.js in the active profile:
user_pref("browser.startup.page", 3);
user_pref("browser.sessionstore.restore_on_demand", true);
user_pref("browser.sessionstore.restore_pinned_tabs_on_demand", true);
user_pref("layers.acceleration.disabled", true);
This configured Firefox to restore the previous session, use software rendering, and leave ordinary and pinned tabs unloaded until selected. I didn’t want all 339 pages loading together on the first successful startup.
I had already kept copies of the failed one-tab recovery files. I removed those files from sessionstore-backups, then placed the validated session where Firefox looks on a clean start:
PROFILE="$HOME/Library/Application Support/Firefox/Profiles/4aehpbj7.default-release"
BACKUP="$HOME/Desktop/Firefox Session Backup 2026-08-03_14-55-59"
rm -f "$PROFILE/.parentlock"
rm -f "$PROFILE/sessionstore-backups/recovery.jsonlz4"
rm -f "$PROFILE/sessionstore-backups/recovery.baklz4"
cp -p "$BACKUP/sessionstore-backups/previous.jsonlz4" \
"$PROFILE/sessionstore.jsonlz4"
cmp "$BACKUP/sessionstore-backups/previous.jsonlz4" \
"$PROFILE/sessionstore.jsonlz4"
cmp returned no output, confirming that the seeded file was byte-for-byte identical to the backup I had validated.
I launched Firefox normally and requested the last session:
open -na /Applications/Firefox.app --args -restore-last-session
Real page titles started appearing. All ten windows came back.
Firefox then wrote a new multi-megabyte recovery.jsonlz4. I parsed that live checkpoint and got the same 339 open tabs as the backup, along with the five closed windows. The checkpoint kept updating, the recent-crash count stayed at zero, and the GPU IPC timeout did not return in the macOS log.
I am keeping the untouched backup and the 339-link HTML export until Firefox has made it through several clean shutdowns and restores. Once I am satisfied that the new checkpoints are reliable, I can remove the forced startup setting and test hardware acceleration again. For the recovery itself, software rendering and on-demand tab loading stay in place.