Sandboxed Chromium fails with EACCES on Amsterdam V2
speklapje23
FREEOP

2 days ago

Hi Railway team,

A synthetic browser startup preflight fails on Railway’s Amsterdam V2 runtime. We run Linux as non-root UID 1000, with Playwright 1.63.0 and Chrome for Testing 153.0.8010.12, Chromium headless shell build 1243. Chromium’s sandbox is enabled.

The browser exits with SIGTRAP and: FATAL:sandbox/linux/services/credentials.cc:131] Check failed: . : Permission denied (13)

In this exact Chromium version, line 131 checks errors from CLONE_NEWUSER operations. We haven’t established whether clone or unshare failed, or which host policy denied it.

Can Railway confirm the restriction and whether a supported, narrowly scoped configuration allows Chromium’s sandbox to initialize as UID 1000? We need to retain the browser sandbox without running the browser as root, privileged mode, or disabling sandbox protections. If unsupported on this runtime, please confirm.

$10 Bounty

1 Replies

Railway
BOT

2 days ago

This thread has been opened as a bounty so the community can help solve it.

Status changed to Open Railway • 2 days ago


hdnexus
PRO

2 days ago

Short answer: no, there's no configuration available to you that restores Chromium's namespace sandbox on a managed container runtime, and running as root wouldn't change it either. Here's how to prove that yourself in about thirty seconds, and what the workable alternatives are.

Why UID 1000 is a red herring

credentials.cc:131 is in Chromium's user-namespace capability check, it fails when clone(CLONE_NEWUSER) / unshare(CLONE_NEWUSER) returns an error, and the zygote has no fallback path when it can't create a user namespace, which is the SIGTRAP. The denial is on the syscall. If the host policy blocks unprivileged userns creation, root inside the container hits the same wall, because the restriction is applied above your container, not by your container's user model. So "run it as root" isn't a workaround you're missing, it's the same failure.

Confirming which layer denied it

Run this inside the container, no Chromium needed:

cat /proc/sys/user/max_user_namespaces # 0 = disabled outright

cat /proc/sys/kernel/unprivileged_userns_clone # may not exist on this kernel

unshare -U true; echo "unshare -U -> $?"

unshare -U --map-root-user true; echo "mapped -> $?"

grep Seccomp /proc/self/status # 2 = filter mode

capsh --print 2>/dev/null | head -5 # what you actually hold

Read it like this: max_user_namespaces at 0 means the sysctl disabled it and that's the whole story. Nonzero but unshare -U still failing means a seccomp filter or LSM is denying the syscall, Seccomp: 2 in that output tells you a filter is active. ENOSPC would mean you'd exhausted the namespace limit, which is the one case that's actually about your workload; you're not seeing that. Either way the answer to "which host policy" is answerable without waiting on staff.

One caveat on your own analysis: I wouldn't read EACCES as a precise fingerprint for a specific mechanism. It points away from an exhausted limit and toward a policy denial, but the commands above are what actually distinguishes them.

What to do instead

--no-sandbox, with the container as the boundary. Your service is already isolated at the container level, so the trust boundary moves from the renderer to the service. If the browser only loads content you control, this is the standard answer on managed runtimes and the security story is defensible. If it renders untrusted URLs, isolate that service properly: its own project, no shared credentials, no internal network reach to the rest of your stack, so a renderer compromise gets nothing worth having. Don't just add the flag and move on if untrusted content is in scope.

Move the browser, not the app. chromium.connectOverCDP() against Browserless, Browserbase, or your own VM where you control the kernel flags. Playwright doesn't care that the browser is remote, your app stays on Railway, and this is the only option that genuinely keeps the sandbox you asked to retain. If the sandbox is a hard requirement rather than a preference, this is your answer.

Stop launching the browser in the preflight. If the check only verifies the binary is installed and the right version, assert on chromium.executablePath() and the version string. A full sandboxed launch in a preflight tests the platform's namespace policy, not your application, which is how you ended up debugging this instead of shipping.

If you want a staff answer, narrow the question

"Is this supported" invites a no. Better: is unprivileged user-namespace creation blocked by the seccomp profile on Amsterdam V2, and does that policy differ from other regions? That's specific enough to get a real reply. And if this same image worked on an older Railway runtime, say so, that reframes it from expected platform policy to a regression, which is a different and more urgent thread.


Welcome!

Sign in to your Railway account to join the conversation.

Loading...