Skip to content

Commit a86febb

Browse files
authored
Merge pull request #44 from sysprog21/clone3
Add clone3 namespace-flag regression tests
2 parents 390495c + 0000de4 commit a86febb

2 files changed

Lines changed: 227 additions & 0 deletions

File tree

scripts/run-tests.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,38 @@ expect_output()
9393
rm -f "$OUTPUT"
9494
}
9595

96+
expect_output_count()
97+
{
98+
name="$1"
99+
expected="$2"
100+
expected_count="$3"
101+
shift 3
102+
printf " %-40s " "$name"
103+
OUTPUT=$(mktemp)
104+
if run_with_timeout "$@" > "$OUTPUT" 2>&1; then
105+
rc=0
106+
else
107+
rc=$?
108+
fi
109+
actual_count=$(grep -c "$expected" "$OUTPUT" || true)
110+
if [ "$rc" -eq 0 ] && [ "$actual_count" -eq "$expected_count" ]; then
111+
printf "${GREEN}PASS${NC}\n"
112+
PASS=$((PASS + 1))
113+
else
114+
if [ "$rc" -eq 124 ]; then
115+
printf "${RED}TIMEOUT${NC}\n"
116+
elif [ "$rc" -ne 0 ]; then
117+
printf "${RED}FAIL${NC} (exit=$rc, match_count=$actual_count/$expected_count)\n"
118+
else
119+
printf "${RED}FAIL${NC} (match_count=$actual_count/$expected_count)\n"
120+
fi
121+
echo " expected pattern: ${expected}"
122+
head -20 "$OUTPUT" | sed 's/^/ /'
123+
FAIL=$((FAIL + 1))
124+
fi
125+
rm -f "$OUTPUT"
126+
}
127+
96128
echo "=== kbox integration tests ==="
97129
echo " binary: ${KBOX}"
98130
echo " rootfs: ${ROOTFS}"
@@ -277,6 +309,16 @@ for test_prog in dup-test clock-test signal-test path-escape-test errno-test; do
277309
fi
278310
done
279311

312+
if "$KBOX" image -S "$ROOTFS" -- /bin/sh -c "test -x /opt/tests/clone3-test" 2> /dev/null; then
313+
expect_output_count "clone3-test" \
314+
"kbox: clone3 denied: namespace flags" 9 \
315+
"$KBOX" image --forward-verbose -S "$ROOTFS" --syscall-mode=seccomp \
316+
-- "/opt/tests/clone3-test"
317+
else
318+
printf " %-40s ${YELLOW}SKIP${NC} (not in rootfs)\n" "clone3-test"
319+
SKIP=$((SKIP + 1))
320+
fi
321+
280322
# ---- Networking (requires --net / SLIRP support) ----
281323
echo ""
282324
echo "--- Networking ---"

