From 68ee2a4022ba2eb2647e142aa8cb48eeaad44352 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Sun, 4 Oct 2026 09:27:44 +0000 Subject: [PATCH 1/2] fix(deps): hold branches on its std path so nightly ASan compiles 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. The AddressSanitizer job is the only one that both tracks floating nightly and builds turso_core, so stable CI stayed green while the commit that passed as nightly run #241 failed as #242 a day later, on an unchanged tree. 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 declares a direct branches edge under the sqlite feature purely to turn std on for the whole graph. abort() then resolves to std::process::abort(), which is what a std-linking build wants anyway, and the unstable intrinsic leaves the tree entirely. No crate is added. Features unify per version, so a bump of the declaration to 0.5.0 would resolve a second copy and leave turso_core's 0.4.x one with no std -- verified to reproduce the original error verbatim. Dependabot ignores the crate for that reason, following the get-size2 precedent, and test_branches_std_shim.py asserts each invariant that keeps the shim working. --- .github/dependabot.yml | 20 ++++++ Cargo.lock | 1 + Cargo.toml | 11 +++ crates/bashkit/Cargo.toml | 6 +- knowledge/operations/dependencies.md | 23 +++++++ scripts/tests/test_branches_std_shim.py | 92 +++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 scripts/tests/test_branches_std_shim.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 777a57f94..862072329 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 6ed7a0172..39417f90d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,6 +349,7 @@ dependencies = [ "base64 0.23.1", "bigdecimal", "bitflags 2.13.2", + "branches", "bstr", "bytes", "bzip2", diff --git a/Cargo.toml b/Cargo.toml index 1b59e4b62..7cf427d0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bashkit/Cargo.toml b/crates/bashkit/Cargo.toml index dddae4ead..fc8dde6d3 100644 --- a/crates/bashkit/Cargo.toml +++ b/crates/bashkit/Cargo.toml @@ -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 @@ -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 diff --git a/knowledge/operations/dependencies.md b/knowledge/operations/dependencies.md index 444404072..b2c005847 100644 --- a/knowledge/operations/dependencies.md +++ b/knowledge/operations/dependencies.md @@ -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 diff --git a/scripts/tests/test_branches_std_shim.py b/scripts/tests/test_branches_std_shim.py new file mode 100644 index 000000000..baae49247 --- /dev/null +++ b/scripts/tests/test_branches_std_shim.py @@ -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() From 96986b1e7cff541d0ed6b6de359a988602c7f5cc Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Sun, 4 Oct 2026 09:27:53 +0000 Subject: [PATCH 2/2] fix(deps): bump http-cache-semantics to 4.3.0, drop its suppression GHSA-ch52-4w7c-c8xp was accepted as an explicit risk because 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 acceptance recorded. astro 7.3.5 requires ^4.2.0, so the floor goes in site/package.json beside the other security overrides and the suppression comes out. `./site: no known vulnerabilities found` now holds with nothing ignored. braces 3.0.3 is still both the latest release and the last affected one, so GHSA-vfj7-8cjw-p6xm stays, now as the sole npm entry. --- knowledge/log.md | 6 ++++++ knowledge/security/threat-model.md | 1 - scripts/audit-lockfiles.sh | 25 ++++++++++--------------- site/package.json | 1 + site/pnpm-lock.yaml | 9 +++++---- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/knowledge/log.md b/knowledge/log.md index 2e10709df..ebdc02ef1 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -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. diff --git a/knowledge/security/threat-model.md b/knowledge/security/threat-model.md index c1a01c7b3..b9766713c 100644 --- a/knowledge/security/threat-model.md +++ b/knowledge/security/threat-model.md @@ -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 diff --git a/scripts/audit-lockfiles.sh b/scripts/audit-lockfiles.sh index e5effd857..57c1239a8 100755 --- a/scripts/audit-lockfiles.sh +++ b/scripts/audit-lockfiles.sh @@ -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 @@ -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 diff --git a/site/package.json b/site/package.json index ecaf1c1d1..81eba6f05 100644 --- a/site/package.json +++ b/site/package.json @@ -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", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 224b4b0c7..673e24aab 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: 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' @@ -1380,8 +1381,8 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-cache-semantics@4.3.0: + resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -3249,7 +3250,7 @@ snapshots: get-tsconfig: 5.0.0-beta.4 github-slugger: 2.0.0 html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 + http-cache-semantics: 4.3.0 js-yaml: 4.3.2 jsonc-parser: 3.3.1 magic-string: 1.4.2 @@ -3728,7 +3729,7 @@ snapshots: html-void-elements@3.0.0: {} - http-cache-semantics@4.2.0: {} + http-cache-semantics@4.3.0: {} inline-style-parser@0.2.7: {}