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
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ updates:
# pin comment sits next to the dep in crates/bashkit/Cargo.toml.
- dependency-name: "get-size2"

# branches is not a dependency bashkit uses — no bashkit source
# references it. It is declared under the `sqlite` feature purely to turn
# ON `branches/std` for the whole graph: `turso_core` depends on
# `branches` with `default-features = false`, and that no_std path
# compiles `core::intrinsics::abort()`, which rustc removed in
# 1.101.0-nightly — it turned the nightly ASan job red on an unchanged
# tree (nightly run #242).
#
# Cargo unifies features per *version*, so the shim only works while
# bashkit and turso_core resolve to the same copy. turso_core requires
# `^0.4.3`; a bump of bashkit's declaration to 0.5.0 would resolve a
# second copy and leave turso_core's 0.4.x one with no `std` again,
# silently re-breaking nightly. Ignore outright rather than by
# update-type: 0.5.0 carries the same unstable intrinsic, so there is
# nothing to gain from any bump.
#
# Drop this once `branches` compiles on nightly without `std`; the
# rationale sits next to the dep in the root Cargo.toml.
- dependency-name: "branches"

# Fuzz targets are a separate cargo workspace with their own lockfile, so the
# "/" entry above never touches them. Without this the fuzz lockfile silently
# rots between releases (it drifted onto an unsound `anyhow` and two yanked
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,17 @@ russh = "0.63"
# Embedded SQLite engine (Turso, pure Rust). Upstream is BETA — gated behind
# the `sqlite` feature and disabled by default; see knowledge/runtimes/sqlite-builtin.md.
turso_core = "0.8.0-pre.1"
# Not used by any bashkit code. Present only to turn ON `branches/std` for the
# whole graph: `turso_core` depends on `branches` with `default-features = false`,
# and the resulting no_std path compiles `core::intrinsics::abort()`, which rustc
# removed in 1.101.0-nightly — it broke the nightly ASan job while stable CI stayed
# green (nightly run #242). Upstream `branches` is unfixed at its latest release
# (0.5.0 carries the same line), so there is no version to bump to. Cargo offers no
# way to add a feature to a transitive dep other than a direct edge, so this is it.
# `std` makes `abort()` call `std::process::abort()` — the correct behaviour for a
# std-linking build regardless, and it drops the unstable intrinsic entirely.
# Remove when `branches` compiles on nightly without `std`.
branches = { version = "0.4.3", default-features = false, features = ["std"] }

# Serial test execution
serial_test = "4"
Expand Down
6 changes: 5 additions & 1 deletion crates/bashkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ zapcode-core = { version = "1.5.1", optional = true }
# Pulls a multi-MB transitive dep tree only when the `sqlite` feature is active.
turso_core = { workspace = true, optional = true }

# Feature-unification shim for `branches`, a transitive dep of `turso_core`.
# Never referenced in code — see the comment in the workspace Cargo.toml.
branches = { workspace = true, optional = true }

[features]
default = ["bash_tool", "ring", "tzdata"]
# Named IANA timezone support for the `date` builtin's `TZ=` handling, via the
Expand Down Expand Up @@ -258,7 +262,7 @@ typescript = ["dep:zapcode-core"]
# Requires the multi-threaded tokio runtime so the sync `IO` trait can bridge
# back to the async VFS via `block_in_place`.
# Usage: cargo build --features sqlite
sqlite = ["dep:turso_core", "tokio/rt-multi-thread"]
sqlite = ["dep:turso_core", "dep:branches", "tokio/rt-multi-thread"]
# Enable RealFs backend for accessing host filesystem directories
# WARNING: This intentionally breaks the sandbox boundary.
# Usage: cargo build --features realfs
Expand Down
6 changes: 6 additions & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Bashkit Knowledge Update Log

## 2026-10-04

* **Build**: Nightly went red on a tree nobody had touched. `turso_core` depends on `branches` with `default-features = false`, and that no_std path compiles `core::intrinsics::abort()` — an unstable intrinsic rustc removed in 1.101.0-nightly. Only the AddressSanitizer job both tracks floating nightly and builds `turso_core` (`bashkit-cli` enables `sqlite` by default; Miri builds `bashkit --lib` without it), so stable CI stayed green and the same commit that passed as run #241 failed as #242 a day later. There is no version to bump to: `branches` 0.5.0, its latest release, carries the same line, and `turso_core` requires `^0.4.3` regardless. Cargo cannot add a feature to a transitive dependency, so `bashkit` now declares a direct `branches` edge under the `sqlite` feature purely to turn `std` on for the whole graph — `abort()` becomes `std::process::abort()`, which is what a std-linking build wants anyway, and the unstable intrinsic leaves the tree. Recorded in [Dependency Policy](operations/dependencies.md#feature-unification-edges).
* **Enforcement**: That shim has three ways to lapse and all of them are quiet. Cargo unifies features per *version*, so a Dependabot bump of the declaration to 0.5.0 resolves a *second* copy and leaves `turso_core`'s 0.4.x one with no `std` — verified, not assumed: the bump reproduces the original error verbatim and locks both 0.4.6 and 0.5.0. An optional dep that no feature enables is a no-op that still reads as a fix. And none of it fails a stable build, so the breakage reappears a day later in a job nobody is watching. `scripts/tests/test_branches_std_shim.py` asserts the `std`-only feature set, the 0.4 version line, exactly one locked copy, that `sqlite` activates the edge, and that Dependabot ignores the crate — each verified to fail when its invariant is removed. The Dependabot ignore follows the `get-size2` precedent: same shape of shim, same reason.
* **Security**: GHSA-ch52-4w7c-c8xp is no longer suppressed. When #2498 accepted it, 4.2.0 was both `http-cache-semantics`' latest release and the advisory's `last_affected` — there was no floor to raise. 4.3.0 has since published above it, which is exactly the removal condition that entry recorded, so the suppression is gone and `site/package.json` carries a `>=4.3.0` override instead, beside the other security floors. `./site: no known vulnerabilities found` now holds with nothing ignored. A `last_affected` advisory with no fix is a floor that can appear later without anything announcing it, so the recorded removal condition is the only thing that makes an accepted risk temporary rather than permanent — re-reading those conditions belongs in maintenance, not in whatever next touches the file. `braces` 3.0.3 is still both latest and last affected, so GHSA-vfj7-8cjw-p6xm stays, now as the sole entry.

## 2026-10-02

* **Security**: Dependabot's directory list had drifted, and nothing would have said so. `crates/bashkit-python/test-fixtures/random-fs` is an independent cargo workspace (`[workspace]` table, own lockfile) with a `pyo3` dependency and had no `dependabot.yml` entry, while its napi twin `crates/bashkit-js/test-fixtures/random-fs` did. Both fixtures' lockfiles are gitignored and generated at test time, so the advisory scan does not see them in a fresh checkout either — Dependabot was the only mechanism watching that `pyo3`, and for it there was none (no advisory against 0.29.0; 0.29.3 was available). Entry added. `scripts/tests/test_dependabot_coverage.py` now fails when any cargo, pnpm or uv workspace in the tree has no entry: Dependabot cannot glob, so unlike the advisory scan it has no discovery to switch to, and "remember to add it by hand" is the step that has gone wrong three times now (2026-08-22, 2026-10-02, this). A missing entry is quiet by nature — nothing fails, no PR opens, the dependency just stops being watched. Also corrected two stale claims in the [Threat Model](security/threat-model.md): there are six cargo workspaces and not five, four of whose lockfiles are committed, and the Dependabot row listed two directories when the config enumerates thirteen.
Expand Down
23 changes: 23 additions & 0 deletions knowledge/operations/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,29 @@ the existing Python graph. Remove it when Monty's AST upgrades its string
type. The Python binding's `num-bigint` dependency must still match Monty's
0.4 line.

## Feature-unification edges

