Skip to content

0.1.3: deno_core 0.404 -> 0.408 (#37) - #38

Merged
yfedoseev merged 5 commits into
mainfrom
release/0.1.3
Jul 28, 2026
Merged

0.1.3: deno_core 0.404 -> 0.408 (#37)#38
yfedoseev merged 5 commits into
mainfrom
release/0.1.3

Conversation

@yfedoseev

Copy link
Copy Markdown
Owner

Draft. Scope for 0.1.3 is #37 — the deno_core 0.408 bump deferred from #35.

Why this is a draft

0.408 is expected to fail the ubuntu debug test jobs with signal: 6, SIGABRT during V8 isolate construction. That is the known symptom from #37, reproduced deliberately. This PR exists to get the reason, which was invisible last time.

The actual change of substance

cargo test now runs with --nocapture. That is not cosmetic: libtest captures each tests stdout/stderr and replays it only on failure, so when a test **aborts the process** the captured output dies with it. In #35 that left a bare signal: 6againstworkers::tests::create_workerandbasic_js_execution with no diagnosis, which is why the bump was reverted rather than fixed. Unbuffered output means the last line before an abort is the aborts own message — a V8 FATAL, or a panic that could not unwind out of a V8 callback.

Worth keeping regardless of the outcome here: any future abort-on-construction is otherwise undiagnosable from CI logs alone.

Ruled out so far

  • Not a stack overflow. Rust detects those via its SIGSEGV handler and then calls abort(), so SIGABRT does not exclude it — but the CI log contains no has overflowed its stack, and v8_recursion::test_thread_stack_is_at_least_16mb passes in the same debug run. .cargo/config.toml already sets RUST_MIN_STACK=67108864.
  • Not a new deno_core assertion. debug_assert counts in runtime/jsruntime.rs, runtime/mod.rs, runtime/bindings.rs and runtime/jsrealm.rs are identical between 0.404 and 0.408.
  • Not release-reachable. On 0.408 in release the full suite passes, all 11 warm-reuse tests pass, the leak A/B is unchanged (1,040,662 -> 468 B per reuse), and examples/canvas_fp_probe.rs still reports len=17502 fnv1a=5b1d42ee9bdc9713.

Transitive V8 moves 149.2.0 -> 149.4.0 with this bump — the other candidate, and one deno_core`s own source diff cannot show.

Next

  1. Read the abort reason from the ubuntu debug jobs on this run.
  2. If it is not conclusive, bisect 0.405 / 0.406 / 0.407.
  3. Fix or report upstream, then mark ready for review.

Do not merge while the debug jobs are red.

Version bump across Cargo.toml, browser_oxide_py and pyproject.toml (all three
are checked for consistency by the verify job), plus a CHANGELOG section
tracking the deferred deno_core bump.

Scope for this release is #37: deno_core 0.404 -> 0.408, which aborts during
V8 isolate construction in Linux debug builds.

Signed-off-by: Yury Fedoseev <yfedoseev@gmail.com>
Re-applies the bump that was reverted from 0.1.2, plus the CI change needed to
actually diagnose it.

The blocker in #35 was not the abort itself but that its reason was invisible.
libtest captures each test's stdout/stderr and replays it only on failure, so
when a test *aborts* the process the captured output dies with it -- the log
showed a bare `signal: 6, SIGABRT` against `workers::tests::create_worker` and
`basic_js_execution`, with no message. Adding `--nocapture` streams output
unbuffered, so the last thing printed before an abort is the abort's own
diagnostic (a V8 FATAL line, or a panic that could not unwind out of a V8
callback).

Ruled out so far:

  * Not a stack overflow. Rust detects those via its SIGSEGV handler and then
    calls abort(), so SIGABRT does not exclude it -- but the CI log contains no
    "has overflowed its stack", and `v8_recursion::test_thread_stack_is_at_
    least_16mb` passes in the same debug run. `.cargo/config.toml` already sets
    RUST_MIN_STACK=67108864.
  * Not a new deno_core assertion: debug_assert counts in runtime/jsruntime.rs,
    runtime/mod.rs, runtime/bindings.rs and runtime/jsrealm.rs are unchanged
    between 0.404 and 0.408.
  * Not release-mode-reachable: on 0.408 in release the full suite passes, all
    11 warm-reuse tests pass, the leak A/B is unchanged (1,040,662 -> 468 B per
    reuse) and examples/canvas_fp_probe.rs still reports
    len=17502 fnv1a=5b1d42ee9bdc9713.

Transitive V8 moves 149.2.0 -> 149.4.0 with this bump, which is the other
candidate and is not something deno_core's own source diff would show.

The `--nocapture` change is worth keeping regardless of #37: any future
abort-on-construction is otherwise undiagnosable from CI logs alone.

Signed-off-by: Yury Fedoseev <yfedoseev@gmail.com>
Root cause of the 0.408 SIGABRT, found by reading the abort message that
`--nocapture` finally let through:

  V8 posted a delayed task, but this isolate was created outside of a tokio
  runtime context and the delay cannot be honored. Enter a tokio runtime
  (e.g. via `tokio::runtime::Runtime::enter()`) before creating the `JsRuntime`.

deno_core 0.408 captures `tokio::runtime::Handle::try_current()` when it
registers an isolate (runtime/jsruntime.rs) and spawns V8's *delayed* foreground
tasks -- GC memory-reducer work and friends -- on that handle. With no handle,
runtime/setup.rs::spawn_delayed_task prints that line and calls
std::process::abort(). Upstream aborts rather than panics on purpose: V8 calls
it from C++ frames Rust cannot unwind through, so a panic message would be
swallowed anyway.

Two things I had wrong earlier, both now disproven:

  * It is NOT debug-only. The abort is unconditional. Release builds passed only
    while V8 happened not to post a delayed task inside the window under test.
    Release was exposed too.
  * It is NOT Linux-only. Reproduced on Windows in a standalone crate; CI shows
    ubuntu, macos AND windows failing on #38.

Proven with a minimal deno_core-only crate (no skia, so debug links on Windows),
constructing an isolate and churning the heap to provoke GC scheduling:

  no tokio entered   handle_at_ctor=false  -> abort 0xC0000409
  runtime entered    handle_at_ctor=true   -> clean exit 0

Fix: `ensure_tokio_context()` enters a process-lifetime fallback runtime for the
duration of construction when the caller has none, applied to both the page and
worker realms. The fallback must outlive the isolate -- the captured handle would
dangle otherwise -- hence a OnceLock rather than a temporary. Keeping this in the
library rather than pushing it onto callers is deliberate:
BrowserJsRuntime::new/with_profile/with_options are synchronous public API,
callable from a plain fn main or a #[test], and the failure mode is a process
abort, not an error.

Also, per review: V8 heap limits are now environment-tunable instead of
hard-coded, since the right ceiling is a property of the deployment and the
fixed 4 GB silently over-committed small containers.

  BROWSER_OXIDE_HEAP_MAX_MB      default 4096 (4 GB)
  BROWSER_OXIDE_HEAP_INITIAL_MB  default 1024 (1 GB)

Unparseable/zero values fall back to defaults with a warning rather than
failing; an initial above the ceiling is clamped, because V8 rejects that
pairing. Documented in docs/CONFIGURATION.md, including that the limits are
per-isolate so a PagePool of N commits up to N x the ceiling, plus a note on the
new tokio-context requirement for embedders.

tests/runtime_env.rs covers both. It is one #[test] fn on purpose: each check
needs its own isolate, V8 requires reverse-order drops, and splitting them
across fns aborted the binary. It must stay a plain #[test] -- a #[tokio::test]
would enter a runtime and silently stop testing the regression.

Signed-off-by: Yury Fedoseev <yfedoseev@gmail.com>
@yfedoseev
yfedoseev marked this pull request as ready for review July 28, 2026 01:15
Replaces the placeholder Planned entry with what actually shipped: the #37
tokio-context fix, env-tunable heap limits, deno_core 0.408, the --nocapture CI
change, and the verification results (canvas fingerprint unchanged across the V8
bump, zero real-site regressions, full matrix green).

Signed-off-by: Yury Fedoseev <yfedoseev@gmail.com>
…bsence

`Release MCP binary` failed for v0.1.2 at "Generate release notes from CHANGELOG":

  No '> subtitle' blockquote found under '## [0.1.2]' in CHANGELOG.md.

`extract-release-notes.sh` requires a `> ...` blockquote immediately under the
version header (it becomes the GitHub Release title) plus at least one `### `
heading. The 0.1.2 and 0.1.3 sections had neither subtitle. Added both, verified
with the script directly.

The more important fix is the gate. Changelog Gate only grepped for the section
header:

  grep -qE "^##+ \[?${V//./\\.}\]?" CHANGELOG.md

so it went green while the release was guaranteed to fail. v0.1.2 passed the
gate, was tagged, published to crates.io, and only then did the MCP release die
on a missing subtitle. A post-tag failure is the expensive kind: the tag and the
crates.io publish are already public and cannot be withdrawn, only superseded.

The gate now runs the same validator the release workflows run
(`extract-release-notes.sh --check`) instead of a parallel, weaker check. One
validator, one source of truth — a header-only check could always pass while the
real extraction failed.

Note v0.1.2's GitHub Release is still absent. Its four binaries built fine; only
the release-creation step failed, and re-running cannot help because the v0.1.2
tag points at a commit without the subtitle. It needs `gh release create v0.1.2`
by hand if we want it, which is a separate decision from unblocking 0.1.3.

Signed-off-by: Yury Fedoseev <yfedoseev@gmail.com>
@yfedoseev
yfedoseev merged commit 9cd4af3 into main Jul 28, 2026
28 checks passed
@yfedoseev
yfedoseev deleted the release/0.1.3 branch July 28, 2026 03:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant