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
5 changes: 5 additions & 0 deletions actions/setup/sh/cloud_hypervisor_host_preflight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,10 @@ if ! test -e /dev/kvm; then
exit 1
fi

if ! test -c /dev/kvm; then
echo "::error::/dev/kvm must be a character device."
exit 1
fi

echo "runner is eligible for cloud-hypervisor preview"
echo "::endgroup::"
19 changes: 19 additions & 0 deletions actions/setup/sh/cloud_hypervisor_kvm_access.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,37 @@ if [[ ! -e /dev/kvm ]]; then
exit 1
fi

if [[ ! -c /dev/kvm ]]; then
echo "::error::/dev/kvm must be a character device."
exit 1
fi

if ! command -v setfacl >/dev/null 2>&1; then
echo "::error::setfacl is required to grant scoped access to /dev/kvm."
exit 1
fi

runner_uid="$(id -u)"
if [[ ! "${runner_uid}" =~ ^[0-9]+$ ]]; then
echo "::error::failed to resolve a numeric runner UID."
exit 1
fi
sudo setfacl -m "u:${runner_uid}:rw" /dev/kvm

if [[ ! -r /dev/kvm || ! -w /dev/kvm ]]; then
echo "::error::failed to grant the runner user read/write access to /dev/kvm."
exit 1
fi

acl_output="$(getfacl -ncp /dev/kvm | sed 's/[[:space:]]#effective:.*$//' || true)"
if [[ -z "${acl_output}" ]]; then
echo "::error::failed to read /dev/kvm ACLs for verification."
exit 1
fi
if ! grep -Eq "^user:${runner_uid}:rw-$" <<<"${acl_output}"; then
echo "::error::failed to verify scoped ACL entry for the runner user on /dev/kvm."
exit 1
fi

echo "runner user has scoped read/write access to /dev/kvm"
echo "::endgroup::"
41 changes: 40 additions & 1 deletion actions/setup/sh/cloud_hypervisor_setup_bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,26 @@ curl -fsSL -o "${bundle_root}/${manifest_name}" "${asset_base_url}/${manifest_na
echo "downloaded release assets"
echo "::endgroup::"

echo "::group::Validate cloud-hypervisor bundle archive structure"
archive_path="${bundle_root}/${asset_name}"
if tar -tzf "${archive_path}" | grep -E '(^/|(^|/)\.\.(/|$))' >/dev/null; then
echo "::error::cloud-hypervisor bundle contains unsafe archive paths"
exit 1
fi
archive_table="$(tar -tvzf "${archive_path}")"
if [[ -z "${archive_table}" ]]; then
echo "::error::cloud-hypervisor bundle archive is empty"
exit 1
fi
if grep -Eq '^[lh]' <<<"${archive_table}"; then
echo "::error::cloud-hypervisor bundle must not include symbolic or hard links"
exit 1
fi
echo "archive structure validated"
echo "::endgroup::"

echo "::group::Extract cloud-hypervisor bundle"
tar -xzf "${bundle_root}/${asset_name}" -C "${extract_dir}"
tar --no-same-owner --no-same-permissions -xzf "${archive_path}" -C "${extract_dir}"
echo "bundle extracted to ${extract_dir}"
echo "::endgroup::"

Expand Down Expand Up @@ -97,6 +115,21 @@ verify_sha256() {
fi
}

validate_extracted_file() {
local file="$1"
if [[ -z "${file}" || ! -f "${file}" || -L "${file}" ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] exit 1 inside validate_extracted_file terminates the entire shell script directly rather than returning a non-zero status to the caller. This is intentional here, but it prevents callers from wrapping the function in conditional logic and makes the function harder to unit-test in isolation.

💡 Suggestion

If the exit-on-failure behaviour is deliberate (consistent with the surrounding script style), add a brief comment explaining that. If reusability is desired, replace exit 1 with return 1 and let callers do validate_extracted_file "$x" || exit 1.

@copilot please address this.

echo "::error::invalid extracted cloud-hypervisor bundle file: ${file}"
exit 1
fi
local real_file real_extract_dir
real_file="$(realpath "${file}")"
real_extract_dir="$(realpath "${extract_dir}")"
if [[ "${real_file}" != "${real_extract_dir}"/* ]]; then
echo "::error::extracted bundle file is outside expected directory: ${file}"
exit 1
fi
}

# Artifact names are fixed by the gh-aw-firewall cloud-hypervisor release contract.
binary_rel="cloud-hypervisor"
kernel_rel="vmlinux.bin"
Expand All @@ -115,6 +148,12 @@ if [[ -z "${binary_path}" || -z "${kernel_path}" || -z "${rootfs_path}" || -z "$
exit 1
fi

validate_extracted_file "${binary_path}"
validate_extracted_file "${kernel_path}"
validate_extracted_file "${rootfs_path}"
validate_extracted_file "${supervisor_path}"
validate_extracted_file "${virtiofsd_path}"

if [[ "$(dirname "${binary_path}")" != "$(dirname "${virtiofsd_path}")" ]]; then
echo "::error::virtiofsd must be colocated with the cloud-hypervisor binary"
exit 1
Expand Down
6 changes: 3 additions & 3 deletions pkg/workflow/cloud_hypervisor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,15 +350,15 @@ func TestCloudHypervisorShellScriptContent(t *testing.T) {
}{
{
script: "cloud_hypervisor_kvm_access.sh",
contains: []string{"RUNNER_ENVIRONMENT", "github-hosted", "ImageOS", "setfacl", "u:${runner_uid}:rw", "/dev/kvm", "-r /dev/kvm", "-w /dev/kvm"},
contains: []string{"RUNNER_ENVIRONMENT", "github-hosted", "ImageOS", "setfacl", "u:${runner_uid}:rw", "/dev/kvm", "-c /dev/kvm", "getfacl -ncp /dev/kvm", "-r /dev/kvm", "-w /dev/kvm"},
},
{
script: "cloud_hypervisor_host_preflight.sh",
contains: []string{"RUNNER_ENVIRONMENT", "github-hosted", "ImageOS", "/dev/kvm", "cloud-hypervisor preview"},
contains: []string{"RUNNER_ENVIRONMENT", "github-hosted", "ImageOS", "/dev/kvm", "test -c /dev/kvm", "cloud-hypervisor preview"},
},
{
script: "cloud_hypervisor_setup_bundle.sh",
contains: []string{"cloud-hypervisor-test-x86_64.tar.gz", "cloud-hypervisor-test-x86_64.SHA256SUMS", "cloud-hypervisor-test-x86_64.manifest.json", "vmlinux.bin", "rootfs.ext4", "awf-supervisor", "virtiofsd", "virtiofsd_path=", "virtiofsd_sha256="},
contains: []string{"cloud-hypervisor-test-x86_64.tar.gz", "cloud-hypervisor-test-x86_64.SHA256SUMS", "cloud-hypervisor-test-x86_64.manifest.json", "archive structure validated", "tar --no-same-owner --no-same-permissions", "validate_extracted_file", "vmlinux.bin", "rootfs.ext4", "awf-supervisor", "virtiofsd", "virtiofsd_path=", "virtiofsd_sha256="},
},
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/workflow/compiler_validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ func (c *Compiler) emitGeneralToolWarnings(workflowData *WorkflowData, markdownP
"Unsupported hosts are rejected; gh-aw and AWF do not fall back to docker or gvisor."))
c.IncrementWarningCount()
}
if isCloudHypervisorRuntime(workflowData) {
fmt.Fprintln(os.Stderr, formatCompilerMessage(markdownPath, "warning",
"sandbox.agent.runtime: cloud-hypervisor uses a privileged KVM preview path with an attached MCP gateway topology. "+
"Require a human security review before merge or rollout, and record explicit approval in your change process."))
c.IncrementWarningCount()
}
if workflowData.SafeOutputs != nil && workflowData.SafeOutputs.AssignToAgent != nil &&
workflowData.SafeOutputs.GitHubApp != nil && workflowData.SafeOutputs.AssignToAgent.GitHubToken == "" {
fmt.Fprintln(os.Stderr, console.FormatWarningMessageStderr(
Expand Down
35 changes: 34 additions & 1 deletion pkg/workflow/compiler_validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ func TestEmitExperimentalFeatureWarningsGHAWDetection(t *testing.T) {
os.Stderr = w
t.Cleanup(func() {
os.Stderr = oldStderr
_ = w.Close()
_ = r.Close()
})

Expand All @@ -218,6 +217,40 @@ func TestEmitExperimentalFeatureWarningsGHAWDetection(t *testing.T) {
}
}

func TestEmitGeneralToolWarningsCloudHypervisorReviewTrigger(t *testing.T) {
compiler := NewCompiler()
workflowData := &WorkflowData{
SandboxConfig: &SandboxConfig{
Agent: &AgentSandboxConfig{
Runtime: AgentRuntimeCloudHypervisor,
},
},
}

oldStderr := os.Stderr
r, w, err := os.Pipe()
require.NoError(t, err)
os.Stderr = w
t.Cleanup(func() {
os.Stderr = oldStderr
_ = r.Close()
})

compiler.emitGeneralToolWarnings(workflowData, "test.md")

require.NoError(t, w.Close())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] w is closed twice — once in t.Cleanup (line 236) and again here — which can race or double-close the pipe's write end, causing an unpredictable test panic.

💡 Fix

Remove the explicit w.Close() at line 242 and let t.Cleanup own the pipe lifecycle. Read the pipe output inside t.Cleanup after closing w:

var buf bytes.Buffer
t.Cleanup(func() {
    _ = w.Close()
    _, _ = io.Copy(&buf, r)
    _ = r.Close()
    os.Stderr = oldStderr
})
compiler.emitGeneralToolWarnings(workflowData, "test.md")
// assertions moved here, but buf must be populated in Cleanup above

This mirrors the pattern the adjacent test already uses after its own cleanup fix in this diff.

@copilot please address this.

os.Stderr = oldStderr

var buf bytes.Buffer
_, err = io.Copy(&buf, r)
require.NoError(t, err)
stderrOutput := buf.String()

assert.Contains(t, stderrOutput, "sandbox.agent.runtime: cloud-hypervisor uses a privileged KVM preview path")
assert.Contains(t, stderrOutput, "Require a human security review before merge or rollout")
assert.Equal(t, 1, compiler.GetWarningCount())
}

// TestValidatePermissions tests permission parsing and MCP tool constraint validation.
func TestValidatePermissions(t *testing.T) {
tests := []struct {
Expand Down
Loading