tests/guest/clone3-test.c

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/* SPDX-License-Identifier: MIT */
2+
/* Guest test: verify that clone3 namespace-flag sanitization blocks
3+
* CLONE_NEW* flags, and that an unreadable clone_args struct triggers the
4+
* fail-closed EPERM path.
5+
*
6+
* The guest-side assertions intentionally stay within behavior visible inside
7+
* the sandbox. The integration harness runs this binary with verbose seccomp
8+
* mode and matches kbox's "clone3 denied: namespace flags" log line, so the
9+
* regression check does not rely on host-specific CLONE_NEWUSER policy.
10+
*/
11+
#include <errno.h>
12+
#include <signal.h>
13+
#include <stdint.h>
14+
#include <stdio.h>
15+
#include <stdlib.h>
16+
#include <string.h>
17+
#include <sys/syscall.h>
18+
#include <sys/wait.h>
19+
#include <unistd.h>
20+
21+
#define CHECK(cond, msg) \
22+
do { \
23+
if (!(cond)) { \
24+
fprintf(stderr, "FAIL: %s (%s)\n", msg, #cond); \
25+
exit(1); \
26+
} \
27+
} while (0)
28+
29+
/* Linux struct clone_args (UAPI, 11 fields, 88 bytes). kbox requires
30+
* kernel >= 5.13 for seccomp USER_NOTIF, so this layout is always valid.
31+
*/
32+
struct clone3_args {
33+
uint64_t flags;
34+
uint64_t pidfd;
35+
uint64_t child_tid;
36+
uint64_t parent_tid;
37+
uint64_t exit_signal;
38+
uint64_t stack;
39+
uint64_t stack_size;
40+
uint64_t tls;
41+
uint64_t set_tid;
42+
uint64_t set_tid_size;
43+
uint64_t cgroup;
44+
};
45+
46+
_Static_assert(sizeof(struct clone3_args) == 88,
47+
"clone3_args must match kernel CLONE_ARGS_SIZE_VER2 (88 bytes)");
48+
49+
#define CLONE3_ARGS_SIZE sizeof(struct clone3_args)
50+
51+
static long do_clone3(struct clone3_args *args, size_t size)
52+
{
53+
return syscall(__NR_clone3, args, size);
54+
}
55+
56+
static void reap_child(pid_t pid)
57+
{
58+
int status;
59+
pid_t wp;
60+
while ((wp = waitpid(pid, &status, 0)) < 0 && errno == EINTR)
61+
;
62+
if (wp < 0)
63+
fprintf(stderr, "warning: waitpid(%d) failed: %s\n", pid,
64+
strerror(errno));
65+
}
66+
67+
/* Each CLONE_NEW* flag that the supervisor must block. */
68+
struct flag_case {
69+
uint64_t flag;
70+
const char *name;
71+
};
72+
73+
static const struct flag_case namespace_flags[] = {
74+
{0x10000000ULL, "CLONE_NEWUSER"}, {0x00020000ULL, "CLONE_NEWNS"},
75+
{0x20000000ULL, "CLONE_NEWPID"}, {0x40000000ULL, "CLONE_NEWNET"},
76+
{0x04000000ULL, "CLONE_NEWUTS"}, {0x08000000ULL, "CLONE_NEWIPC"},
77+
{0x02000000ULL, "CLONE_NEWCGROUP"}, {0x00000080ULL, "CLONE_NEWTIME"},
78+
};
79+
80+
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
81+
82+
static void test_namespace_flags(void)
83+
{
84+
for (size_t i = 0; i < ARRAY_SIZE(namespace_flags); i++) {
85+
struct clone3_args args;
86+
memset(&args, 0, sizeof(args));
87+
args.flags = namespace_flags[i].flag;
88+
args.exit_signal = SIGCHLD;
89+
90+
errno = 0;
91+
long rc = do_clone3(&args, CLONE3_ARGS_SIZE);
92+
if (rc == 0)
93+
_exit(0);
94+
if (rc > 0) {
95+
reap_child(rc);
96+
fprintf(stderr, "FAIL: clone3(%s) succeeded (child %ld)\n",
97+
namespace_flags[i].name, rc);
98+
exit(1);
99+
}
100+
char msg[128];
101+
snprintf(msg, sizeof(msg),
102+
"clone3(%s) should return EPERM, got rc=%ld errno=%d",
103+
namespace_flags[i].name, rc, errno);
104+
CHECK(rc < 0, msg);
105+
snprintf(msg, sizeof(msg),
106+
"clone3(%s) errno should be EPERM (%d), got %d",
107+
namespace_flags[i].name, EPERM, errno);
108+
CHECK(errno == EPERM, msg);
109+
printf(" ok: clone3(%s) -> EPERM\n", namespace_flags[i].name);
110+
}
111+
}
112+
113+
static void test_combined_namespace_flags(void)
114+
{
115+
struct clone3_args args;
116+
memset(&args, 0, sizeof(args));
117+
args.flags = 0x10000000ULL | 0x00020000ULL | 0x20000000ULL;
118+
args.exit_signal = SIGCHLD;
119+
120+
errno = 0;
121+
long rc = do_clone3(&args, CLONE3_ARGS_SIZE);
122+
if (rc == 0)
123+
_exit(0);
124+
if (rc > 0) {
125+
reap_child(rc);
126+
fprintf(stderr, "FAIL: clone3(combined) succeeded (child %ld)\n", rc);
127+
exit(1);
128+
}
129+
CHECK(rc < 0, "clone3(NEWUSER|NEWNS|NEWPID) should fail");
130+
CHECK(errno == EPERM, "clone3(NEWUSER|NEWNS|NEWPID) errno should be EPERM");
131+
printf(" ok: clone3(combined namespace flags) -> EPERM\n");
132+
}
133+
134+
static void test_unreadable_clone_args(void)
135+
{
136+
/* Pass a bogus pointer that guest_mem_read (process_vm_readv) cannot
137+
* dereference. The supervisor must fail closed with EPERM rather than
138+
* falling through to CONTINUE.
139+
*/
140+
errno = 0;
141+
/* cppcheck-suppress intToPointerCast */
142+
long rc = syscall(__NR_clone3, (void *) 1, CLONE3_ARGS_SIZE);
143+
if (rc == 0)
144+
_exit(0);
145+
CHECK(rc < 0, "clone3(bogus pointer) should fail");
146+
char msg[128];
147+
snprintf(msg, sizeof(msg),
148+
"clone3(bogus pointer) errno should be EPERM, got %d", errno);
149+
CHECK(errno == EPERM, msg);
150+
printf(" ok: clone3(unreadable clone_args) -> EPERM\n");
151+
}
152+
153+
static void test_valid_clone3_succeeds(void)
154+
{
155+
/* Sanity: plain fork via clone3 (no namespace flags) must succeed. */
156+
struct clone3_args args;
157+
memset(&args, 0, sizeof(args));
158+
args.exit_signal = SIGCHLD;
159+
160+
long rc = do_clone3(&args, CLONE3_ARGS_SIZE);
161+
if (rc == 0)
162+
_exit(0);
163+
CHECK(rc > 0, "clone3(plain fork) should succeed and return child pid");
164+
int status = -1;
165+
pid_t wp;
166+
while ((wp = waitpid(rc, &status, 0)) < 0 && errno == EINTR)
167+
;
168+
CHECK(wp == rc, "waitpid should return child pid");
169+
CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0,
170+
"child should exit normally with status 0");
171+
printf(" ok: clone3(plain fork) -> pid %ld\n", rc);
172+
}
173+
174+
int main(void)
175+
{
176+
printf("--- clone3 namespace-flag regression tests ---\n");
177+
178+
test_namespace_flags();
179+
test_combined_namespace_flags();
180+
test_unreadable_clone_args();
181+
test_valid_clone3_succeeds();
182+
183+
printf("PASS: clone3_test\n");
184+
return 0;
185+
}

0 commit comments

Comments
 (0)