Skip to content

BLOCKED: Monty 0.0.21 upgrade silently disables PythonLimits::max_memory - #2298

Closed
chaliy wants to merge 1 commit into
mainfrom
claude/pensive-hypatia-6299iz
Closed

BLOCKED: Monty 0.0.21 upgrade silently disables PythonLimits::max_memory#2298
chaliy wants to merge 1 commit into
mainfrom
claude/pensive-hypatia-6299iz

Conversation

@chaliy

@chaliy chaliy commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Do not merge. Draft on purpose. This branch carries a complete, compiling Monty 0.0.21 upgrade, and in the course of verifying it I found an upstream regression that makes the upgrade unsafe to land as-is. Keeping it open as the record of the blocker.

What I found

Upgrading to Monty 0.0.21 silently turns PythonLimits::max_memory into a no-op for every embedder of the bashkit library. Memory bombs that are contained today run to completion.

Evidence — nested_list_bomb allocates 1,000,000 list items under a 2 MB limit:

assertion `left != right` failed: Creating 1M list items should hit limits
  left: 0
 right: 0

Exit code 0. Five tests fail on exactly this axis:

python_security_tests::whitebox_resource_limits::nested_list_bomb
python_security_tests::whitebox_resource_limits::successive_allocations_accumulate
python_security_tests::whitebox_resource_limits::tight_memory_blocks_many_small_objects
threat_model_tests::python_security_regressions::threat_python_pow_exhaustion
execution_budget_tests::python_vfs_round_trips_consume_shared_work_budget   (added here)

Why it happens

0.0.21 reworked resource tracking in two ways that compound:

  1. The ResourceTracker trait became a concrete struct, and LimitedTracker was deleted. on_grow / on_free — the per-allocation VM heap-growth hooks — no longer exist at all. Bashkit's BudgetTracker used exactly those to meter the VM.
  2. Memory accounting moved to the global allocator. max_memory is now evaluated as:
fn probe_memory() -> usize {
    LIVE_MEMORY.load(Relaxed).saturating_sub(BASELINE_MEMORY.load(Relaxed))
}

Those statics are only populated by monty-alloc installed as the process #[global_allocator] and armed via set_limit. Unarmed, the defaults are LIVE_MEMORY = 0 and BASELINE_MEMORY = usize::MAX, so saturating_sub pins used at 0 — and used > limit is never true. The limit is not merely loose; it can never fire.

Verified nothing arms it, and that monty-alloc is not even in the dependency tree:

$ grep -rn "global_allocator" crates/ --include=*.rs        # (no matches)
$ grep -rn "monty-alloc\|monty_alloc" Cargo.lock crates/    # (no matches)
$ grep -rn "LIVE_MEMORY\|BASELINE_MEMORY" crates/           # (no matches)

Why I can't just fix it here

Enforcement now requires being the process's global allocator. bashkit is a library: a #[global_allocator] in a library is imposed on every downstream binary and collides with any allocator the embedder chose. There is no host-side hook left to reimplement the metering against — on_grow/on_free are gone, and ResourceTracker::check_allocation is only useful to whoever performs the allocation, which is the VM, not us.

So this is genuinely blocked upstream, not a porting problem. Options, none of which I want to pick unilaterally:

  1. Stay on 0.0.19 (recommended for now) — keep max_memory enforced, revisit when upstream restores a host hook.
  2. Ask upstream to restore heap-growth callbacks or expose a host-pluggable tracker, then upgrade.
  3. Accept the loss, document max_memory as unenforced-in-library, and arm monty-alloc only in bashkit-cli — this narrows the guarantee for library embedders and needs an explicit, deliberate decision plus a threat-model change.

What is otherwise ready on this branch

