Repository navigation
Incorrect built-in Math constants in edge-runtime v1.74.3; Math.LN10 also wrong in v1.74.2 (linux/amd64) #723
Description
Bug report
Several native JavaScript Math.* constants return incorrect values inside supabase/edge-runtime:v1.74.3.
The issue reproduces with a single dependency-free TypeScript file. The equivalent probe returns the expected values in the official Deno 2.1.4 container.
Math.LN10 was also independently reproduced as incorrect in edge-runtime v1.74.2, including after deleting and cleanly pulling the same image digest.
Environment
- Docker Desktop Server: 28.3.2
- Docker operating system: Linux
- Docker host architecture: x86_64
- Kernel: 6.6.87.2-microsoft-standard-WSL2
- Supabase CLI: 2.109.1
- Control image:
denoland/deno:2.1.4
Deno control:
- Deno 2.1.4
- V8 13.0.245.12-rusty
- Target
x86_64-unknown-linux-gnu
Edge Runtime v1.74.3:
- Image:
public.ecr.aws/supabase/edge-runtime:v1.74.3 - Image ID:
sha256:c52405002a890ca9fcf77978671c57f3a988e03174afb277f84ac65bc917013c - RepoDigest:
public.ecr.aws/supabase/edge-runtime@sha256:c52405002a890ca9fcf77978671c57f3a988e03174afb277f84ac65bc917013c - Architecture:
amd64 - OS:
linux - Created:
2026-07-31T04:09:03.225642475Z - Runtime reports:
edge-runtime 0.1.0deno 2.1.4 (release, x86_64-unknown-linux-gnu)
Additional v1.74.2 evidence:
- Image:
public.ecr.aws/supabase/edge-runtime:v1.74.2 - Digest:
sha256:a82676277615aee03c4f288cbbbf68dedb5ba8693073e567ab8dbfdd11ba5d45 - Architecture:
amd64/linux
No application or Supabase environment variables were passed to the isolated v1.74.3 reproducer.
Minimal reproduction
Create <temp-dir>/index.ts:
function bits(value: number): string {
const buffer = new ArrayBuffer(8);
new DataView(buffer).setFloat64(0, value, false);
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
const constants = {
E: Math.E,
LN2: Math.LN2,
LN10: Math.LN10,
LOG2E: Math.LOG2E,
LOG10E: Math.LOG10E,
PI: Math.PI,
SQRT1_2: Math.SQRT1_2,
SQRT2: Math.SQRT2,
};
Deno.serve(() =>
Response.json(
Object.fromEntries(
Object.entries(constants).map(([name, value]) => [
name,
{
value: value.toPrecision(17),
bits: bits(value),
},
]),
),
),
);Replace <temp-dir> below with its absolute host path.
docker pull public.ecr.aws/supabase/edge-runtime:v1.74.3
docker run -d --rm --name edge-math-runtime-1743-repro --mount type=bind,source=<temp-dir>,target=/probe,readonly -p 127.0.0.1::9000 public.ecr.aws/supabase/edge-runtime:v1.74.3 start --main-service=/probe --port=9000 --policy=per_worker
docker port edge-math-runtime-1743-repro 9000/tcp
curl http://127.0.0.1:<edge-host-port>/
docker stop edge-math-runtime-1743-reproNo dependency installation is necessary.
Expected behavior
The built-in constants should match standard JavaScript/Deno values and IEEE-754 representations:
| Constant | Expected value | Expected bits |
|---|---|---|
| Math.E | 2.7182818284590451 | 4005bf0a8b145769 |
| Math.LN2 | 0.69314718055994529 | 3fe62e42fefa39ef |
| Math.LN10 | 2.3025850929940459 | 40026bb1bbb55516 |
| Math.LOG2E | 1.4426950408889634 | 3ff71547652b82fe |
| Math.LOG10E | 0.43429448190325182 | 3fdbcb7b1526e50e |
| Math.PI | 3.1415926535897931 | 400921fb54442d18 |
| Math.SQRT1_2 | 0.70710678118654757 | 3fe6a09e667f3bcd |
| Math.SQRT2 | 1.4142135623730951 | 3ff6a09e667f3bcd |
Actual behavior
Edge Runtime v1.74.3 returns:
| Constant | Actual value | Actual bits |
|---|---|---|
| Math.E | 2.0000000000000000 | 4000000000000000 |
| Math.LN2 | 0.0000000000000000 | 0000000000000000 |
| Math.LN10 | 2.0000000000000000 | 4000000000000000 |
| Math.LOG2E | 1.0000000000000000 | 3ff0000000000000 |
| Math.LOG10E | 0.0000000000000000 | 0000000000000000 |
| Math.PI | 3.1415926535897931 | 400921fb54442d18 |
| Math.SQRT1_2 | 0.70710678118654757 | 3fe6a09e667f3bcd |
| Math.SQRT2 | 1.4142135623730951 | 3ff6a09e667f3bcd |
Control
Run the same index.ts with Deno 2.1.4:
docker run -d --rm --name edge-math-deno-control --mount type=bind,source=<temp-dir>,target=/probe,readonly -p 127.0.0.1::8000 denoland/deno:2.1.4 deno run --allow-net /probe/index.ts
docker port edge-math-deno-control 8000/tcp
curl http://127.0.0.1:<deno-host-port>/
docker stop edge-math-deno-controlThe Deno control returns every expected value and bit pattern.
Additional evidence
With Edge Runtime v1.74.2:
Math.LN10returned2.0000000000000000- Actual bits:
4000000000000000 - Expected bits:
40026bb1bbb55516
The v1.74.2 image was deleted and cleanly pulled again. The pulled image had the same expected digest and reproduced the same incorrect result.
This refutes a simple local image/cache corruption explanation.
Impact
Code performing mathematical operations may silently receive incorrect built-in constants.
Libraries that validate or use these constants can fail during module initialization or produce incorrect numerical results.
As one observed downstream example, decimal.js-light validates Math.LN10 during initialization and throws when the value does not match. The library is not required for this reproduction.
Notes
- The reproducer contains only native JavaScript/TypeScript.
- No external dependency is imported.
- No database, secrets, application environment, or application code is involved.
- The exact internal cause has not been established.
- V8 snapshot corruption, serialization, Docker Desktop/WSL2 behavior, Windows-host specificity, or a
deno_core/V8 issue remain hypotheses only. - Reproduction on a native Linux host has not yet been tested.
Activity
raf-master commentedon Aug 27, 2026
Additional reproduction: GitHub Actions Ubuntu runner
I reproduced the same issue on an independent GitHub-hosted Ubuntu runner, outside my original Windows / WSL2 / Docker Desktop environment.
Public reproducer repository:
https://github.com/raf-master/edge-runtime-math-linux-control
GitHub Actions run:
https://github.com/raf-master/edge-runtime-math-linux-control/actions/runs/33109361743
Environment:
6.17.0-1022-azurex86_6428.0.4linux/amd64Edge Runtime image:
public.ecr.aws/supabase/edge-runtime:v1.74.3RepoDigest:
sha256:c52405002a890ca9fcf77978671c57f3a988e03174afb277f84ac65bc917013cControl:
denoland/deno:2.1.4The Deno 2.1.4 control returned all 8 expected values and IEEE-754 bit patterns.
The Edge Runtime returned exactly the same incorrect values previously observed:
The reproducer classified the result as:
CASE_LINUX_FAIL_SAMEThis means the failure is not specific to my original Windows/WSL2/Docker Desktop host. It reproduces on an independent Ubuntu Linux VM using Docker Engine.
This still does not establish the internal root cause, and I have not tested bare-metal Linux. V8 snapshot/serialization/deno_core remain hypotheses only.
da1suk8 commentedon Aug 29, 2026
I can add three data points that narrow this down considerably.
TL;DR: the corruption is specific to the linux/amd64 image, the five broken constants are exactly the five that V8 computes at heap-genesis time (vs. the three installed from literals / hardware
sqrt), and in edge-runtime those genesis values are frozen into the binary at build time by the startup snapshot. So this looks like a corrupted build-time snapshot in the amd64 release lane, not a runtime or user-environment issue.1. The linux/arm64 variant of the same tag is clean
I ran the unmodified reproducer from the OP against both platform variants of
public.ecr.aws/supabase/edge-runtime:v1.74.3(digestsha256:c52405002a890ca9fcf77978671c57f3a988e03174afb277f84ac65bc917013c, matching the digest cited above) on the same machine (Docker Desktop, Apple Silicon):4000000000000000,0000000000000000,4000000000000000,3ff0000000000000,0000000000000000); PI/SQRT1_2/SQRT2 intactThe emulated amd64 run reproduces bit-for-bit what was already measured on native x86_64 (including the GitHub-hosted runner repro above), so emulation isn't a factor; the arm64-native result is the new information.
Additionally, building current
mainfrom source natively (macOS arm64, debug) yields all-correct values — the source itself looks fine.2. The broken/intact split matches V8's genesis code exactly
Bit-level, each broken value is the true value truncated toward zero to an integer, as a float64: e→2.0, ln2→0.0, ln10→2.0, log2(e)→1.0, log10(e)→0.0. Exponents ≥ 1 survive as the truncated integer, values < 1 collapse to +0 — i.e. a clean float→integer→float roundtrip of the computed result, not a bit-masking pattern.
V8 13.0.245.12 installs the Math constants like this (bootstrapper.cc#3679):
Broken set == the
base::ieee754::*computations; intact set == the literal + hardwaresqrt. That split is hard to explain by anything at request time, and easy to explain if the genesis-time computation (or its serialized result) was corrupted once.3. Genesis runs on the release builder, and its output ships in the binary
edge-runtime bakes its own V8 startup snapshot at build time (
crates/base/build.rs→deno_core::snapshot::create_snapshot), and workers boot from that snapshot. SoMath.*in production reflects whatever the machine that built the release computed. This fits all the symptoms: stable across clean pulls of the same digest (it's inside the binary), differing between v1.74.2 (onlyLN10) and v1.74.3 (five constants), and independent of the user's environment.As for why only amd64: in
.github/workflows/release.yml, the amd64 lane (publish_x86) builds with BuildKit GHA cache (cache-from/cache-to: type=gha) plus the Dockerfile's persistent cargo cache mounts (--mount=type=cache,target=…/target), while the arm64 lane (publish_arm) setsno-cache: true. Since cargo won't re-runbuild.rswhile its outputs look fresh, a once-corrupted snapshot artifact in the cached target dir can keep being linked into subsequent releases — which would also explain the difference in degree between v1.74.2 and v1.74.3. What corrupted the ieee754 results in the first place I can't tell from outside; the caching explains the persistence and the arch asymmetry.Suggested next steps
crates/base/build.rs: aftercreate_snapshot, boot a runtime from the fresh snapshot and assert the eightMathconstants, so a corrupted snapshot can never ship again regardless of root cause.fix(build): verify Math constants in runtime snapshot #726
no-cache: trueand compare the probe output against the current image.da1suk8 commentedon Sep 29, 2026
Root cause: the corruption lives in the default V8 startup snapshot embedded in the prebuilt x86_64 Linux
librusty_v8published by supabase/rusty_v8. edge-runtime never computes these constants; it inherits them.Why
build.rscreates its snapshot withstartup_snapshot: None. V8 13.0.245.12 then starts theSnapshotCreatorfrom the library's embedded default blob (https://github.com/v8/v8/blob/13.0.245.12/src/snapshot/snapshot.cc#L898) and the context is deserialized, not built by genesis. Genesis, whereMath.Eis computed, only runs inside mksnapshot; forcing it elsewhere fails withCheck failed: Heap setup supported only in mksnapshot. So every edge-runtime snapshot, isolate and image carries whatever mksnapshot produced when the V8 library itself was built.This retires my cache theory from August: the approved CI run for #726 shows the values are already wrong in a snapshot created fresh in that run.
Evidence
I extracted the generated
snapshot.ofrom each published archive and searched it for the IEEE-754 bytes of the eight constants:The five absent values are exactly the ones V8 computes at genesis and boxes via
Factory::NewNumber; absent double bytes plus the observed integers means they were stored as Smis (2, 0, 2, 1, 0).The code is not the difference:
ieee754.o,bootstrapper.o,factory.o, the serializer objects and the mksnapshot-generatedembedded.oare identical instruction for instruction between the fork's and upstream's x86_64 archives. Only the heap snapshot's numbers differ, so it is the environment that particular mksnapshot ran in. The fork has no CI build runs on either release date and its devcontainer isFROM --platform=linux/amd64; built on an Apple Silicon host, that means mksnapshot ran under x86 emulation. I could not reproduce that step, so this last part is inference.Affected range
Every linux/amd64 build since #549 switched to the fork binaries (2025-06-18). The first stable tag is v1.69.0; v1.68.x is unaffected. Note the v8 crate downloads
{RUSTY_V8_MIRROR}/v{CARGO_PKG_VERSION}/…, i.e. the assets of the fork's v130.0.7 release, whatever the git tag.Proposed fix
librusty_v8from the fork on native x86_64 Linux (the fork's ci.yml already builds withV8_FROM_SOURCE=trueon ubuntu-22.04) and publish. Upstream's archive is not a drop-in: the fork's V8 adds the thin-locker API used bybinding.cc.How to reproduce the check (no build needed)
scan.py(parses the GNU ar archive, takes the largest member namedsnapshot.o, i.e. the generated one definingDefaultSnapshotBlob):Expected:
up.aallTrue;fork.aFalsefor E, LN2, LN10, LOG2E, LOG10E. For the code comparison, write any member (e.g.ieee754.o) out with the same parser and diffllvm-objdump -d --no-show-raw-insn --demangleoutput after stripping addresses; it is empty for every object named above.cc @satyarohith
da1suk8 commentedon Sep 29, 2026
Opened a PR on supabase/rusty_v8 that adds this check to CI so a snapshot like this cannot be published again: supabase/rusty_v8#2
da1suk8 commentedon Oct 1, 2026
Verified on the published linux/amd64 images with the probe from this issue: v1.77.1 still returns the truncated values, v1.77.2 returns all eight constants bit-exact.
Correction to my earlier note: the original build was a cross-compile on an Intel Mac Pro, not emulation on Apple Silicon.
da1suk8 commentedon Oct 1, 2026
Status from my side: the fixed
librusty_v8shipped in v1.77.2 (verified above), and the build-time guard is merged in #726. The remaining piece is the upstream check in supabase/rusty_v8#2, which is independent of this issue.I think this can be closed; leaving that to the maintainers.
nyannyacha commentedon Oct 1, 2026
Closing this as the issue was resolved in latest release.