Skip to content

fix: verify the downloaded browser can resolve its libraries - #8639

Open
RadiumGu wants to merge 1 commit into
kirodotdev:mainfrom
RadiumGu:fix/verify-browser-shared-libraries
Open

fix: verify the downloaded browser can resolve its libraries#8639
RadiumGu wants to merge 1 commit into
kirodotdev:mainfrom
RadiumGu:fix/verify-browser-shared-libraries

Conversation

@RadiumGu

@RadiumGu RadiumGu commented Sep 5, 2026

Copy link
Copy Markdown

Problem

install-browser reported success on a host missing 12 shared libraries. browser_ok reported true, the settings panel went green, and the failure only arrived at the user's first browse:

chrome-headless-shell: error while loading shared libraries: libatk-1.0.so.0:
cannot open shared object file: No such file or directory

os_deps.host_deps_unsatisfied exists to catch exactly this — it keys on Playwright's "Host system is missing dependencies to run browsers" text, because Playwright exits 0 and classifies it as a warning. It never fired.

Root cause is upstream, and specific to linux-arm64

Playwright's registry declares the directory to scan:

_validateHostRequirements: (sdkLanguage) =>
  this._validateHostRequirements(sdkLanguage, chromium.dir, ["chrome-linux"], [], ["chrome-win"]),

But the arm64 build unpacks into chrome-linux-arm64. So executablesOrSharedLibraries() walks a path that does not exist, missingDeps stays empty, if (!missingDeps.size) return; takes the clean exit — and the caller then writes a DEPENDENCIES_VALIDATED marker that suppresses re-validation for 30 days.

MEASURED on Amazon Linux 2023 arm64:

evidence timestamp
chromium-1243/DEPENDENCIES_VALIDATED written 02:23:08
atk / mesa-libgbm actually installed 02:46:30

The marker was written 23 minutes before the libraries existed. The controlled comparison is webkit in the same session on the same host: its declared directory does match its layout, so validation threw, printed the warning, and wrote no marker. Same Playwright, same host, opposite outcome — the directory name is the variable.

This is a false negative, not a missing warning, so no amount of text matching can fix it.

Change

os_deps.missing_shared_libraries() reads the real files: enumerate the ELF objects actually present and ask ldd what cannot be resolved. It never hardcodes a build subdirectory — that assumption is the upstream bug.

A successful download is followed by that probe, and a missing library becomes its own failed step (verify-browser-libraries-<engine>) rather than flipping the download's verdict. The download genuinely succeeded; labelling it failed would send the operator to re-fetch bytes already on disk when what they need is root and a package manager. The step carries the missing sonames plus the existing missing_deps_hint() remedy, and reports returncode: 0 honestly because nothing exited non-zero.

The engine's cache dirs are prefix-matched so chromium also covers chromium_headless_shell-<rev> — headless is the default launch mode, so the shell is the binary a browse actually starts, and checking only chromium-<rev> would clear a build that is not the one being run.

Two guards against blocking installs that work

Found by running the probe on a real host rather than only against mocks — the first version would have been worse than the bug:

  1. Bundled libraries. A browser ships libraries it loads from beside itself. Without the build's own directories on LD_LIBRARY_PATH (and a second exclusion by file name), firefox reported libxul.so, liblgpllibs.so, libmozsqlite3.so and 7 more as missing while launching perfectly. Playwright passes its own directory list for this same reason.
  2. ldd's header line. ldd prefixes its report with <path>:, which is not a soname. Unfiltered it was reported as a missing library named after an absolute path.

Unknown is not failure

Off Linux, with no ldd, or on timeout the probe answers None and the install proceeds untouched. The download needs no privilege and may be perfectly good; turning "could not check" into "broken" would withdraw working browsers.

Verification

On the host that exhibited the bug:

engine probe result step added
chromium (+ headless shell) no missing libraries none
firefox no missing libraries none
webkit 26 missing verify-browser-libraries-webkit, ok=false

webkit is a genuine positive, not a synthetic one: its dependencies (libavif, libenchant-2, libflite*, …) are deliberately absent from _RPM_CHROMIUM_PACKAGES, which is Chromium-scoped by design. Cross-checked by launching it — it fails to load exactly those libraries.

