Repository navigation
Cloud Hypervisor: guest agent can read mcpg pre-filter logs under /tmp/gh-aw #9204
Description
Summary
On the Cloud Hypervisor runtime, the tmp-gh-aw export publishes all of /tmp/gh-aw into the guest. filesystem.allowWrite narrows what is writable, but everything under the export root remains readable by the agent. MCP Gateway's log directory defaults to a path inside that export and records full, pre-filter tool-call payloads — so the agent can read tool-call content through the log sink.
Details
Where the logs land
- mcpg's
DefaultLogDiris/tmp/gh-aw/mcp-logs(gh-aw-mcpg internal/config/config_core.go:51). - The compiled workflow passes
MCP_GATEWAY_LOG_DIRthrough withdocker run -ebut never assigns it, so mcpg falls back to that default (see.github/workflows/smoke-cloud-hypervisor.lock.yml, mcpgdocker runcommand). - AWF creates
/tmp/gh-aw/mcp-logsmode0777(src/log-directory-setup.ts:122-127), so nothing gates access inside the VM either.
What the logs contain
gh-aw-mcpg internal/server/tool_registry.go:325-339 logs the full tool-call response on every call:
logger.LogInfo("client", "MCP tool call response, session=%s, tool=%s, result=%s",
..., sanitize.MarshalAndSanitize(data))This call site is in the inner handler. The jq response filter (tryApplyToolResponseFilter) is applied by the outer wrapToolHandler, i.e. afterwards. The log therefore captures the pre-filter payload.
Payload redaction does not apply here: sanitize.EnablePayloadRedaction() is only called when cfg.Enclave != nil || cfg.Delegation != nil (internal/proxy/proxy.go:142-145). A standard (non-enclave) workflow leaves ShouldRedactPayload false.
Why file permissions don't help
mcpg runs as --user $(id -u):$(id -g) — the runner uid. The guest agent runs as that same uid (getSafeHostUid/getSafeHostGid). Same owner, so mode bits give no separation.
mcp-payloads is NOT part of this problem
/tmp/gh-aw/mcp-payloads is also inside the export, but it must stay readable:
PayloadTruncatedInstructions(internal/middleware/jqschema.go:39) instructs the agent to read the spilled file atpayloadPath.MCP_GATEWAY_PAYLOAD_PATH_PREFIXis not set, so the agent receives the real in-guest path.- Spilled content is written after the jq filter runs, and is session-segmented (one session per run).
Masking it would break large tool responses, and it is not a filter bypass. Only mcp-logs is at issue.
Adjacent, lower severity
/tmp/gh-aw/sandbox/firewall/logs and /tmp/gh-aw/sandbox/firewall/audit are also readable, giving the agent the Squid access log — every domain attempted and its allow/deny verdict, i.e. a map of the egress policy.
Recommended solution
1. Relocate the mcpg log directory (do first — cheapest)
Set MCP_GATEWAY_LOG_DIR explicitly to a path outside the writable export — e.g. under ${RUNNER_TEMP}/gh-aw/..., which is exported read-only, or fully outside any export. This is a compiler-side change in gh-aw and requires no AWF mount work. It removes the exposure for the common case immediately.
2. Add a mask overlay kind to the staged mount tree (durable defence-in-depth)
virtiofsd is rooted at the staged tree, so anything over-mounted there simply does not exist from the guest's view — no virtiofsd flag and no guest cooperation needed.
In src/cloud-hypervisor/mount-tree.ts, extend VirtiofsdOverlayKind with a mask class applied after the recursive read-only pass:
mount -t tmpfs -o ro,nosuid,nodev,size=0,mode=000 none <stagedDestination>
An empty read-only tmpfs is guaranteed empty, cannot be raced, and needs no host scratch directory.
Required adjustments:
assertTreeIsReadonly/assertOnlyOverlaysAreWritablemust accept a masked mount as a legitimate non-source mount.- Reject any
filesystem.allowWriteoverlay that overlaps a mask. - Reuse the existing containment / realpath / symlink / clean-absolute-path assertions.
- Apply ordering strictly: rbind → make-rprivate → deepest-first
roremounts → writable overlays → masks.
Candidate masks: /tmp/gh-aw/mcp-logs, /tmp/gh-aw/sandbox/firewall/logs, /tmp/gh-aw/sandbox/firewall/audit.
3. Consider enabling payload redaction outside enclave/delegation (mcpg-side)
Even with the directory hidden from the guest, pre-filter payloads are still written to disk and artifact-uploaded. Whether EnablePayloadRedaction() should apply to standard profiles is an mcpg policy decision, tracked separately.
Open question before prioritising
It is not yet established whether the jq response filter and the GITHUB_MCP_GUARD_MIN_INTEGRITY / GITHUB_MCP_GUARD_REPOS / GH_AW_SINK_VISIBILITY controls are security boundaries or response shaping. If those guards live upstream in github-mcp-server, guarded content may never reach mcpg's logger and this reduces to hygiene. Steps 1 and 2 are cheap enough to be worth doing regardless.
Verification notes
- Export set and modes:
src/cloud-hypervisor/exports.ts:55-150 - Write narrowing:
src/cloud-hypervisor/filesystem-write-policy.ts,src/cloud-hypervisor/filesystem-write-enforcement.ts - Host mount enforcement:
src/cloud-hypervisor/mount-tree.ts - virtiofsd args (no uid/gid translation, v1.10 pinned):
src/cloud-hypervisor/virtiofsd.ts:422-440 - mcpg references are against
gh-aw-mcpgatb9c9d53e; the workflow pinsghcr.io/github/gh-aw-mcpg:v0.4.27. sanitize.MarshalAndSanitizewas not audited, so the precise residual content of an unredacted log line is unconfirmed.
Activity