Skip to content

Commit 2a69962

Browse files
l0kodkees
authored andcommitted
samples/check-exec: Add an enlighten "inc" interpreter and 28 tests
Add a very simple script interpreter called "inc" that can evaluate two different commands (one per line): - "?" to initialize a counter from user's input; - "+" to increment the counter (which is set to 0 by default). It is enlighten to only interpret executable files according to AT_EXECVE_CHECK and the related securebits: # Executing a script with RESTRICT_FILE is only allowed if the script # is executable: ./set-exec -f -- ./inc script-exec.inc # Allowed ./set-exec -f -- ./inc script-noexec.inc # Denied # Executing stdin with DENY_INTERACTIVE is only allowed if stdin is an # executable regular file: ./set-exec -i -- ./inc -i < script-exec.inc # Allowed ./set-exec -i -- ./inc -i < script-noexec.inc # Denied # However, a pipe is not executable and it is then denied: cat script-noexec.inc | ./set-exec -i -- ./inc -i # Denied # Executing raw data (e.g. command argument) with DENY_INTERACTIVE is # always denied. ./set-exec -i -- ./inc -c "+" # Denied ./inc -c "$(<script-ask.inc)" # Allowed # To directly execute a script, we can update $PATH (used by `env`): PATH="${PATH}:." ./script-exec.inc # To execute several commands passed as argument: Add a complete test suite to check the script interpreter against all possible execution cases: make TARGETS=exec kselftest-install ./tools/testing/selftests/kselftest_install/run_kselftest.sh Cc: Al Viro <viro@zeniv.linux.org.uk> Cc: Christian Brauner <brauner@kernel.org> Cc: Kees Cook <keescook@chromium.org> Cc: Paul Moore <paul@paul-moore.com> Cc: Serge Hallyn <serge@hallyn.com> Signed-off-by: Mickaël Salaün <mic@digikod.net> Link: https://lore.kernel.org/r/20241212174223.389435-8-mic@digikod.net Signed-off-by: Kees Cook <kees@kernel.org>
1 parent 3e707b0 commit 2a69962

File tree

11 files changed

+451
-3
lines changed

11 files changed

+451
-3
lines changed

samples/Kconfig

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,9 @@ config SAMPLE_CHECK_EXEC
296296
depends on CC_CAN_LINK && HEADERS_INSTALL
297297
help
298298
Build a tool to easily configure SECBIT_EXEC_RESTRICT_FILE and
299-
SECBIT_EXEC_DENY_INTERACTIVE.
299+
SECBIT_EXEC_DENY_INTERACTIVE, and a simple script interpreter to
300+
demonstrate how they should be used with execveat(2) +
301+
AT_EXECVE_CHECK.
300302

301303
source "samples/rust/Kconfig"
302304

samples/check-exec/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
/inc
12
/set-exec

samples/check-exec/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: BSD-3-Clause
22

33
userprogs-always-y := \
4+
inc \
45
set-exec
56

67
userccflags += -I usr/include