`branches` is a direct dependency of `bashkit` under the `sqlite` feature and is
never referenced by any Bashkit code. It exists only to turn its `std` feature on
for the whole graph. `turso_core` depends on `branches` with
`default-features = false`, and the resulting `no_std` body of `branches::abort()`
is `core::intrinsics::abort()`, an unstable intrinsic rustc removed in
1.101.0-nightly. Stable CI stayed green; the nightly AddressSanitizer job went red
on an unchanged tree (nightly run #242, 2026-10-04), because that job is the only
one that both tracks floating nightly and builds `turso_core` — `bashkit-cli`
enables `sqlite` by default, and Miri only builds `bashkit --lib` without it.

There is no version to bump to: `branches` 0.5.0, its latest release, carries the
same line, and `turso_core` requires `^0.4.3` regardless. Cargo has no way to add
a feature to a transitive dependency other than a direct edge, so the edge is the
fix. Enabling `std` is also the behaviour this build wants on its own terms —
`abort()` becomes `std::process::abort()`, which raises `SIGABRT` and honours
registered abort handlers, instead of a bare trap — and it drops the unstable
intrinsic from the graph entirely. The edge adds no crate to the tree and enables
no runtime surface by itself.

Remove it when `branches` compiles on nightly without `std`.

## Measure size and audit surface separately

The `idna_adapter` pin removes 21 crates but only **15 KB (0.16%)** from the
Expand Down
1 change: 0 additions & 1 deletion knowledge/security/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -1740,7 +1740,6 @@ checks.
| RUSTSEC-2023-0071 | `rsa` | Marvin timing sidechannel (TM-CRY-002). No patched version exists; reachable only via the opt-in `ssh` feature | `rsa` ships a constant-time release |
| RUSTSEC-2023-0089 | `atomic-polyfill` | Unmaintained, no known vulnerability; transitive via `monty` → `postcard` → `heapless` (pinned at 0.7, which still requires it) | Upstream drops the dependency |
| GHSA-vfj7-8cjw-p6xm | `braces` (npm) | Stack-exhaustion DoS through deeply nested patterns. No patched version exists — 3.0.3 is both the latest release and the last affected one. Reached only as `ava` → `globby` → `fast-glob` → `micromatch` → `braces`, a devDependency of `crates/bashkit-js`, globbing test-file patterns committed to this repo; it is not in the published package's `files` list, so no consumer installs it | `braces` ships a release above 3.0.3 |
| GHSA-ch52-4w7c-c8xp | `http-cache-semantics` (npm) | `max-stale` handling can disclose cross-user cached responses. No patched version exists — 4.2.0 is both the latest release and the last affected one. A direct dependency of `astro` 7.3.5, the site's build tooling; the exposure needs a cache serving more than one user, and this is a single-tenant build step caching its own fetches | `http-cache-semantics` ships a release above 4.2.0 |

A suppression that stops matching is not evidence that it can be dropped.
`cargo deny check advisories` currently warns `advisory-not-detected` for
Expand Down
25 changes: 10 additions & 15 deletions scripts/audit-lockfiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,17 @@ IGNORED_ADVISORIES=(

# npm advisories, by GHSA (or CVE) id. A patched release is almost always
# reachable by a bump, because the npm dependencies are all dev- or
# example-only; these two are the exception the mechanism exists for. Both
# advisories landed on 2026-10-02 against the newest published version of the
# package, with no fixed release to move to, so the choice is an explicit,
# documented acceptance or a red scan that stays red — not a pinned floor
# nobody records.
# example-only; the entry below is the exception the mechanism exists for. It
# landed on 2026-10-02 against the newest published version of the package,
# with no fixed release to move to, so the choice is an explicit, documented
# acceptance or a red scan that stays red — not a pinned floor nobody records.
#
# Both are `last_affected` advisories in OSV, not `fixed` ones: there is no
# patched version, so check the advisory's own fixed-range before dropping
# either entry. Neither package reaches a consumer of a published artifact.
# It is a `last_affected` advisory in OSV, not a `fixed` one: there is no
# patched version, so re-check the advisory's own fixed-range before dropping
# the entry. A sibling entry for GHSA-ch52-4w7c-c8xp
# (`http-cache-semantics`) was dropped this way once 4.3.0 published above its
# `last_affected` 4.2.0; `site/package.json` now pins that floor as an
# override. The package below reaches no consumer of a published artifact.
#
# GHSA-vfj7-8cjw-p6xm — stack-exhaustion DoS in `braces` from deeply nested
# patterns. Reached only as ava → globby → fast-glob → micromatch → braces, a
Expand All @@ -73,15 +75,8 @@ IGNORED_ADVISORIES=(
# its package.json `files` array, so no consumer installs this chain. Drop
# once `braces` ships a release above 3.0.3.
#
# GHSA-ch52-4w7c-c8xp — `http-cache-semantics` max-stale handling can disclose
# cross-user cached responses from a shared HTTP cache. A direct dependency of
# astro 7.3.5, which is the site's build tooling; the exposure needs a cache
# serving more than one user, and this one is a single-tenant build step that
# caches its own fetches. 4.2.0 is both the latest release and the last
# affected one. Drop once `http-cache-semantics` ships a release above 4.2.0.
IGNORED_NPM_ADVISORIES=(
GHSA-vfj7-8cjw-p6xm
GHSA-ch52-4w7c-c8xp
)

# PyPI advisories, by OSV (or GHSA) id. Empty: nothing has yet needed
Expand Down
92 changes: 92 additions & 0 deletions scripts/tests/test_branches_std_shim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""The `branches` feature-unification shim, and the ways it can silently lapse.

`turso_core` depends on `branches` with `default-features = false`. The
resulting no_std body of `branches::abort()` is `core::intrinsics::abort()`, an
unstable intrinsic rustc removed in 1.101.0-nightly, so the crate stopped
compiling there — nightly run #242 went red on an unchanged tree while stable
CI stayed green. `branches` has no fixed release (0.5.0 carries the same line)
and Cargo cannot add a feature to a transitive dependency, so `bashkit`
declares a direct edge under the `sqlite` feature purely to turn `std` on for
the whole graph.

Every part of that is quiet when it breaks. Cargo unifies features per
*version*, so the shim stops working the moment a second copy of `branches`
resolves — and nothing in a stable build notices, because only the nightly
ASan job both tracks floating nightly and builds `turso_core`. The failure
reappears a day later, in a job nobody is watching, on a tree that looks fine.
These tests convert each way that can happen into a loud failure.

See knowledge/operations/dependencies.md#feature-unification-edges.
"""
from pathlib import Path
import tomllib
import unittest

import yaml

ROOT = Path(__file__).resolve().parents[2]


def load(path):
with open(ROOT / path, 'rb') as handle:
return tomllib.load(handle)


class BranchesStdShimTests(unittest.TestCase):
def test_workspace_declares_branches_with_std_and_nothing_else(self):
# `std` is what replaces the unstable intrinsic with
# std::process::abort(). default-features would also drag in
# `prefetch`, which this shim has no reason to enable.
dep = load('Cargo.toml')['workspace']['dependencies']['branches']
self.assertEqual(dep['features'], ['std'])
self.assertIs(dep['default-features'], False)

def test_shim_stays_on_the_version_line_turso_core_resolves(self):
# Cargo unifies features per version. turso_core requires `^0.4.3`, so
# a declaration of 0.5.0 here resolves a *second* copy and leaves
# turso_core's 0.4.x one with no `std` — the original breakage, back
# with the shim still present and looking correct.
req = load('Cargo.toml')['workspace']['dependencies']['branches']['version']
self.assertTrue(
req.startswith('0.4.'),
f'branches must stay on the 0.4 line turso_core resolves, got {req!r}',
)

def test_exactly_one_copy_of_branches_is_locked(self):
# The invariant the two tests above exist to protect, asserted against
# what Cargo actually resolved. Two copies means the shim applies to
# one of them and turso_core compiles the other without `std`.
versions = [
pkg['version']
for pkg in load('Cargo.lock')['package']
if pkg['name'] == 'branches'
]
self.assertEqual(
len(versions), 1, f'expected one locked `branches`, got {versions}'
)

def test_sqlite_feature_activates_the_shim(self):
# The edge has to be active exactly when turso_core is. An optional dep
# that no feature enables is a no-op that still reads as a fix.
manifest = load('crates/bashkit/Cargo.toml')
self.assertTrue(manifest['dependencies']['branches']['optional'])
sqlite = manifest['features']['sqlite']
self.assertIn('dep:branches', sqlite)
self.assertIn('dep:turso_core', sqlite)

def test_dependabot_ignores_branches(self):
# Dependabot rewrites manifests, and a bump to 0.5.0 is exactly the
# second-copy case above. 0.5.0 carries the same unstable intrinsic, so
# there is nothing a bump could gain.
config = yaml.safe_load((ROOT / '.github/dependabot.yml').read_text())
root_cargo = next(
entry
for entry in config['updates']
if entry['package-ecosystem'] == 'cargo' and entry['directory'] == '/'
)
ignored = {item['dependency-name'] for item in root_cargo['ignore']}
self.assertIn('branches', ignored)


if __name__ == '__main__':
unittest.main()
1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"overrides": {
"esbuild": ">=0.28.1",
"fast-uri": "^3.1.8",
"http-cache-semantics": ">=4.3.0",
"js-yaml": "^4.3.1",
"nanoid": "^3.3.17",
"postcss": ">=8.5.18",
Expand Down
9 changes: 5 additions & 4 deletions site/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading