Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ jobs:
cargo +nightly-2026-10-30 fuzz build --features cpython cpython_http_fuzz
cargo +nightly-2026-10-30 fuzz build --features python python_fuzz
cargo +nightly-2026-10-30 fuzz build --features python,cpython python_diff_fuzz
cargo +nightly-2026-10-30 fuzz build --features typescript ts_fuzz

# Gate job for branch protection — name must stay "Check"
check:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
strategy:
fail-fast: false
matrix:
target: [parser_fuzz, lexer_fuzz, analyze_fuzz, arithmetic_fuzz, glob_fuzz, cpython_fuzz, cpython_http_fuzz, python_fuzz, python_diff_fuzz]
target: [parser_fuzz, lexer_fuzz, analyze_fuzz, arithmetic_fuzz, glob_fuzz, cpython_fuzz, cpython_http_fuzz, python_fuzz, python_diff_fuzz, ts_fuzz]

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -76,6 +76,7 @@ jobs:
python_diff_fuzz) features=(--features python,cpython) ;;
cpython_*) features=(--features cpython) ;;
python_*) features=(--features python) ;;
ts_fuzz) features=(--features typescript) ;;
esac
cargo +nightly-2026-10-30 fuzz run "${features[@]}" ${{ matrix.target }} -- \
-max_total_time="$DURATION_INPUT" \
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,19 @@ jobs:
cargo +nightly-2026-10-30 test -Z build-std --target x86_64-unknown-linux-gnu \
-p bashkit --features python --lib --test integration -- --test-threads=1 python

# The vendored ZapCode VM (crates/bashkit/src/zapcode_vendor) is our
# code too, and ts runs under the same metered allocator. The filters
# pick up the unit, threat-model, security and tenant stress tests (a
# bare `ts` would match every `tests` module path).
- name: Run ts (ZapCode) tests with AddressSanitizer
env:
RUSTFLAGS: "-Z sanitizer=address"
ASAN_OPTIONS: "detect_stack_use_after_return=1:detect_leaks=0"
run: |
cargo +nightly-2026-10-30 test -Z build-std --target x86_64-unknown-linux-gnu \
-p bashkit --features typescript --lib --test integration -- --test-threads=1 \
typescript threat_ts ts_tenant_stress zapcode

python-stress-soak:
name: Python multi-tenant stress soak
# PR CI runs python_tenant_stress_tests at 3 rounds. The soak repeats
Expand All @@ -220,3 +233,29 @@ jobs:
run: |
cargo test --release -p bashkit --features python --test integration \
-- python_tenant_stress

ts-stress-soak:
name: TypeScript multi-tenant stress soak
# PR CI runs ts_tenant_stress_tests at 3 rounds. The soak repeats them in
# release mode so rare cross-tenant leaks (meter left on a worker thread,
# charge not released after an abandoned call) show up.
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@d86e0fa8f147d6539442c26a61ebba8becc19489 # 1.96.0

- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2
with:
cache-on-failure: true

- name: Run stress soak
env:
BASHKIT_STRESS_ROUNDS: "300"
run: |
cargo test --release -p bashkit --features typescript --test integration \
-- ts_tenant_stress
2 changes: 1 addition & 1 deletion crates/bashkit-capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::runtime::{Builder, Runtime};

// Monty python3 charges each VM for its own allocations through this
// Monty python3 and ZapCode ts charge each VM for its own allocations through this
// allocator (see bashkit::MeteredAllocator). In a cdylib it only governs this
// library's Rust allocations, never the host runtime's allocator.
#[global_allocator]
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex as StdMutex};
use tokio::sync::{Mutex, OwnedSemaphorePermit, Semaphore};

// Monty python3 charges each VM for its own allocations through this
// Monty python3 and ZapCode ts charge each VM for its own allocations through this
// allocator (see bashkit::MeteredAllocator). In a cdylib it only governs this
// library's Rust allocations, never the host runtime's allocator.
#[global_allocator]
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use bashkit::{
};
use bashkit_scripted_tool::{ScriptedTool as RustScriptedTool, ToolArgs, ToolDef};

// Monty python3 charges each VM for its own allocations through this
// Monty python3 and ZapCode ts charge each VM for its own allocations through this
// allocator (see bashkit::MeteredAllocator). In a cdylib it only governs this
// library's Rust allocations, never the host runtime's allocator.
#[global_allocator]
Expand Down
2 changes: 1 addition & 1 deletion crates/bashkit-scripted-tool/tests/tool_registry_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::sync::{
};
use std::time::Duration;

// Monty python3 charges each VM for its own allocations through this
// Monty python3 and ZapCode ts charge each VM for its own allocations through this
// allocator (see bashkit::MeteredAllocator). In a cdylib it only governs this
// library's Rust allocations, never the host runtime's allocator.
#[cfg(feature = "python")]
Expand Down
4 changes: 2 additions & 2 deletions crates/bashkit-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ use serde::Serialize;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{JsFuture, future_to_promise};

