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
76 changes: 71 additions & 5 deletions src/kiro_crew/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -1679,12 +1679,48 @@ def main():
# available — better to function (with the original regression risk)
# than to refuse to start.

# Pre-read files that must survive dir hiding
# Pre-read files that must survive dir hiding.
#
# An expose source that cannot be READ degrades to "not exposed" with a
# stderr warning, the same way the Step 7 hardlink scan degrades open.
# This read runs during sandbox SETUP, so letting the OSError propagate
# aborts the child before the command runs at all -- and selective
# exposure is an OPTIMIZATION (keep ~/.aws/config reachable so
# credential_process still resolves inside an otherwise-hidden ~/.aws),
# never a security control. Failing the whole spawn because an optional
# convenience is unreadable trades a working sandbox for no sandbox.
#
# `isfile` already covers ABSENT; this covers UNREADABLE, and the two
# are not the same test: `stat` can succeed on a path whose `open` is
# then denied. Seen in the wild as a filesystem restriction inherited
# from the parent process, denying read on a 0600 file the child's own
# uid owned -- so DAC bits and uid both looked correct while every
# cc-mode spawn on that host died here.
#
# Catching the error is the only guard that HOLDS. Do not "tighten" this
# into a pre-flight `os.access(src_path, os.R_OK)`: measured on the
# affected host, `os.stat()` succeeded and `os.access()` reported BOTH
# X_OK and R_OK as True while the operation was denied anyway. The
# weaker check looks equivalent from the source alone and would
# silently restore the abort.
#
# The warning is not optional. Skipping silently would leave the child
# with no ~/.aws/config and no explanation, turning a loud setup failure
# into a later auth failure that points nowhere near this line.
expose_data = {{}}
for src_path, filename in EXPOSE_FILES:
if os.path.isfile(src_path):
with open(src_path, "rb") as fh:
expose_data[src_path] = fh.read()
try:
with open(src_path, "rb") as fh:
expose_data[src_path] = fh.read()
except OSError as exc:
print(
"sandbox: WARNING — cannot read %s (%s); it will be "
"ABSENT inside the sandbox. Anything depending on it "
"(e.g. credential_process in ~/.aws/config) will fail."
% (src_path, exc),
file=sys.stderr,
)

# Bind-mount empty dirs over credential paths (per-dir tmpdir to
# prevent content leaking across mounts via shared backing dir).
Expand Down Expand Up @@ -1725,8 +1761,38 @@ def main():
if HIDE_SSH and os.path.isdir(SSH_DIR):
kh_data = b""
if os.path.isfile(SSH_KNOWN_HOSTS):
with open(SSH_KNOWN_HOSTS, "rb") as fh:
kh_data = fh.read()
# Host trust data FAILS CLOSED. This is deliberately NOT the
# degrade-open treatment the EXPOSE_FILES pre-read above gets,
# and the two sites are NOT symmetric:
#
# - an unreadable ~/.aws/config costs REACHABILITY, so
# skipping it trades a convenience for a working sandbox;
# - an unreadable known_hosts costs VERIFICATION. The launcher
# puts StrictHostKeyChecking=accept-new into
# GIT_SSH_COMMAND, gated ONLY on that variable being unset
# -- never on whether this read succeeded. So continuing
# with an empty kh_data points UserKnownHostsFile at an
# absent file while auto-accept is still on: every host then
# reads as NEW and an interceptor's key is accepted. With
# known_hosts present, accept-new REFUSES a CHANGED key.
#
# A degrade here would therefore convert "refuse a changed key"
# into "accept anything". Aborting is the safe direction: no
# sandbox at all beats one that has quietly stopped verifying
# hosts. Report first so the abort is diagnosable, then re-raise
# and let it kill setup.
try:
with open(SSH_KNOWN_HOSTS, "rb") as fh:
kh_data = fh.read()
except OSError as exc:
print(
"sandbox: FATAL — cannot read %s (%s). Refusing to "
"continue: proceeding without it would leave host-key "
"verification accepting any new key."
% (SSH_KNOWN_HOSTS, exc),
file=sys.stderr,
)
raise
# Cross-fs source for the same kernel-race reason as SENSITIVE_DIRS
# (line 371) and SENSITIVE_FILES (line 389).
ssh_tmp = tempfile.mkdtemp(dir=_tmpfs_src).encode()
Expand Down
Loading
Loading