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