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 } 206