Synced from monorepo
Synced from monorepo Changes: - Shell: accept target response id on rewind execute - Shell: stamp response id on chat user message chunks - Worktree: optional rebuild and stale git registration cleanup in auto-GC - Worktree: kind-aware auto-GC TTLs and config knobs - Worktree: macOS process CWD scan and Unix PID liveness for GC guards - Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only) - Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups - Shell: stop overwriting user skills - Tools: read markdown in `skills/` directories untruncated - `/usage` shows per-session token and dollar usage in the TUI - Security: prompt on environment-dumping `ps` variants - Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission - Tools: make scheduler deletion durable - Shell: add relocation storage primitives - Shell: give side model calls their own conversation ids - Fix five workflow-runtime bugs (budget, pause, cancel, reconnect) - Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask) - Pager: expose doctor in the TUI - Security: block unauthorized RCE via abused safe commands - Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent" - Security: block `rg --pre` arbitrary code execution in auto-mode - Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section - App builder deployer: `allow_forking` and `show_built_with_grok` - Pager: stop stacking duplicate "Worked for" markers on parked turns - Shell: support `max` as a distinct reasoning effort tier - Tools: serialize background `/loop` fires on the whole work unit - Shell: add working-directory relocation state primitives - Proto: `ClientToolResult` and `ChatConfig` client-side tools - Shell: model providers - Chat: select App Builder product on the Build path - Shell: attach author identity to feedback when the deployment opts in - Doctor: fix for SSH wrap setup - Workflow authoring skills: create-workflow and import-claude-workflow docs - Add read-only grok doctor - Sandbox: apply Landlock without a controlling TTY - Pager: recover image paste over grok wrap on headless remotes - Pager: make actions screen-mode aware - Shell: resume sessions when the working directory moves - Pager: centralize terminal diagnostics - Workspace: gate inline shell file access - Pager: centralize terminal probes - Pager: edit minimal prompts in an external editor - Pager: standardize backgrounding on Ctrl+B - Shell: recap rides the parent turn's prompt cache - Tools: add scheduler lifecycle version clock Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
This commit is contained in:
parent
a881e6703f
commit
3af4d5d398
556 changed files with 56609 additions and 21892 deletions
|
|
@ -22,7 +22,7 @@ pub(crate) fn grok_home() -> PathBuf {
|
|||
/// or seed RNGs.
|
||||
///
|
||||
/// These are individual files (use `allow_file`, not `allow_path`).
|
||||
/// `/dev/pts` is a directory (PTY slaves on Linux) so it uses `allow_path`.
|
||||
/// Directory nodes under `/dev` belong in [`DEVICE_DIRS`].
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
pub(crate) const DEVICE_FILES: &[&str] = &[
|
||||
"/dev/null", // output sink — used by virtually every CLI tool
|
||||
|
|
@ -31,13 +31,13 @@ pub(crate) const DEVICE_FILES: &[&str] = &[
|
|||
"/dev/urandom", // entropy — used by crypto/TLS
|
||||
"/dev/tty", // controlling terminal — used by git, ssh, gpg
|
||||
"/dev/ptmx", // PTY allocation — used by terminal spawning
|
||||
"/dev/fd", // file descriptor access (symlink to /proc/self/fd on Linux)
|
||||
];
|
||||
|
||||
/// Device directories that need write access.
|
||||
/// Device directories that need write access (use `allow_path`, not `allow_file`).
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
pub(crate) const DEVICE_DIRS: &[&str] = &[
|
||||
"/dev/pts", // PTY slaves (Linux)
|
||||
"/dev/fd", // fd table (symlink to /proc/self/fd on Linux; a directory)
|
||||
];
|
||||
|
||||
// ── Temporary directories ───────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -169,6 +169,31 @@ fn load_config_file(path: &Path) -> Option<SandboxConfig> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether a device **file** entry is safe to pass to `allow_file` / Landlock
|
||||
/// PathFd materialization.
|
||||
///
|
||||
/// `/dev/tty` always exists, but without a controlling terminal `open()` returns
|
||||
/// ENXIO and nono's apply aborts the **entire** ruleset. Built-in profiles fail
|
||||
/// open, which was a silent sandbox bypass under `setsid`/CI/headless launches.
|
||||
///
|
||||
/// Only that class of failure (and missing nodes) is filtered here. Other open
|
||||
/// errors — notably **EISDIR** on directory nodes — must not drop the path:
|
||||
/// directories are granted via [`DEVICE_DIRS`] / `allow_path`, and a plain
|
||||
/// `File::open` EISDIR does not mean Landlock would reject the grant.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn device_file_openable(path: &Path) -> bool {
|
||||
match std::fs::File::open(path) {
|
||||
Ok(_) => true,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
|
||||
// ENXIO/ENODEV: e.g. /dev/tty with no controlling terminal — PathFd
|
||||
// materialization would abort the whole Landlock ruleset.
|
||||
Err(e) if matches!(e.raw_os_error(), Some(libc::ENXIO) | Some(libc::ENODEV)) => false,
|
||||
// EISDIR, EACCES, etc.: still attempt the grant path. allow_file may
|
||||
// reject ExpectedFile; that only skips this entry, not the whole apply.
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileName {
|
||||
/// Convert this profile into a nono `CapabilitySet` for the given workspace.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
|
|
@ -238,7 +263,9 @@ impl ProfileName {
|
|||
// Device special files (character devices like /dev/null, /dev/tty, etc.).
|
||||
for dev in DEVICE_FILES {
|
||||
let p = Path::new(dev);
|
||||
if !p.exists() {
|
||||
// nono opens each entry read-only at apply time, so a node that exists
|
||||
// but cannot be opened would abort the whole ruleset, not just itself.
|
||||
if !device_file_openable(p) {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = caps.allow_file_mut(p, AccessMode::ReadWrite) {
|
||||
|
|
@ -820,4 +847,94 @@ read_write = ["/tmp/ci-artifacts"]
|
|||
.expect_err("Off.resolve must Err");
|
||||
assert!(err.to_string().contains("off"), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn enxio_device_file_is_skipped_but_directory_is_not() {
|
||||
assert!(
|
||||
device_file_openable(Path::new("/dev/null")),
|
||||
"openable device must still be allow-listed"
|
||||
);
|
||||
|
||||
// /dev/tty without a controlling terminal → ENXIO (the apply-abort case).
|
||||
// Skip the assertion when a ctty is present (open succeeds).
|
||||
match std::fs::File::open("/dev/tty") {
|
||||
Err(e) if e.raw_os_error() == Some(libc::ENXIO) => {
|
||||
assert!(
|
||||
!device_file_openable(Path::new("/dev/tty")),
|
||||
"ENXIO /dev/tty must be skipped so Landlock apply cannot abort"
|
||||
);
|
||||
}
|
||||
Ok(_) | Err(_) => {}
|
||||
}
|
||||
|
||||
// Directories must stay grantable. On Linux, File::open returns EISDIR;
|
||||
// on macOS it often succeeds. Either way the probe must return true so
|
||||
// directory devices (e.g. /dev/fd via DEVICE_DIRS) are not dropped.
|
||||
let dir = std::env::temp_dir().join(format!("grok-sbx-test-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
match std::fs::File::open(&dir) {
|
||||
Err(e) => {
|
||||
assert_eq!(
|
||||
e.raw_os_error(),
|
||||
Some(libc::EISDIR),
|
||||
"unexpected directory open error: {e}"
|
||||
);
|
||||
assert!(
|
||||
device_file_openable(&dir),
|
||||
"EISDIR must not drop a path from grant consideration"
|
||||
);
|
||||
}
|
||||
Ok(_) => {
|
||||
assert!(
|
||||
device_file_openable(&dir),
|
||||
"openable directory must remain grantable"
|
||||
);
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Building the strict CapabilitySet must succeed even when /dev/tty cannot
|
||||
/// be opened (no controlling terminal). Regression for the silent Landlock
|
||||
/// apply-abort under setsid/CI/headless.
|
||||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn strict_capability_set_builds_without_openable_dev_tty() {
|
||||
let workspace = std::env::current_dir().unwrap();
|
||||
let result = ProfileName::Strict.to_capability_set(&workspace);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"strict CapabilitySet must build even if /dev/tty is unopenable: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
/// `/dev/fd` is a directory (→ `/proc/self/fd` on Linux). It must be granted
|
||||
/// via DEVICE_DIRS/`allow_path`, not dropped by a file-open EISDIR probe.
|
||||
#[test]
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn dev_fd_is_granted_as_device_dir_not_skipped_as_file() {
|
||||
assert!(
|
||||
!DEVICE_FILES.contains(&"/dev/fd"),
|
||||
"/dev/fd must not sit in DEVICE_FILES (File::open → EISDIR)"
|
||||
);
|
||||
assert!(
|
||||
DEVICE_DIRS.contains(&"/dev/fd"),
|
||||
"/dev/fd must be in DEVICE_DIRS so allow_path can grant it"
|
||||
);
|
||||
let dev_fd = Path::new("/dev/fd");
|
||||
if dev_fd.exists() {
|
||||
// Directory open fails with EISDIR for plain File::open — the probe
|
||||
// must still report grantable so we don't regress directory devices.
|
||||
assert!(
|
||||
device_file_openable(dev_fd),
|
||||
"/dev/fd must not be filtered out by the ENXIO-only open probe"
|
||||
);
|
||||
assert!(
|
||||
dev_fd.is_dir(),
|
||||
"expected /dev/fd to be a directory on this platform"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue