fix: harden artifact store, skills loader, hooks, shell and agent warnings#2480
Merged
dgageot merged 1 commit intodocker:mainfrom Apr 21, 2026
Conversation
…nings
A code review of the repository surfaced several bugs and security
issues that this commit addresses together:
* content/store: resolveIdentifier and StoreArtifact now strictly
validate digests ("sha256:" + 64 hex chars) before using them as
filesystem path components. Without this, an identifier such as
"sha256:../../etc/passwd" flowed through filepath.Join and let
GetArtifact*/DeleteArtifact read or delete files outside the
store directory. A new ErrInvalidDigest sentinel is returned and
the refs-file path revalidates the digest on read.
* skills/remote: the remote-skills loader now rejects index entries
whose name is not a plain single path component (matched by a
conservative regex). Previously only entry.Files was validated,
so a hostile index could use a name like "../evil" to write
cache files outside the cache directory and later redirect
SKILL.md reads to attacker-chosen filesystem locations.
* hooks/executor: PreToolUse hooks that fail to run to completion
(timeout, parent-context cancellation, spawn error, missing
binary, ...) now deny the tool call instead of silently allowing
it. executeHook normalizes a fired context to a plain execution
error so aggregateResults can reliably fail closed on that event.
PostToolUse and other observational events keep their log-and-
continue behavior.
* agent: Agent.pendingWarnings is now guarded by a dedicated
sync.Mutex. addToolWarning and DrainWarnings are called from the
runtime loop, the MCP server, the TUI and the session manager in
parallel, and the concurrent append/read+clear was a data race.
* tools/builtin/shell: when cmd.Start() succeeds but the follow-up
createProcessGroup call fails, a new reapSpawnedChild helper
sends SIGTERM (with a SIGKILL escalation after a grace period)
and calls cmd.Wait() so the child is reaped and its stdout/
stderr pipes are closed. Both affected error paths now use it.
Each change ships with targeted tests (including -race where
relevant). mise lint is clean and the full test suite passes.
Assisted-By: docker-agent
trungutt
approved these changes
Apr 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A code review of the repository surfaced several bugs and security issues. This PR groups the fixes for the ones that can be addressed without touching unrelated files.
Fixes
pkg/content/store.go— path traversal via crafted digestresolveIdentifierpreviously accepted anysha256:-prefixed string verbatim, so an identifier likesha256:../../etc/passwdwould flow throughfilepath.Join(baseDir, digest+".tar")inGetArtifactImage/GetArtifactPath/DeleteArtifact, allowing reads or deletions outside the artifact store. Digests are now strictly validated (sha256:+ 64 lowercase hex characters) both for the bare-digest and digest-reference (repo@sha256:...) forms, on the value read back from the refs index, and inStoreArtifact(defense-in-depth before the write path). Malformed digests are rejected with a newErrInvalidDigestsentinel.pkg/skills/remote.go— path traversal via remote skill nameThe remote-skills loader already validated
entry.Filesagainst path traversal, but notentry.Name. The skill name flows intofilepath.Join(cacheBase, urlHash, skillName)and into the returnedSkill'sFilePath/BaseDir, so a hostile remote index could use a name like../evilora/bto cache files outside the skills cache directory and later redirectSKILL.mdreads to attacker-chosen filesystem locations. Skill names now must match a conservative regex (^[A-Za-z0-9_-][A-Za-z0-9._-]{0,127}$).pkg/hooks/executor.go— PreToolUse hooks failed openaggregateResultssilently skipped hook results whose process failed to run (timeout, context cancellation, shell not found, binary missing…), leavingfinalResult.Allowed == true. ForPreToolUsehooks — which are used as a security gate around tool calls — this was a fail-open: a hook configured to deny e.g.rm -rfcould be bypassed by anything that made it error out before reporting a verdict.Now:
executeHooknormalizes a fired timeout / parent-context cancellation to a plain execution error, so the condition is reliably detectable by the caller.aggregateResults, wheneventType == EventPreToolUseand a hook returned an execution error, setsAllowed = falseand surfaces the reason inMessage/Stderr. Other event types (PostToolUse,SessionStart,Stop,Notification, …) keep the log-and-continue behavior since they are observational, not gating.pkg/agent/agent.go— data race onpendingWarningsaddToolWarningandDrainWarningsmutatea.pendingWarnings(append / read + reset) from any goroutine that callsAgent.Tools(),StartedTools()orensureToolSetsAreStarted(). Those are reached from the runtime loop, the MCP server, the session manager, the TUI and tests — often in parallel — so the concurrent writes were a data race and could corrupt the slice or produce torn reads. A dedicatedwarningsMu sync.Mutexnow guards both paths.pkg/tools/builtin/shell.go— zombie child oncreateProcessGroupfailureRunShellBackgroundandrunNativeCommandboth callcmd.Start()and thencreateProcessGroup(cmd.Process). IfcreateProcessGroupfailed, the child had already been started — but the existing code either leaked it (runNativeCommand: returned without killing or waiting) or only calledkillwithout a subsequentcmd.Wait()(RunShellBackground), leaving a zombie and holding the stdout/stderr pipes open until GC. A newreapSpawnedChildhelper sendsSIGTERMvia the process group, escalates toSIGKILLafter a short grace period, and always callscmd.Wait(). Both error paths now use it.Testing
pendingWarnings.mise lint— 0 issues across 754 files.go test ./...— all packages pass.go test -race— all affected packages pass.Known issues intentionally not fixed here
The review also surfaced several issues outside the footprint of this PR (SSRF in the
fetchbuilt-in tool and in OAuth resource-metadata discovery, env-var injection in thescript_shelltool, file-permission widening in filesystem edit/write, and unsynchronized in-memory session metadata). They're best addressed in follow-up PRs scoped to those subsystems.