// Monty python3 charges each VM for its own allocations through this
// Monty python3 and ZapCode ts charge each VM for its own allocations through this
// allocator (see bashkit::MeteredAllocator). In a cdylib it only governs this
// library's Rust allocations, never the host runtime's allocator.
#[cfg(feature = "python")]
#[cfg(any(feature = "python", feature = "typescript"))]
#[global_allocator]
static METERED_ALLOC: bashkit::MeteredAllocator = bashkit::MeteredAllocator;

Expand Down
4 changes: 3 additions & 1 deletion crates/bashkit/docs/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -893,9 +893,11 @@ access is bridged through Bashkit's VFS, never the host filesystem.
| Shebang line injection (TM-TS-023) | `#!...` header | Shebang stripped safely | MITIGATED |
| Deeply nested source aborts the host (TM-TS-024) | A few KB of `[[[...` or `1+1+...` overflows ZapCode's recursive parser/compiler | Nesting estimate rejects depth over 1000; ZapCode steps run on a 128 MiB-stack thread | MITIGATED |
| Huge `padStart`/`padEnd` aborts the host (TM-TS-025) | `'a'.padStart(2**40)` asks the allocator for ~1 TiB | Vendored ZapCode caps the target length at 10,000,000, like `repeat`; the run fails cleanly | MITIGATED |
| Memory growth past `max_memory` aborts the host (TM-TS-026) | Doubling a string or array, or joining/printing an array that shares one big string, until an allocation fails | A per-run meter stops the script once its heap passes `max_memory`; output-amplifying builtins size the result first; `ts` refuses to run without `MeteredAllocator` | MITIGATED |
| Host-call loop outruns the time limit (TM-TS-027) | `while (true) { await writeFile(...) }` resumes ZapCode's clock at every call | Wall-clock deadline checked at every host call; callbacks capped at the remaining time; the loop yields to the executor | MITIGATED |
| Bash var expansion injection (TM-TS-020) | Unquoted `$VAR` expanded before TS runs | By design; use single quotes to prevent expansion | MITIGATED |

Per-threat coverage (TM-TS-001 … TM-TS-025), including error-isolation and
Per-threat coverage (TM-TS-001 … TM-TS-027), including error-isolation and
exit-code propagation cases, is also exercised by the `threat_ts_*` tests.

### Request Signing & Snapshot Integrity (TM-CRY-*, TM-SNAP-*)
Expand Down
12 changes: 9 additions & 3 deletions crates/bashkit/docs/typescript.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Embedded TypeScript (ZapCode)

> **Experimental.** ZapCode is an early-stage TypeScript interpreter that may
> have undiscovered crash or security bugs. Resource limits are enforced by
> ZapCode's VM. The integration should be treated as experimental.
> have undiscovered crash or security bugs. Bashkit enforces the memory and
> wall-clock limits itself (ZapCode only counts allocations and VM time). The
> integration should be treated as experimental.

Bashkit embeds the [ZapCode](https://github.com/TheUncharted/zapcode) TypeScript
interpreter, a pure-Rust implementation with ~2µs cold start, no V8 dependency,
Expand All @@ -17,11 +18,16 @@ resource limits and no host access.

## Quick Start

Enable the `typescript` feature and register via builder:
Enable the `typescript` feature, install `bashkit::MeteredAllocator` as the
global allocator (the memory limit reads it; without it `ts` refuses to run),
and register via builder:

```rust
use bashkit::Bash;

#[global_allocator]
static ALLOC: bashkit::MeteredAllocator = bashkit::MeteredAllocator;

# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
let mut bash = Bash::builder().typescript().build();
Expand Down
4 changes: 4 additions & 0 deletions crates/bashkit/examples/typescript_external_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
use bashkit::{Bash, TypeScriptExternalFnHandler, TypeScriptLimits, ZapcodeValue};
use std::sync::Arc;

// ts memory limits need the metered allocator (see bashkit::MeteredAllocator).
#[global_allocator]
static ALLOC: bashkit::MeteredAllocator = bashkit::MeteredAllocator;

#[tokio::main]
async fn main() -> bashkit::Result<()> {
let handler: TypeScriptExternalFnHandler = Arc::new(|name, args| {
Expand Down
4 changes: 4 additions & 0 deletions crates/bashkit/examples/typescript_scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

use bashkit::Bash;

// ts memory limits need the metered allocator (see bashkit::MeteredAllocator).
#[global_allocator]
static ALLOC: bashkit::MeteredAllocator = bashkit::MeteredAllocator;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("=== Bashkit TypeScript Integration ===\n");
Expand Down
Loading
Loading