Skip to content

Cloud Hypervisor: guest agent can read mcpg pre-filter logs under /tmp/gh-aw #9204

Closed

Description

@lpcox

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 DefaultLogDir is /tmp/gh-aw/mcp-logs (gh-aw-mcpg internal/config/config_core.go:51).
  • The compiled workflow passes MCP_GATEWAY_LOG_DIR through with docker run -e but never assigns it, so mcpg falls back to that default (see .github/workflows/smoke-cloud-hypervisor.lock.yml, mcpg docker run command).
  • AWF creates /tmp/gh-aw/mcp-logs mode 0777 (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 at payloadPath.
  • MCP_GATEWAY_PAYLOAD_PATH_PREFIX is 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 / assertOnlyOverlaysAreWritable must accept a masked mount as a legitimate non-source mount.
  • Reject any filesystem.allowWrite overlay that overlaps a mask.
  • Reuse the existing containment / realpath / symlink / clean-absolute-path assertions.
  • Apply ordering strictly: rbind → make-rprivate → deepest-first ro remounts → 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-mcpg at b9c9d53e; the workflow pins ghcr.io/github/gh-aw-mcpg:v0.4.27.
  • sanitize.MarshalAndSanitize was not audited, so the precise residual content of an unredacted log line is unconfirmed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions