Summary
edge-runtime bundle intermittently terminates with SIGBUS (Docker exit 135) on
a fresh cache while bundling a local CommonJS import. No Supabase project,
credentials, network imports, or third-party npm package is required.
The reduced control below failed 6/90 times across three GitHub-hosted x64
runners. A separate SDK consumer produced three native crash dumps, all pointing
to SQLite WAL shared-memory access during Deno cache initialization.
Environment
- Image:
public.ecr.aws/supabase/edge-runtime:v1.76.2@sha256:edd22bef4477b900d5c300e287ce9b18bff9b81a0291bee14ee0b7c7b71a2899
- Embedded Deno: 2.1.4.
- GitHub-hosted Ubuntu 24.04 x64; kernel
6.17.0-1022-azure.
- Observed on AMD EPYC 9V74 and 7763 hosts across the diagnostic runs.
- Docker records
OOMKilled: false; ample host RAM and disk were available.
- Enlarging
/dev/shm did not prevent the crash. A local ARM control did not
reproduce it; that is not proof the bug is x64-only.
Reduced reproduction
Save as repro.ts, then run node repro.ts with Node 24 and local Docker.
It uses fresh containers and deletes only its own temporary directory/containers.
Repeat on a native Linux x64 host; the failure is timing-sensitive.
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const image = 'public.ecr.aws/supabase/edge-runtime:v1.76.2@sha256:edd22bef4477b900d5c300e287ce9b18bff9b81a0291bee14ee0b7c7b71a2899'
const root = mkdtempSync(join(tmpdir(), 'edge-cache-repro-'))
let failures = 0
try {
mkdirSync(join(root, 'node_modules/control'), { recursive: true })
writeFileSync(join(root, 'package.json'), '{"private":true,"type":"module"}\n')
writeFileSync(join(root, 'deno.json'), '{"nodeModulesDir":"manual","lock":false}\n')
writeFileSync(join(root, 'node_modules/control/package.json'), '{"name":"control","main":"index.cjs"}\n')
writeFileSync(join(root, 'node_modules/control/index.cjs'), "exports.value = 'control'\n")
writeFileSync(join(root, 'index.ts'), "import { value } from './node_modules/control/index.cjs'\nDeno.serve(() => new Response(value))\n")
execFileSync('docker', ['pull', image], { stdio: 'inherit' })
for (let attempt = 1; attempt <= 30; attempt++) {
const id = execFileSync('docker', [
'create', '--workdir', '/work',
'--env', 'RUST_LOG=deno::cache::cache_db=trace',
image, 'bundle', '--entrypoint', '/work/index.ts',
'--output', '/work/function.eszip', '--timeout', '120',
], { encoding: 'utf8' }).trim()
try {
execFileSync('docker', ['cp', `${root}/.`, `${id}:/work`])
const result = spawnSync('docker', ['start', '--attach', id], {
encoding: 'utf8', timeout: 150_000,
})
console.log({ attempt, exitCode: result.status })
if (result.status !== 0) {
failures++
console.log(result.stdout, result.stderr)
console.log(execFileSync('docker', ['inspect', '--format', '{{json .State}}', id], { encoding: 'utf8' }))
}
} finally {
execFileSync('docker', ['rm', '--force', id])
}
}
} finally {
rmSync(root, { recursive: true, force: true })
}
console.log({ failures })
process.exitCode = failures === 0 ? 0 : 1
Trace and native evidence
All six reduced-control crashes log duplicate openings of both analysis caches,
then an initialization retry and a database is locked error before deletion:
DEBUG Opening cache /root/.cache/deno/dep_analysis_cache_v2...
DEBUG Opening cache /root/.cache/deno/node_analysis_cache_v2...
DEBUG Opening cache /root/.cache/deno/dep_analysis_cache_v2...
DEBUG Opening cache /root/.cache/deno/node_analysis_cache_v2...
Could not initialize cache database '.../node_analysis_cache_v2', retrying...
Error code 14: Unable to open the database file
Could not initialize cache database '.../node_analysis_cache_v2', deleting and retrying...
Error code 5: The database file is locked
Representative native stack from an uninstrumented SDK bundle crash:
Program terminated with signal SIGBUS, Bus error.
#0 walIndexAppend at sqlite3/sqlite3.c:66237
#1 walFrames
#2 sqlite3WalFrames
#3 pagerWalFrames
...
#14 rusqlite::raw_statement::RawStatement::step
#16 rusqlite::Connection::execute_batch
#17 deno::cache::cache_db::CacheDB::initialize_connection at deno/cache/cache_db.rs:237
#18 CacheDB::open_connection_and_init at deno/cache/cache_db.rs:270
#20 deno::cache::cache_db::open_connection at deno/cache/cache_db.rs:419
...
#31 CacheDB::spawn_eager_init_thread
The signal is SIGBUS / BUS_ADRERR; its address falls inside the mapped
dep_analysis_cache_v2-shm or node_analysis_cache_v2-shm file. Line 419 is the
retry after removing the database. A syscall trace from another run shows one
thread unlinking the DB while another still uses it, followed by both opening
and truncating the same -shm pathname. That traced run itself completed; the
three crashes were captured as bounded core dumps and analyzed offline.
Suspected mechanism
EmitterFactory::caches()
creates a fresh Caches on every call and eagerly initializes both databases.
module_graph_builder()
calls it and then calls module_info_cache(), which calls it again. Their mutexes
are independent but their file paths are identical.
open_connection
retries after creating the parent directory, then deletes the database after
another initialization error, including SQLITE_BUSY. Replacing an open SQLite
database lets different DB inodes share WAL/shm paths, an unsafe pattern also
documented in SQLite's guidance.
Sharing the factory's cache instance and not treating SQLITE_BUSY as a reason to
unlink an in-use database appear to be the appropriate fixes. These fixes have
not been implemented or tested here.
Counterfactuals, not a claimed upstream fix
Across three x64 runners, the same SDK graph/image passed 90/90 times with
--tmpfs /root/.cache/deno:size=268435456. This also pre-creates the directory, so
it does not distinguish filesystem behavior from initialization timing. Twelve
runs still logged busy errors; none logged deletion. Merely preheating a disk
cache also gave 90/90 passes, but 39 runs still deleted/recreated a database, so
that does not appear to be a safe solution.
The reduced test contains no application secrets and all failures were retained;
these were not retries used to turn a failing CI job green.
Summary
edge-runtime bundleintermittently terminates with SIGBUS (Docker exit 135) ona fresh cache while bundling a local CommonJS import. No Supabase project,
credentials, network imports, or third-party npm package is required.
The reduced control below failed 6/90 times across three GitHub-hosted x64
runners. A separate SDK consumer produced three native crash dumps, all pointing
to SQLite WAL shared-memory access during Deno cache initialization.
Environment
public.ecr.aws/supabase/edge-runtime:v1.76.2@sha256:edd22bef4477b900d5c300e287ce9b18bff9b81a0291bee14ee0b7c7b71a28996.17.0-1022-azure.OOMKilled: false; ample host RAM and disk were available./dev/shmdid not prevent the crash. A local ARM control did notreproduce it; that is not proof the bug is x64-only.
Reduced reproduction
Save as
repro.ts, then runnode repro.tswith Node 24 and local Docker.It uses fresh containers and deletes only its own temporary directory/containers.
Repeat on a native Linux x64 host; the failure is timing-sensitive.
Trace and native evidence
All six reduced-control crashes log duplicate openings of both analysis caches,
then an initialization retry and a
database is lockederror before deletion:Representative native stack from an uninstrumented SDK bundle crash:
The signal is
SIGBUS / BUS_ADRERR; its address falls inside the mappeddep_analysis_cache_v2-shmornode_analysis_cache_v2-shmfile. Line 419 is theretry after removing the database. A syscall trace from another run shows one
thread unlinking the DB while another still uses it, followed by both opening
and truncating the same
-shmpathname. That traced run itself completed; thethree crashes were captured as bounded core dumps and analyzed offline.
Suspected mechanism
EmitterFactory::caches()creates a fresh
Cacheson every call and eagerly initializes both databases.module_graph_builder()calls it and then calls
module_info_cache(), which calls it again. Their mutexesare independent but their file paths are identical.
open_connectionretries after creating the parent directory, then deletes the database after
another initialization error, including SQLITE_BUSY. Replacing an open SQLite
database lets different DB inodes share WAL/shm paths, an unsafe pattern also
documented in SQLite's guidance.
Sharing the factory's cache instance and not treating SQLITE_BUSY as a reason to
unlink an in-use database appear to be the appropriate fixes. These fixes have
not been implemented or tested here.
Counterfactuals, not a claimed upstream fix
Across three x64 runners, the same SDK graph/image passed 90/90 times with
--tmpfs /root/.cache/deno:size=268435456. This also pre-creates the directory, soit does not distinguish filesystem behavior from initialization timing. Twelve
runs still logged busy errors; none logged deletion. Merely preheating a disk
cache also gave 90/90 passes, but 39 runs still deleted/recreated a database, so
that does not appear to be a safe solution.
The reduced test contains no application secrets and all failures were retained;
these were not retries used to turn a failing CI job green.