samples/check-exec/inc.c

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
/*
3+
* Very simple script interpreter that can evaluate two different commands (one
4+
* per line):
5+
* - "?" to initialize a counter from user's input;
6+
* - "+" to increment the counter (which is set to 0 by default).
7+
*
8+
* See tools/testing/selftests/exec/check-exec-tests.sh and
9+
* Documentation/userspace-api/check_exec.rst
10+
*
11+
* Copyright © 2024 Microsoft Corporation
12+
*/
13+
14+
#define _GNU_SOURCE
15+
#include <errno.h>
16+
#include <linux/fcntl.h>
17+
#include <linux/prctl.h>
18+
#include <linux/securebits.h>
19+
#include <stdbool.h>
20+
#include <stdio.h>
21+
#include <stdlib.h>
22+
#include <string.h>
23+
#include <sys/prctl.h>
24+
#include <unistd.h>
25+
26+
/* Returns 1 on error, 0 otherwise. */
27+
static int interpret_buffer(char *buffer, size_t buffer_size)
28+
{
29+
char *line, *saveptr = NULL;
30+
long long number = 0;
31+
32+
/* Each command is the first character of a line. */
33+
saveptr = NULL;
34+
line = strtok_r(buffer, "\n", &saveptr);
35+
while (line) {
36+
if (*line != '#' && strlen(line) != 1) {
37+
fprintf(stderr, "# ERROR: Unknown string\n");
38+
return 1;
39+
}
40+
switch (*line) {
41+
case '#':
42+
/* Skips shebang and comments. */
43+
break;
44+
case '+':
45+
/* Increments and prints the number. */
46+
number++;
47+
printf("%lld\n", number);
48+
break;
49+
case '?':
50+
/* Reads integer from stdin. */
51+
fprintf(stderr, "> Enter new number: \n");
52+
if (scanf("%lld", &number) != 1) {
53+
fprintf(stderr,
54+
"# WARNING: Failed to read number from stdin\n");
55+
}
56+
break;
57+
default:
58+
fprintf(stderr, "# ERROR: Unknown character '%c'\n",
59+
*line);
60+
return 1;
61+
}
62+
line = strtok_r(NULL, "\n", &saveptr);
63+
}
64+
return 0;
65+
}
66+
67+
/* Returns 1 on error, 0 otherwise. */
68+
static int interpret_stream(FILE *script, char *const script_name,
69+
char *const *const envp, const bool restrict_stream)
70+
{
71+
int err;
72+
char *const script_argv[] = { script_name, NULL };
73+
char buf[128] = {};
74+
size_t buf_size = sizeof(buf);
75+
76+
/*
77+
* We pass a valid argv and envp to the kernel to emulate a native
78+
* script execution. We must use the script file descriptor instead of
79+
* the script path name to avoid race conditions.
80+
*/
81+
err = execveat(fileno(script), "", script_argv, envp,
82+
AT_EMPTY_PATH | AT_EXECVE_CHECK);
83+
if (err && restrict_stream) {
84+
perror("ERROR: Script execution check");
85+
return 1;
86+
}
87+
88+
/* Reads script. */
89+
buf_size = fread(buf, 1, buf_size - 1, script);
90+
return interpret_buffer(buf, buf_size);
91+
}
92+
93+
static void print_usage(const char *argv0)
94+
{
95+
fprintf(stderr, "usage: %s <script.inc> | -i | -c <command>\n\n",
96+
argv0);
97+
fprintf(stderr, "Example:\n");
98+
fprintf(stderr, " ./set-exec -fi -- ./inc -i < script-exec.inc\n");
99+
}
100+
101+
int main(const int argc, char *const argv[], char *const *const envp)
102+
{
103+
int opt;
104+
char *cmd = NULL;
105+
char *script_name = NULL;
106+
bool interpret_stdin = false;
107+
FILE *script_file = NULL;
108+
int secbits;
109+
bool deny_interactive, restrict_file;
110+
size_t arg_nb;
111+
112+
secbits = prctl(PR_GET_SECUREBITS);
113+
if (secbits == -1) {
114+
/*
115+
* This should never happen, except with a buggy seccomp
116+
* filter.
117+
*/
118+
perror("ERROR: Failed to get securebits");
119+
return 1;
120+
}
121+
122+
deny_interactive = !!(secbits & SECBIT_EXEC_DENY_INTERACTIVE);
123+
restrict_file = !!(secbits & SECBIT_EXEC_RESTRICT_FILE);
124+
125+
while ((opt = getopt(argc, argv, "c:i")) != -1) {
126+
switch (opt) {
127+
case 'c':
128+
if (cmd) {
129+
fprintf(stderr, "ERROR: Command already set");
130+
return 1;
131+
}
132+
cmd = optarg;
133+
break;
134+
case 'i':
135+
interpret_stdin = true;
136+
break;
137+
default:
138+
print_usage(argv[0]);
139+
return 1;
140+
}
141+
}
142+
143+
/* Checks that only one argument is used, or read stdin. */
144+
arg_nb = !!cmd + !!interpret_stdin;
145+
if (arg_nb == 0 && argc == 2) {
146+
script_name = argv[1];
147+
} else if (arg_nb != 1) {
148+
print_usage(argv[0]);
149+
return 1;
150+
}
151+
152+
if (cmd) {
153+
/*
154+
* Other kind of interactive interpretations should be denied
155+
* as well (e.g. CLI arguments passing script snippets,
156+
* environment variables interpreted as script). However, any
157+
* way to pass script files should only be restricted according
158+
* to restrict_file.
159+
*/
160+
if (deny_interactive) {
161+
fprintf(stderr,
162+
"ERROR: Interactive interpretation denied.\n");
163+
return 1;
164+
}
165+
166+
return interpret_buffer(cmd, strlen(cmd));
167+
}
168+
169+
if (interpret_stdin && !script_name) {
170+
script_file = stdin;
171+
/*
172+
* As for any execve(2) call, this path may be logged by the
173+
* kernel.
174+
*/
175+
script_name = "/proc/self/fd/0";
176+
/*
177+
* When stdin is used, it can point to a regular file or a
178+
* pipe. Restrict stdin execution according to
179+
* SECBIT_EXEC_DENY_INTERACTIVE but always allow executable
180+
* files (which are not considered as interactive inputs).
181+
*/
182+
return interpret_stream(script_file, script_name, envp,
183+
deny_interactive);
184+
} else if (script_name && !interpret_stdin) {
185+
/*
186+
* In this sample, we don't pass any argument to scripts, but
187+
* otherwise we would have to forge an argv with such
188+
* arguments.
189+
*/
190+
script_file = fopen(script_name, "r");
191+
if (!script_file) {
192+
perror("ERROR: Failed to open script");
193+
return 1;
194+
}
195+
/*
196+
* Restricts file execution according to
197+
* SECBIT_EXEC_RESTRICT_FILE.
198+
*/
199+
return interpret_stream(script_file, script_name, envp,
200+
restrict_file);
201+
}
202+
203+
print_usage(argv[0]);
204+
return 1;
205+
}