If the memory question gets resolved, the rest of the port is done and compiles clean:

  • Both monty and monty-types bumped together — dependabot's split (chore(deps): bump monty-types from 0.0.19 to 0.0.21 #2296 / chore(deps): bump monty from 0.0.19 to 0.0.21 #2297) can never pass alone, because one version of each leaves two monty-types in the graph and its result types stop unifying (monty_types::results::ExtFunctionResult vs monty_types::ExtFunctionResult). That single cause produced ~22 red checks on each of those PRs.
  • ResourceLimits::new()default(); max_recursion_depth now takes usize, not Option<usize>.
  • The [patch.crates-io] jiter git pin is dropped — 0.0.21 tracks the published jiter 0.16.0, already on pyo3 0.29, which is precisely the removal condition that pin documented for itself. The workspace builds from published crates again.
  • Supply-chain exemptions updated; cargo vet reports only the 3 expected new entries (jiter 0.16.0, ruff_python_codegen 0.0.3, ruff_python_literal 0.0.3).
  • cargo audit on the 0.0.21 graph is clean — no vulnerabilities, only the 2 already-suppressed unmaintained warnings.

Time/recursion containment does survive the port (bash_timeout_clamps_python_duration passes; max_duration is clamped to the caller's remaining deadline before the VM starts). It is specifically and only max_memory that breaks.

Dependabot split this upgrade into two PRs (#2296 monty-types, #2297
monty). Either one alone leaves two `monty-types` versions in the graph,
so its result types stop unifying and ~22 CI checks go red. Bumping both
together reduces that to two real API breaks:

  * `ResourceLimits::new()` -> `default()`, and `max_recursion_depth`
    now takes `usize` rather than `Option<usize>`.
  * `ResourceTracker` changed from a host-implementable trait into a
    concrete struct and `LimitedTracker` was removed, so the
    `BudgetTracker` wrapper that charged the shared `ExecutionBudget`
    from inside the VM's own allocation/statement checkpoints no longer
    has anything to hook into, and 0.0.21 ships no replacement hook.

Python now drives the shared budget the same way the TypeScript builtin
always has: an up-front charge (input bytes, code size, and a reserve
proportional to the VM memory ceiling) plus per-round-trip charging in
the start/resume loop. Containment is unchanged, because it was never
the tracker's job — `max_duration` is clamped to the caller's remaining
execution deadline before the VM starts, and Monty's own tracker still
enforces that duration plus `max_memory` and the recursion ceiling
synchronously. Only the finer-grained work-unit accounting for a script
that never re-enters the host loop is lost; the up-front reserve
approximates it.

monty 0.0.21 tracks the published jiter 0.16.0, which is already on
pyo3 0.29 — the exact condition the `[patch.crates-io]` jiter git pin
documented for its own removal — so that pin is dropped.

Adds a regression test for the charging path that now carries the load,
so removing host-loop charging fails loudly rather than silently.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit 74bf892 Commit Preview URL

Branch Preview URL
Aug 14 2026, 09:22 AM

@chaliy chaliy changed the title feat(python): upgrade Monty to 0.0.21 and drop the jiter patch BLOCKED: Monty 0.0.21 upgrade silently disables PythonLimits::max_memory Aug 14, 2026
@chaliy
chaliy marked this pull request as draft August 14, 2026 09:29
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown

Closing this rather than leaving it open, because it is not actionable until the upstream memory-enforcement question is settled, and I need the branch for other work.

Nothing is lost. The full analysis stays in the description above, and the blocker is recorded on the two dependabot PRs it affects — #2296 and #2297 — which remain open as the tracking items for this upgrade.

The port itself is preserved at commit 74bf8927 and can be recovered at any time with:

git fetch origin refs/pull/2298/head

Recap of why this must not merge: Monty 0.0.21 silently turns PythonLimits::max_memory into a no-op for every library embedder, because memory accounting moved to counters that only a process-level #[global_allocator] (monty-alloc) populates, and the per-allocation on_grow/on_free hooks were removed. A 1M-item list allocation under a 2 MB cap exits 0 instead of being killed.


Generated by Claude Code

@chaliy chaliy closed this Aug 14, 2026
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