15 new tests. 328 passed / 3 skipped across test_browser_cli_install.py, test_browser_cli_os_deps.py, test_browser_cli_launch.py, test_browser_cli_view.py, test_playwright_cli_installer.py, test_spawn_audit.py, test_no_stale_browser_mcp_refs.py.

test_spawn_audit correctly flagged the new ldd spawn; it is added to BENIGN_SPAWNS with a justification. It is a fixed two-element argv over paths enumerated from Playwright's own cache (never agent-named, and a hostile file name cannot become a command because argv is a list), and it must not route through sandboxed_spawn_argv: the probe answers whether this host can resolve the libraries, and a scrubbed environment would answer for the sandbox instead — LD_LIBRARY_PATH is the one input that has to survive to the child.

Scope

detect() / browsers_present() stay presence-and-revision only. They are on the /api/status poll path, so adding an ldd sweep there would put a subprocess fan-out behind every dashboard poll. The consequence is that a browser whose libraries went missing after install still reads green in the panel until the next install attempt; closing that needs a cached probe keyed on directory mtime, which is a separate change.

Independent of #8637 (unwritable npm prefix) — different functions in install.py, no overlap.

Playwright's host validation cannot be trusted to notice that a browser
it just downloaded cannot launch, so `install-browser` reported success
on a host missing 12 shared libraries, `browser_ok` reported true, the
panel went green, and the failure only arrived at the user's first browse
as an opaque stack trace.

The cause is upstream and specific to linux-arm64: Playwright's registry
declares the directory to scan as `chrome-linux`, while the arm64 build
unpacks into `chrome-linux-arm64`. It scans a path that does not exist,
finds no dependencies at all, concludes the host is fine and writes its
DEPENDENCIES_VALIDATED marker -- which then suppresses re-validation for
30 days. MEASURED on Amazon Linux 2023 arm64: that marker was written 23
minutes BEFORE the libraries were installed. The same run on webkit,
whose declared directory does match, threw and wrote no marker -- the two
side by side are what identify the directory name as the cause.

So this is a false negative, not a missing warning, and
`host_deps_unsatisfied` -- which keys on Playwright's warning text -- can
never fire for it.

`missing_shared_libraries` therefore reads the real files: it enumerates
the ELF objects actually on disk and asks ldd what cannot be resolved,
never hardcoding a build's subdirectory name, since that assumption is
the upstream bug. A successful download is followed by that probe, and a
missing library becomes its own failed step rather than flipping the
download's verdict -- the download genuinely succeeded, and labelling it
failed would send the operator to re-fetch bytes already on disk when
what they need is root and a package manager.

Two guards keep the check from blocking installs that work. Libraries the
build ships itself are excluded, both by passing the build's own
directories as LD_LIBRARY_PATH and by name: without that, firefox
reported libxul.so and nine others missing while launching perfectly.
And ldd's `<path>:` header line is not a soname -- unfiltered it was
reported as a missing library named after an absolute path.

"Could not determine" is distinct from "nothing missing" throughout: off
Linux, with no ldd, or on timeout the probe answers None and the install
proceeds. An absent probe is not evidence of a broken browser.

Verified on the failing host: chromium and firefox both report no missing
libraries and add no step, while webkit -- whose dependencies are not in
the rpm package list -- reports 26 and fails the step with the remedy.
@RadiumGu
RadiumGu requested a review from a team as a code owner September 5, 2026 03:32
@RadiumGu
RadiumGu requested a review from dwu96 September 5, 2026 03:32
@github-actions github-actions Bot added the fork Pull request from a fork (external contributor) label Sep 5, 2026
@github-actions github-actions Bot added the readiness: action required A blocking check or review needs attention label Sep 5, 2026
@dwu96

dwu96 commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Problem / Motivation
  • ## Why it matters
  • ## What changed
  • ## Tests

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

1 similar comment
@iamwhatever

Copy link
Copy Markdown
Collaborator

👋 Hi! This PR's description is missing some required sections from our PR template. Workflow runs won't be auto-approved until the description is updated.

Missing sections:

  • ## Problem / Motivation
  • ## Why it matters
  • ## What changed
  • ## Tests

Please update your PR description to include these sections, then push or re-save the description. The workflows will be approved on the next cycle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants