Skip to content

Commit f2c5fde

Browse files
authored
fix(setup): clean up root-owned /tmp/awf-*-chroot-home directories (#41852)
1 parent c7c642f commit f2c5fde

5 files changed

Lines changed: 220 additions & 4 deletions

File tree

.changeset/patch-fix-awf-chroot-home-cleanup.md

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/clean.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,21 @@ if [ -d "${tmpDir}" ]; then
4040
echo "Warning: failed to clean up ${tmpDir}" >&2
4141
fi
4242
fi
43+
44+
# Remove AWF chroot home directories under /tmp (e.g. /tmp/awf-*-chroot-home).
45+
# These are created by AWF when running with --enable-host-access on GitHub-hosted runners.
46+
# Files inside may be owned by root (written by Docker containers or privileged AWF processes),
47+
# causing EACCES failures if cleanup is attempted without sudo.
48+
if awf_chroot_home_dirs="$(sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d -print 2>/dev/null)"; then
49+
if [ -n "${awf_chroot_home_dirs}" ]; then
50+
if sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d -exec rm -rf -- {} + 2>/dev/null; then
51+
echo "Cleaned up /tmp/awf-*-chroot-home directories (sudo)"
52+
else
53+
echo "Warning: failed to clean /tmp/awf-*-chroot-home directories" >&2
54+
fi
55+
else
56+
echo "No /tmp/awf-*-chroot-home directories found"
57+
fi
58+
else
59+
echo "Warning: unable to inspect /tmp/awf-*-chroot-home directories with sudo" >&2
60+
fi
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import fs from "fs";
3+
import os from "os";
4+
import path from "path";
5+
import { spawnSync } from "child_process";
6+
import { fileURLToPath } from "url";
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
9+
const POST_SCRIPT_PATH = path.join(__dirname, "..", "post.js");
10+
const CLEAN_SCRIPT_PATH = path.join(__dirname, "..", "clean.sh");
11+
12+
const tempDirs = [];
13+
14+
function createTempDir(prefix) {
15+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
16+
tempDirs.push(dir);
17+
return dir;
18+
}
19+
20+
function createFakeSudoEnvironment() {
21+
const root = createTempDir("chroot-home-cleanup-");
22+
const fakeBin = path.join(root, "fake-bin");
23+
fs.mkdirSync(fakeBin, { recursive: true });
24+
25+
const logPath = path.join(root, "sudo.log");
26+
const fakeSudoPath = path.join(fakeBin, "sudo");
27+
fs.writeFileSync(
28+
fakeSudoPath,
29+
`#!/usr/bin/env bash
30+
set -euo pipefail
31+
echo "$*" >> "$FAKE_SUDO_LOG"
32+
if [ "$1" = "find" ]; then
33+
if printf '%s\\n' "$*" | grep -q -- '-print'; then
34+
printf '%b' "\${FAKE_FIND_PRINT_OUTPUT:-}"
35+
exit "\${FAKE_FIND_PRINT_STATUS:-0}"
36+
fi
37+
if printf '%s\\n' "$*" | grep -q -- '-exec'; then
38+
exit "\${FAKE_FIND_EXEC_STATUS:-0}"
39+
fi
40+
fi
41+
exit 0
42+
`,
43+
{ mode: 0o755 }
44+
);
45+
46+
return {
47+
fakeBin,
48+
logPath,
49+
root,
50+
};
51+
}
52+
53+
function runPostScript(env) {
54+
return spawnSync(process.execPath, [POST_SCRIPT_PATH], {
55+
encoding: "utf8",
56+
env: { ...process.env, ...env },
57+
});
58+
}
59+
60+
function runCleanScript(env) {
61+
return spawnSync("bash", [CLEAN_SCRIPT_PATH], {
62+
encoding: "utf8",
63+
env: { ...process.env, ...env },
64+
});
65+
}
66+
67+
afterEach(() => {
68+
while (tempDirs.length > 0) {
69+
const dir = tempDirs.pop();
70+
if (dir && fs.existsSync(dir)) {
71+
fs.rmSync(dir, { recursive: true, force: true });
72+
}
73+
}
74+
});
75+
76+
describe("post.js chroot-home cleanup", () => {
77+
it("logs that no directories were found when find output is empty", () => {
78+
const { fakeBin, logPath } = createFakeSudoEnvironment();
79+
const result = runPostScript({
80+
PATH: `${fakeBin}:${process.env.PATH}`,
81+
FAKE_SUDO_LOG: logPath,
82+
FAKE_FIND_PRINT_OUTPUT: "",
83+
});
84+
85+
expect(result.status).toBe(0);
86+
expect(result.stdout).toContain("No /tmp/awf-*-chroot-home directories found");
87+
expect(fs.readFileSync(logPath, "utf8")).not.toContain("-exec rm -rf -- {} +");
88+
});
89+
90+
it("logs count of cleaned chroot-home directories", () => {
91+
const { fakeBin, logPath } = createFakeSudoEnvironment();
92+
const result = runPostScript({
93+
PATH: `${fakeBin}:${process.env.PATH}`,
94+
FAKE_SUDO_LOG: logPath,
95+
FAKE_FIND_PRINT_OUTPUT: "/tmp/awf-1-chroot-home\n/tmp/awf-2-chroot-home\n",
96+
});
97+
98+
expect(result.status).toBe(0);
99+
expect(result.stdout).toContain("Cleaned up 2 /tmp/awf-*-chroot-home directories");
100+
expect(fs.readFileSync(logPath, "utf8")).toContain("-exec rm -rf -- {} +");
101+
});
102+
});
103+
104+
describe("clean.sh chroot-home cleanup", () => {
105+
it("logs when no chroot-home directories are found", () => {
106+
const { fakeBin, logPath, root } = createFakeSudoEnvironment();
107+
const destination = path.join(root, "destination");
108+
fs.mkdirSync(destination, { recursive: true });
109+
110+
const result = runCleanScript({
111+
PATH: `${fakeBin}:${process.env.PATH}`,
112+
FAKE_SUDO_LOG: logPath,
113+
FAKE_FIND_PRINT_OUTPUT: "",
114+
INPUT_DESTINATION: destination,
115+
});
116+
117+
expect(result.status).toBe(0);
118+
expect(result.stdout).toContain("No /tmp/awf-*-chroot-home directories found");
119+
expect(fs.readFileSync(logPath, "utf8")).not.toContain("-exec rm -rf -- {} +");
120+
});
121+
122+
it("logs successful cleanup when chroot-home directories are found", () => {
123+
const { fakeBin, logPath, root } = createFakeSudoEnvironment();
124+
const destination = path.join(root, "destination");
125+
fs.mkdirSync(destination, { recursive: true });
126+
127+
const result = runCleanScript({
128+
PATH: `${fakeBin}:${process.env.PATH}`,
129+
FAKE_SUDO_LOG: logPath,
130+
FAKE_FIND_PRINT_OUTPUT: "/tmp/awf-1-chroot-home\n",
131+
INPUT_DESTINATION: destination,
132+
});
133+
134+
expect(result.status).toBe(0);
135+
expect(result.stdout).toContain("Cleaned up /tmp/awf-*-chroot-home directories (sudo)");
136+
expect(fs.readFileSync(logPath, "utf8")).toContain("-exec rm -rf -- {} +");
137+
});
138+
});

actions/setup/post.js

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const { spawnSync } = require("child_process");
1414
const fs = require("fs");
1515

1616
function isDebugModeEnabled() {
17-
const toBool = (value) => {
17+
const toBool = value => {
1818
const normalized = String(value || "").toLowerCase();
1919
return normalized === "1" || normalized === "true";
2020
};
@@ -64,9 +64,7 @@ function listTmpGhAwFiles(tmpDir, maxDepth, maxFiles) {
6464
walk(tmpDir, 0);
6565

6666
const truncated = files.length >= maxFiles;
67-
console.log(
68-
`[debug] listing files under ${tmpDir} (max depth ${maxDepth}, max files ${maxFiles})`,
69-
);
67+
console.log(`[debug] listing files under ${tmpDir} (max depth ${maxDepth}, max files ${maxFiles})`);
7068
if (files.length === 0) {
7169
console.log("[debug] no files found");
7270
} else {
@@ -121,4 +119,52 @@ function listTmpGhAwFiles(tmpDir, maxDepth, maxFiles) {
121119
console.error(`Warning: failed to clean up ${tmpDir}: ${err.message}`);
122120
}
123121
}
122+
123+
// Clean up AWF chroot home directories under /tmp (e.g. /tmp/awf-*-chroot-home).
124+
// These are created by AWF when running with --enable-host-access on GitHub-hosted runners.
125+
// Files inside may be owned by root (written by Docker containers or privileged AWF processes),
126+
// causing EACCES failures if cleanup is attempted without sudo.
127+
const awfChrootHomeFindResult = spawnSync(
128+
"sudo",
129+
["find", "/tmp", "-maxdepth", "1", "-name", "awf-*-chroot-home", "-type", "d", "-print"],
130+
{ encoding: "utf8" }
131+
);
132+
if (awfChrootHomeFindResult.status !== 0) {
133+
console.log("Failed to inspect /tmp/awf-*-chroot-home directories");
134+
} else {
135+
const awfChrootHomeDirs = awfChrootHomeFindResult.stdout
136+
.split("\n")
137+
.map(line => line.trim())
138+
.filter(Boolean);
139+
if (awfChrootHomeDirs.length === 0) {
140+
console.log("No /tmp/awf-*-chroot-home directories found");
141+
} else {
142+
const awfChrootHomeCleanupResult = spawnSync(
143+
"sudo",
144+
[
145+
"find",
146+
"/tmp",
147+
"-maxdepth",
148+
"1",
149+
"-name",
150+
"awf-*-chroot-home",
151+
"-type",
152+
"d",
153+
"-exec",
154+
"rm",
155+
"-rf",
156+
"--",
157+
"{}",
158+
"+"
159+
],
160+
{ stdio: "inherit" }
161+
);
162+
if (awfChrootHomeCleanupResult.status === 0) {
163+
const awfChrootHomeNoun = awfChrootHomeDirs.length === 1 ? "directory" : "directories";
164+
console.log(`Cleaned up ${awfChrootHomeDirs.length} /tmp/awf-*-chroot-home ${awfChrootHomeNoun}`);
165+
} else {
166+
console.log("Failed to clean /tmp/awf-*-chroot-home directories");
167+
}
168+
}
169+
}
124170
})();

actions/setup/sh/install_copilot_cli.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ echo "Ensuring correct ownership of $COPILOT_DIR..."
4343
mkdir -p "$COPILOT_DIR"
4444
sudo chown -R "$(id -u):$(id -g)" "$COPILOT_DIR"
4545

46+
# Clean up any stale AWF chroot home directories left by previous runs.
47+
# When AWF ran with `sudo -E awf --enable-host-access`, it created
48+
# /tmp/awf-*-chroot-home directories with root-owned files. These cause
49+
# EACCES failures in the Copilot CLI cleanup path (rimrafSync) on the same or
50+
# subsequent runs, which reports as "engine terminated unexpectedly".
51+
# Remove them here before the agent starts so the runner is in a clean state.
52+
echo "Cleaning up stale AWF chroot home directories..."
53+
sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d -exec rm -rf -- {} + 2>/dev/null || true
54+
4655
# Detect OS and architecture
4756
OS="$(uname -s)"
4857
ARCH="$(uname -m)"

0 commit comments

Comments
 (0)