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
26 changes: 25 additions & 1 deletion crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,8 @@ impl Interpreter {
&s[..end]
}

// Decision: restored `$!` must match Bashkit-produced virtual numeric job ids.
const MAX_RESTORED_LAST_BG_PID_LEN: usize = 20;
const MAX_GLOB_DEPTH: usize = 50;

/// Create a new interpreter with the given filesystem.
Expand Down Expand Up @@ -2205,6 +2207,14 @@ impl Interpreter {
func_bytes,
Self::is_internal_variable,
);
// Keep live budget consistent with validate_shell_state_restore_limits,
// which counts the restored `$!` toward variable bytes.
if let Some(last_bg_pid) = &self.last_bg_pid {
self.memory_budget.variable_bytes = self
.memory_budget
.variable_bytes
.saturating_add(last_bg_pid.len());
}
}

fn migrate_legacy_attr_markers(
Expand Down Expand Up @@ -2292,14 +2302,28 @@ impl Interpreter {
}
}

let budget = crate::limits::MemoryBudget::recompute_from_state(
if let Some(last_bg_pid) = &state.last_bg_pid
&& (last_bg_pid.is_empty()
|| last_bg_pid.len() > Self::MAX_RESTORED_LAST_BG_PID_LEN
|| !last_bg_pid.bytes().all(|b| b.is_ascii_digit()))
{
return Err(crate::limits::LimitExceeded::Memory(
"invalid restored last background pid".to_string(),
)
.into());
}

let mut budget = crate::limits::MemoryBudget::recompute_from_state(
&state.variables,
&state.arrays,
&state.assoc_arrays,
0,
0,
Self::is_internal_variable,
);
if let Some(last_bg_pid) = &state.last_bg_pid {
budget.variable_bytes = budget.variable_bytes.saturating_add(last_bg_pid.len());
}
Comment on lines +2324 to +2326

if budget.variable_count > self.memory_limits.max_variable_count {
return Err(crate::limits::LimitExceeded::Memory(format!(
Expand Down
26 changes: 26 additions & 0 deletions crates/bashkit/tests/integration/snapshot_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,32 @@ async fn snapshot_restore_rejects_tampered_dir_stack_above_pushd_limit() {
);
}

#[tokio::test]
async fn snapshot_restore_rejects_invalid_last_bg_pid() {
let mut src = Bash::new();
src.exec("true &").await.unwrap();
let bytes = src.snapshot().unwrap();

for invalid_pid in ["9".repeat(64), "not-a-pid".to_string()] {
let mut tampered_json: serde_json::Value = serde_json::from_slice(&bytes[32..]).unwrap();
tampered_json["shell"]["last_bg_pid"] = invalid_pid.into();

let tampered_snapshot: Snapshot = serde_json::from_value(tampered_json).unwrap();
let tampered_bytes = tampered_snapshot.to_bytes().unwrap();

// Use permissive limits so the rejection is attributable to last_bg_pid
// validation, not incidental variable-byte pressure from the snapshot.
let mut restored = Bash::new();
let err = restored
.restore_snapshot(&tampered_bytes)
.expect_err("restore must reject invalid snapshot-controlled last_bg_pid");
assert!(
err.to_string().contains("last background pid"),
"expected last_bg_pid validation error, got: {err}"
);
}
}

// ==================== Atomic restore (Issue #1576) ====================

/// A malformed VFS snapshot (path validation failure) must cause
Expand Down
Loading