samples/check-exec/run-script-ask.inc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env sh
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
DIR="$(dirname -- "$0")"
5+
6+
PATH="${PATH}:${DIR}"
7+
8+
set -x
9+
"${DIR}/script-ask.inc"

samples/check-exec/script-ask.inc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env inc
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
?
5+
+

samples/check-exec/script-exec.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env inc
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
+

samples/check-exec/script-noexec.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env inc
2+
# SPDX-License-Identifier: BSD-3-Clause
3+
4+
+

tools/testing/selftests/exec/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ non-regular
1111
null-argv
1212
/check-exec
1313
/false
14+
/inc
1415
/load_address.*
1516
!load_address.c
1617
/recursion-depth
18+
/set-exec
1719
xxxxxxxx*
1820
pipe
1921
S_I*.test

tools/testing/selftests/exec/Makefile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ ALIGN_PIES := $(patsubst %,load_address.%,$(ALIGNS))
1010
ALIGN_STATIC_PIES := $(patsubst %,load_address.static.%,$(ALIGNS))
1111
ALIGNMENT_TESTS := $(ALIGN_PIES) $(ALIGN_STATIC_PIES)
1212

13-
TEST_PROGS := binfmt_script.py
13+
TEST_PROGS := binfmt_script.py check-exec-tests.sh
1414
TEST_GEN_PROGS := execveat non-regular $(ALIGNMENT_TESTS)
15-
TEST_GEN_PROGS_EXTENDED := false
15+
TEST_GEN_PROGS_EXTENDED := false inc set-exec script-exec.inc script-noexec.inc
1616
TEST_GEN_FILES := execveat.symlink execveat.denatured script subdir
1717
# Makefile is a run-time dependency, since it's accessed by the execveat test
1818
TEST_FILES := Makefile
@@ -26,6 +26,8 @@ EXTRA_CLEAN := $(OUTPUT)/subdir.moved $(OUTPUT)/execveat.moved $(OUTPUT)/xxxxx*
2626

2727
include ../lib.mk
2828

29+
CHECK_EXEC_SAMPLES := $(top_srcdir)/samples/check-exec
30+
2931
$(OUTPUT)/subdir:
3032
mkdir -p $@
3133
$(OUTPUT)/script: Makefile
@@ -45,3 +47,11 @@ $(OUTPUT)/load_address.static.0x%: load_address.c
4547
-fPIE -static-pie $< -o $@
4648
$(OUTPUT)/false: false.c
4749
$(CC) $(CFLAGS) $(LDFLAGS) -static $< -o $@
50+
$(OUTPUT)/inc: $(CHECK_EXEC_SAMPLES)/inc.c
51+
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
52+
$(OUTPUT)/set-exec: $(CHECK_EXEC_SAMPLES)/set-exec.c
53+
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
54+
$(OUTPUT)/script-exec.inc: $(CHECK_EXEC_SAMPLES)/script-exec.inc
55+
cp $< $@
56+
$(OUTPUT)/script-noexec.inc: $(CHECK_EXEC_SAMPLES)/script-noexec.inc
57+
cp $< $@

0 commit comments

Comments
 (0)