From 8c0eed4fa95d5c6e4bd2b69830dd0a6c5ef6f29c Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Thu, 21 Aug 2025 06:44:44 +0000 Subject: [PATCH 1/4] Add support for debug messages Add support for printing debug messages when DEBUG is enbaled at build time. For easier usage, the make targets urunit_static_debug and urunit_dynamic_debug, produce binaries with debug messages enabled. Furthermore, add a few debug messages in the whole execution. Signed-off-by: Charalampos Mainas --- Makefile | 20 ++++++++++++++++++-- README.md | 3 +++ main.c | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f59462f..4eb2afb 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,10 @@ BUILD_DIR ?= ${CURDIR}/dist PREFIX ?= /usr/local/bin # Binary variables -URUNIT_BIN_DYNAMIC:= $(BUILD_DIR)/urunit_dynamic -URUNIT_BIN_STATIC := $(BUILD_DIR)/urunit_static +URUNIT_BIN_DYNAMIC := $(BUILD_DIR)/urunit_dynamic +URUNIT_BIN_DYNAMIC_DEBUG := $(BUILD_DIR)/urunit_dynamic_debug +URUNIT_BIN_STATIC := $(BUILD_DIR)/urunit_static +URUNIT_BIN_STATIC_DEBUG := $(BUILD_DIR)/urunit_static_debug # Compiler variables #? CC the compiler to use (default: gcc) @@ -69,19 +71,33 @@ prepare: $(BUILD_DIR) .PHONY: static static: $(URUNIT_BIN_STATIC) +## static_debug Build urunit statically-linked for host arch enabling debug logs. +.PHONY: static_debug +static_debug: $(URUNIT_BIN_STATIC_DEBUG) + ## dynamic Build urunit dynamically-linked for host arch. .PHONY: dynamic dynamic: $(URUNIT_BIN_DYNAMIC) +## dynamic_debug Build urunit dynamically-linked for host arch enabling debug logs. +.PHONY: dynamic_debug +dynamic_debug: $(URUNIT_BIN_DYNAMIC_DEBUG) + $(BUILD_DIR): mkdir -p $@ $(URUNIT_BIN_DYNAMIC): $(URUNIT_SRC) | prepare $(CC) $(CFLAGS) $(URUNIT_SRC) $(LDFLAGS) -o $@ +$(URUNIT_BIN_DYNAMIC_DEBUG): $(URUNIT_SRC) | prepare + $(CC) $(CFLAGS) -DDEBUG $(URUNIT_SRC) $(LDFLAGS) -o $@ + $(URUNIT_BIN_STATIC): $(URUNIT_SRC) | prepare $(CC) $(CFLAGS) $(URUNIT_SRC) $(LDFLAGS) $(STATIC_LDFLAGS) -o $@ +$(URUNIT_BIN_STATIC_DEBUG): $(URUNIT_SRC) | prepare + $(CC) $(CFLAGS) -DDEBUG $(URUNIT_SRC) $(LDFLAGS) $(STATIC_LDFLAGS) -o $@ + ## install Install urunit in PREFIX .PHONY: install install: $(INSTALL_DEPS) diff --git a/README.md b/README.md index 51c3ec1..aaa49f8 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,10 @@ Building `urunit` is as simple as running `make`, but make sure that `make` and a C compiler is installed. In particular: - `make static`: builds `urunit` as a statically-linked binary +- `make static_debug`: builds `urunit` as a statically-linked binary enabling debug messages - `make dynamic`: builds `urunit` as a dynamically-linked binary +- `make dynamic_debug`: builds `urunit` as a dynamically-linked binary enabling debug messages + > **NOTE**: The default build target is `make static`, hence running `make` > will build `urunit` statically. diff --git a/main.c b/main.c index 2a0fd32..3a88053 100644 --- a/main.c +++ b/main.c @@ -61,6 +61,20 @@ #define STATUS_MIN 0 #define ETH0_IF "eth0" +#ifdef DEBUG +#define SHOW_DEBUG 1 +#else +#define SHOW_DEBUG 0 +#endif + +#define DEBUG_PRINTF(fmt, ...) \ + do { if (SHOW_DEBUG) fprintf(stderr, "[DEBUG] " fmt, __VA_ARGS__); } while (0) + +#define DEBUG_PRINT(fmt, ...) \ + do { if (SHOW_DEBUG) fprintf(stderr, "[DEBUG] " fmt); } while (0) + +extern char **environ; + int isolate_child(void) { int ret = 0; sigset_t set; @@ -212,6 +226,20 @@ int spawn_app(int argc, char *argv[], pid_t *child_pid) { } new_argv[new_argc] = NULL; + if (new_argc <= 0 || new_argv[0] == NULL) { + fprintf(stderr, "No application execute\n"); + return 1; + } +#ifdef DEBUG + printf("Starting app %s with the following arguments\n", new_argv[0]); + for (int i = 1; i < new_argc; i++) { + printf("%s\n", new_argv[i]); + } + printf("Environment variables\n"); + for (char **env = environ; *env != NULL; env++) { + printf("%s\n", *env); + } +#endif pid = fork(); if (pid < 0) { perror("fork"); @@ -219,6 +247,7 @@ int spawn_app(int argc, char *argv[], pid_t *child_pid) { } else if (pid == 0) { int status = 1; + DEBUG_PRINT("Isolating child\n"); // Put the child in a process group and // make it the foreground process if there is a tty. if (isolate_child()) { @@ -265,19 +294,23 @@ int reap(const pid_t child_pid, int *child_exitcode_ptr) { case 0: break; default: + DEBUG_PRINTF("Reaped process %d ", reaped_pid); // A child was reaped. Check whether it's the app. // If it is, then set the exit_code, if (reaped_pid == child_pid) { if (WIFEXITED(reaped_status)) { + DEBUG_PRINTF("with exit status %d\n", WEXITSTATUS(reaped_status)); // The app exited normally *child_exitcode_ptr = WEXITSTATUS(reaped_status); } else if (WIFSIGNALED(reaped_status)) { + DEBUG_PRINTF("with exit status %d\n", WTERMSIG(reaped_status)); /* The app was terminated. Emulate what sh / bash * would do, which is to return * 128 + signal number. */ *child_exitcode_ptr = 128 + WTERMSIG(reaped_status); } else { + DEBUG_PRINT("with unknown exit status\n"); return 1; } @@ -320,6 +353,7 @@ int set_default_route() { addr.sin_addr.s_addr = 0; memcpy(&rt.rt_gateway, &addr, sizeof(addr)); + DEBUG_PRINT("Setting default route to eth0\n"); rt.rt_flags = RTF_UP; // TODO: We might want to doscover or somehow // get the interface as a parameter. @@ -342,24 +376,28 @@ int main(int argc, char *argv[]) { should_set_def_route = getenv("URUNIT_DEFROUTE"); if (should_set_def_route) { + DEBUG_PRINT("URUNIT_DEFROUTE was set\n"); ret = set_default_route(); if (ret != 0) { fprintf(stderr, "Failed to set default route\n"); } } + DEBUG_PRINT("Setting subreaper\n"); ret = prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0); if (ret < 0) { perror("Become subreaper"); return 1; } + DEBUG_PRINT("Spawn the app\n"); ret = spawn_app(argc, argv, &app_pid); if (ret) { fprintf(stderr, "Could not spawn app\n"); return ret; } + DEBUG_PRINT("Starting reaping loop\n"); while (1) { ret = reap(app_pid, &app_exitcode); if (ret) { @@ -372,6 +410,7 @@ int main(int argc, char *argv[]) { } } + DEBUG_PRINT("Exiting, will reboot in order to shutdown\n"); sync(); syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_CMD_RESTART, NULL); From eb29c4687b20e1895857fff7cafc1284e1435e9b Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Thu, 21 Aug 2025 11:41:56 +0000 Subject: [PATCH 2/4] Small refactor of child handling Separate the logic of the child, after the fork, in a separate function. Signed-off-by: Charalampos Mainas --- main.c | 53 +++++++++++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/main.c b/main.c index 3a88053..c44960e 100644 --- a/main.c +++ b/main.c @@ -123,6 +123,34 @@ int isolate_child(void) { return 0; } +int child_func(char *argv[]) { + int status = 1; + + DEBUG_PRINT("Isolating child\n"); + // Put the child in a process group and + // make it the foreground process if there is a tty. + if (isolate_child()) { + return 1; + } + + execvp(argv[0], argv); + + // execvp will only return on an error so make sure that we check the errno + // and exit with the correct return status for the error that we encountered + // See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF + switch (errno) { + case ENOENT: + status = 127; + break; + case EACCES: + status = 126; + break; + } + perror("execv failed"); + + return status; +} + int spawn_app(int argc, char *argv[], pid_t *child_pid) { int i = 0; pid_t pid; @@ -245,30 +273,7 @@ int spawn_app(int argc, char *argv[], pid_t *child_pid) { perror("fork"); return 1; } else if (pid == 0) { - int status = 1; - - DEBUG_PRINT("Isolating child\n"); - // Put the child in a process group and - // make it the foreground process if there is a tty. - if (isolate_child()) { - return 1; - } - - execvp(new_argv[0], new_argv); - - // execvp will only return on an error so make sure that we check the errno - // and exit with the correct return status for the error that we encountered - // See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF - switch (errno) { - case ENOENT: - status = 127; - break; - case EACCES: - status = 126; - break; - } - perror("execv failed"); - return status; + return child_func(new_argv); } else { *child_pid = pid; return 0; From a88264372a4b4ca39dd8bf927d5aa00ad1c5f3bc Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Wed, 20 Aug 2025 18:39:49 +0000 Subject: [PATCH 3/4] Add support for reading environment variables Add support for reading the environment variables from a config file which is defined by the environment variable "URUNIT_CONFIG". If this environment variable is not set, then urunit follows the previous execution path and execs the app using the existing environment variables. If the environment variable sis set then it should point to the configuration file with the environment variables. The environment variables are in a list which starts with the special string "UES" and ends with the special string "UEE". For instance a valid list is the following: UES PATH=/bin MYENV=myvalue UEE urunit will set these environment variables for the execution of the application and in case PATH was set, it will use this environment variable to search for the full path othe application's binary. Signed-off-by: Charalampos Mainas --- main.c | 525 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 516 insertions(+), 9 deletions(-) diff --git a/main.c b/main.c index c44960e..d817956 100644 --- a/main.c +++ b/main.c @@ -47,6 +47,8 @@ #include #include #include +#include +#include #include #include @@ -73,6 +75,11 @@ #define DEBUG_PRINT(fmt, ...) \ do { if (SHOW_DEBUG) fprintf(stderr, "[DEBUG] " fmt); } while (0) +struct app_exec_config { + char **envs; + char *path_env; +}; + extern char **environ; int isolate_child(void) { @@ -123,20 +130,481 @@ int isolate_child(void) { return 0; } -int child_func(char *argv[]) { +// ensure_dir: Makes sure a directory exists and if not it creates it. +// +// Arguments: +// 1. path: The directory to check and create if does not exist +// +// Return value: +// It returns 0 in success. Otherwise it returns 1. +int ensure_dir(const char *path) { + struct stat st; + int ret = 0; + + // Check if the path exists + ret = stat(path, &st); + if (ret == 0) { + if (S_ISDIR(st.st_mode)) { + DEBUG_PRINTF("Directory %s already exists.\n", path); + return 0; + } + fprintf(stderr, "'%s' exists but is not a directory.\n", path); + return -1; + } + + // Since it does not exist create it. + ret = mkdir(path, 0555); + if (ret != 0) { + perror("mkdir"); + return -1; + } + DEBUG_PRINTF("Created directory %s\n", path); + + return 0; +} + +// mount_special_fs: Mounts the special filesystems procfs and sysfs in /proc and +// /sys respectively. +// +// Arguments: +// No arguments. +// +// Return value: +// It returns 0 in success. Otherwise it returns 1. +int mount_special_fs() { + int ret = 0; + + ret = ensure_dir("/proc"); + if (ret < 0) { + return 1; + } + ret = mount("proc", "/proc", "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL); + if (ret < 0) { + perror("mount /proc"); + return 1; + } + + ret = ensure_dir("/sys"); + if (ret < 0) { + return 1; + } + ret = mount("sysfs", "/sys", "sysfs", 0, NULL); + if (ret < 0) { + perror("mount /sys"); + return 1; + } + + return 0; +} + +// read_exact_size: Reads exactly sz bytes from a file. It returns a +// dynamically allocated memory and the caller is responsible to free it. +// +// Arguments: +// 1. f: The pointer to a FILE +// 2. sz: The amount of bytes to read +// +// Return value: +// On success it returns a buffer of sz size with all bytes read. +// On failure, it returns NULL. +char *read_exact_size(FILE *f, size_t sz) { + size_t total_read = 0; + size_t bytes_read = 0; + char *buffer = NULL; + + buffer = malloc(sz); + if (!buffer) { + fprintf(stderr, "Failed to allocate memory for file contents\n"); + return NULL; + } + + while (total_read < sz) { + bytes_read = fread(buffer + total_read, 1, sz - total_read, f); + // the retrun value of fread does not distinguish between EOF and + // an error. Therefore, we have to use feof and ferror. + if (bytes_read == 0) { + if (feof(f)) { + // No more bytes to read. + break; + } else if (ferror(f)) { + fprintf(stderr, "Failed to read file data at offset %zu\n", total_read); + goto read_exact_error; + } + } + total_read += bytes_read; + } + + // We are out of the loop so we read as much bytes the caller asked + // or we reached the EOF. Check which of the two happened. + if (total_read != sz) { + fprintf(stderr, "Read %zu bytes, expected %zu bytes\n", total_read, sz); + goto read_exact_error; + } + + return buffer; + +read_exact_error: + free(buffer); + return NULL; +} + +// read_file_and_size: Reads the file from arguments and returns buffer +// with all the contents of the file. Furthermore, it stores in the size argument +// the total size of the file. +// +// Arguments: +// 1. file: The file to read +// 2. size: The total size of the file +// +// Return value: +// On success it returns a buffer with all the contents of the file and updates the +// size argument to contain the total size of the file. +// On failure, it returns NULL. +char *read_file_and_size(char *file, size_t *size) { + FILE *fp = NULL; + struct stat st = { 0 }; + int ret = 0; + char *buf = NULL; + + DEBUG_PRINTF("Read configuration file %s\n", file); + fp = fopen(file, "rb"); + if (!fp) { + perror("Read configuration file"); + return NULL; + } + + // Find the total size of the file in order to read the whole file + // and have a limit to search in the buffer. + ret = fstat(fileno(fp), &st); + if (ret != 0) { + perror("Getting configuration file size"); + goto exit_read_file; + } + DEBUG_PRINTF("Total size of configuration file %ld\n", st.st_size); + + // Make sure to read the whole file in one buffer. + buf = read_exact_size(fp, st.st_size); + if (!buf) { + fprintf(stderr, "Could not read whole configuration file\n"); + goto exit_read_file; + } + DEBUG_PRINTF("Contents of configuration file\n%s\n", buf); + *size = st.st_size; + +exit_read_file: + fclose(fp); + return buf; +} + +// measure_tokens: Measures how many tokens found in a string, searching at most +// max_size bytes. +// +// Arguments: +// 1. str_buf: The string to search at +// 2. max_size: The max size of bytes to llok at the string +// 3. tok: The character to search for. +// +// Return value: +// It returns the number of times the character was found. +size_t measure_tokens(char *str_buf, size_t max_size, char tok) { + size_t i = 0; + size_t cnt = 0; + + // Keep searching till we reach the max_size or + // the end of string '\0' + while (i < max_size && str_buf[i] != 0) { + if (str_buf[i] == tok) { + cnt++; + } + i++; + } + + return cnt; +} + +// parse_envs: Parses a list with one string in every line. The list should begin +// with the special string "UES" and each line should contain an environment +// variable. The last line in the list should be the special string "UEE" +// Given such a list, it constructs an array of pointers to strings where each +// pointer points to a single environment variable. The array is properly +// formatted to be passed as the environment variables table at execve and friends. +// It is important to note, that this function will alter the given list, +// replacing the new line characters with the end of string '\0' character. +// The funtion returns a dynamically allocated memory for storing the environment +// variables array and the caller is responsible to free that memory. +// +// Arguments: +// 1. string_area: The list with in the aformentioned format. If this function +// returns successfully, then this pointer will move after the end +// of the environment variable list, passed the end of The +// "UEE" string. +// 2. max_sz: The max possible size of the list. +// 3. path_env: A pointer to a string where a pointer to the PATH environment +// variable will get stored (if it is found). +// +// Return value: +// It returns an array of strings, where each row points +// to a single environment variable inside the initial list. +// Also, if the environment variable PATH was found, then path_env +// will point to the beginning of that string inside the list. +char **parse_envs(char **string_area, size_t max_sz, char **path_env) { + size_t total_envs = 0; + char **env_vars = NULL; + uint8_t path_found = 0; + char *tmp_env = NULL; + size_t i = 0; + + // Search how many new line characters we have in the list. + total_envs = measure_tokens(*string_area, max_sz, '\n'); + DEBUG_PRINTF("Found %ld total lines in the environment variables list\n", total_envs); + + // The list starts with "UES" + // which will not be stored and therefore, we can use this extra + // pointer for the end of the table (NULL), as execve and friends require. + // NOTE: If the list contains "UEE", we allocate one more pointer that + // is never used. + env_vars = malloc(total_envs * sizeof(char *)); + if (!env_vars) { + fprintf(stderr, "Failed to allocate memory for environment variables\n"); + return NULL; + } + + tmp_env = strtok(*string_area, "\n"); + // Discard the first string since it is the special string "UES" + // Also, it is safe to call strtok, even if there was no '\n', since it will + // return NULL again. + tmp_env = strtok(NULL, "\n"); + while (tmp_env && i < total_envs) { + // Check if we reached the end of the environment variable list + if (memcmp(tmp_env, "UEE", 3) == 0) { + *string_area = tmp_env + 4; // 4 bytes for the "UEE" string + break; + } + // Store the environment variable + DEBUG_PRINTF("Found env %s\n", tmp_env); + env_vars[i] = tmp_env; + // If we have not found PATH yet, + // check if the current environment variable is PATH. + if (!path_found) { + if (memcmp(tmp_env, "PATH=", 5) == 0) { + DEBUG_PRINTF("Found PATH env %s\n", tmp_env); + *path_env = tmp_env; + path_found = 1; + } + } + i++; + tmp_env = strtok(NULL, "\n"); + } + // Special case where malloc did not return NULL with 0 size, + // or no strings with '\n' found after the first occurance of '\n'. + // Both cases mean that we have no environment variables and hence + // we should return NULL. + if (i == 0) { + // free is safe here, since env_vars come from malloc and + // contains either NULL or address. Both cases are fine for free. + free(env_vars); + return NULL; + } + // Add nULL to indicate the end of the table with environment variables. + env_vars[i] = NULL; + + return env_vars; +} + +// get_config_from_file: Reads the contents of argumen and it parses the +// app execution configuration and environment variables list. +// The app execution configuration list starts with the line "UCS" and ends with the +// line "UCE". Respectively, the environment variable list, starts with the "UES" line +// and ends with the line "UES". +// +// Arguments: +// 1. file: The name of the file that contains the configuration. +// 2. sbuf: The variable that will hold the address of the allocated memory +// that was used to read the configuration file. The caller is +// responsible to free it. +// +// Return value: +// On success it returns a pointer to an instance of a struct app_exec_config +// ehich contains all the respective information for setting app the execution +// environment of the application. +struct app_exec_config *get_config_from_file(char *file, char **sbuf) { + char **env_vars = NULL; + size_t size = 0; + char *buf = NULL; + char *path_env = NULL; + struct app_exec_config *econf = NULL; + char *conf_area = NULL; + + buf = read_file_and_size(file, &size); + if (!buf) { + fprintf(stderr, "Could not read file %s\n", file); + return NULL; + } + conf_area = buf; + + // Check if the special string "UES" is present + // which means that now starts the environment variable + // list. + if (memcmp(conf_area, "UES", 3) == 0) { + char *init_conf_area = conf_area; + // Extract the environment variables from the list + env_vars = parse_envs(&conf_area, size, &path_env); + if (!env_vars ) { + fprintf(stderr, "Warning: No environment variables found in the configuration\n"); + } + // If the list was properly formatted, ending with "UEE" + // then string_area should differ from init_string_area + // Otherwise, the list was not properly formatted and + // we abort the parsing. + if (conf_area == init_conf_area) { + fprintf(stderr, "Invalid format of environment variable list. \"UEE\" was not found\n"); + goto get_env_vars_error_free; + } + } + + econf = malloc(sizeof(struct app_exec_config)); + if (!econf) { + fprintf(stderr, "Could not allocate memory for app exec config struct\n"); + goto get_env_vars_error_free; + } + + *sbuf = buf; + econf->envs = env_vars; + econf->path_env = path_env; + return econf; + +get_env_vars_error_free: + free(buf); + return NULL; +} + +// manual_execvpe: Tries to implement in a simple way execvpe, since execvpe is +// only supported by glibc. The rational is to combine every path in env_path +// (which is the PATH) with the file_bin (the executable) and try to execve. +// If a combination does not succeed then we move to the next path in env_path +// +// Arguments: +// 1. env_path: A string containing the PATH environment variable with all possible +// directories to search for the executable. +// 2. file_bin: The basename of the executable. +// 3. argv: The arguments for the application. +// 4. env: The environment variables for the application. +// +// Return value: +// On success it will never return. Otherwise, a non-zero return value +// will get returned and errno will be set appropriately. +int manual_execvpe(const char *env_path, const char *file_bin, char *const argv[], char *const env[]) { int status = 1; + char *path_buf = NULL; + const char *cur_path_end = NULL; + const char *cur_path_start = NULL; + char *tmp_bin_path = NULL; + size_t env_path_len = 0; + size_t file_bin_len = 0; + + if (!env) { + DEBUG_PRINT("No environment variables were set, just execvp and use the current ones\n"); + // No environment variables were given. So, we can just + // use execvp. + execvp(file_bin, argv); + goto manual_exec_exit; + } - DEBUG_PRINT("Isolating child\n"); - // Put the child in a process group and - // make it the foreground process if there is a tty. - if (isolate_child()) { + if (*file_bin == '/' || env_path == NULL) { + DEBUG_PRINT("Binary has full path, therefore just execvp it\n"); + // The file to execute is an absolute path. + // Or there is no custom PATH to search for. + // Therefore, just try to execve the given file + execve(file_bin, argv, env); + goto manual_exec_exit; + } + + file_bin_len = strlen(file_bin); + env_path_len = strlen(env_path); + if (env_path_len <= 5) { + fprintf(stderr, "Invalid format of custom PATH environment variable"); + goto manual_exec_exit; + } + // Move past "PATH+" and get to its values + env_path += 5; + env_path_len -= 5; + + // Allocate memory for the temporary buffer where we will construct + // all combinations. The size should be: + // env_path_len + '/' +file_bin_len + '\0' + path_buf = malloc((env_path_len + file_bin_len + 2) * sizeof(char)); + if (!path_buf) { + fprintf(stderr, "Failed to allocate memory to search binary\n"); return 1; } - execvp(argv[0], argv); + // Store the basename of the executable in the end of the buffer + // and prepend the '/' character to prepare a concatination of a + // path from custom PATH and the basename of the executable + // This will reduce the copies, since we only change the directory + // that we try out each time. + tmp_bin_path = path_buf + env_path_len; + *(tmp_bin_path) = '/'; + memcpy(tmp_bin_path + 1, file_bin, file_bin_len); + *(tmp_bin_path + 1 + file_bin_len) = '\0'; + + // cur_path_start stores the beginning of the current path we try from custom PATH + cur_path_start = env_path; + + do { + char *path_attempt = NULL; + size_t tmp_path_size = 0; + + // cur_path_end stores the end of the current path we try from custom PATH + cur_path_end = strchr(cur_path_start, ':'); + if (!cur_path_end) { + // We reached the last path, but strchr return NULL, + // since the character was not found. Therefore, + // manually set the pointer to the end of the string. + cur_path_end = env_path + env_path_len; + } + tmp_path_size = cur_path_end - cur_path_start; + + // We copy right before the '/' character the current directory + // from custom PATH + path_attempt = (char *)memcpy(tmp_bin_path - tmp_path_size, + cur_path_start, + tmp_path_size); + + DEBUG_PRINTF("Trying %s\n", path_attempt); + execve(path_attempt, argv, env); + + // Execve failed, but check the reason + switch (errno) { + case EACCES: + // Permission denied and therefore, we can not execute + // the file we found. Try the next possible path. + // + // TODO: However, we might want to keep this error + // and report it if everything else fails, because the + // error will get overwritten from the last failure. + case ENOENT: + case ENOTDIR: + // The file or a directory in the path does not exist. + // Just move to the next possible path. + break; + default: + // For any other reason, just abort. + goto manual_exec_exit_free; + } + + // Discard the ':' character + cur_path_start = cur_path_end + 1; + + } while (cur_path_start < (env_path + env_path_len)); - // execvp will only return on an error so make sure that we check the errno - // and exit with the correct return status for the error that we encountered + // We could not execute the binary. +manual_exec_exit_free: + free(path_buf); +manual_exec_exit: + // execvp/execve will only return on an error so make sure that we check + // the errno and exit with the correct return status for the error + // that we encountered. // See: http://www.tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF switch (errno) { case ENOENT: @@ -146,11 +614,50 @@ int child_func(char *argv[]) { status = 126; break; } - perror("execv failed"); + // Just a trick to print the filename in the error. + fprintf(stderr, "exec %s ", file_bin); + perror("failed"); return status; } +int child_func(char *argv[]) { + char *config_file = NULL; + char *config_buf = NULL; + struct app_exec_config *app_envs = NULL; + int ret = 0; + + DEBUG_PRINT("Isolating child\n"); + // Put the child in a process group and + // make it the foreground process if there is a tty. + if (isolate_child()) { + return 1; + } + + // Check if we need to read any configuration for the app execution + config_file = getenv("URUNIT_CONFIG"); + if (config_file) { + // We need to mount sysfs to read the data from retained initrd + ret = mount_special_fs(); + if (ret != 0) { + fprintf(stderr, "Failed to mount special filesystems\n"); + return 1; + } + app_envs = get_config_from_file(config_file, &config_buf); + } + if (app_envs) { + ret = manual_execvpe(app_envs->path_env, argv[0], argv, app_envs->envs); + } else { + ret = manual_execvpe(NULL, argv[0], argv, NULL); + } + // If we returned something went wrong + free(config_buf); + free(app_envs->envs); + free(app_envs); + + return ret; +} + int spawn_app(int argc, char *argv[], pid_t *child_pid) { int i = 0; pid_t pid; From 02424c3c5574a91f6dbe290a50edfe363275cf67 Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Mon, 25 Aug 2025 14:17:34 +0000 Subject: [PATCH 4/4] add support for reading and setting uid,gid and wdir In a similar way with environment variables, read the execution environment configuration from the configuration file. The respective config for the execution environment should have the following format: UCS UID: GID: WD: UCE Given such a configuration, urunit will set the gid then the uid and at last switch to the working_directory. Signed-off-by: Charalampos Mainas --- main.c | 247 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 241 insertions(+), 6 deletions(-) diff --git a/main.c b/main.c index d817956..9b7347d 100644 --- a/main.c +++ b/main.c @@ -58,6 +58,7 @@ #include #include #include +#include #define STATUS_MAX 255 #define STATUS_MIN 0 @@ -75,9 +76,16 @@ #define DEBUG_PRINT(fmt, ...) \ do { if (SHOW_DEBUG) fprintf(stderr, "[DEBUG] " fmt); } while (0) +struct process_config { + uint32_t uid; + uint32_t gid; + char *wdir; +}; + struct app_exec_config { char **envs; char *path_env; + struct process_config *pr_conf; }; extern char **environ; @@ -411,6 +419,158 @@ char **parse_envs(char **string_area, size_t max_sz, char **path_env) { return env_vars; } +// get_uint_val: Converst the value of "KEY: VALUE" string to uint32_t +// +// Arguments: +// 1. str: The string to convert in the form "KEY: VAL" +// 2. value: A pointer to uint32_t where the converted value will get stored. +// +// Return value: +// On success 0 is returned and value contains the coverted value. +// On failure, -1 is returned and value stays intact. +int get_uint_val(char *str, uint32_t *value) { + size_t str_sz = strlen(str); + char *val_str = strchr(str, ':'); + unsigned long val = 0; + char *end = NULL; + + if (val_str == NULL) { + // We could not find the beginning of the value string. + fprintf(stderr, "Failed to find ':' character in %s\n", str); + return -1; + } + + // strchr will return a pointer to ':', but we need to move passed + // ':', hence +1 character. + if (val_str + 1 >= str + str_sz) { + // We can not go over the string. Something is wrong + fprintf(stderr, "Failed to find value after ':' in %s\n", str); + return -1; + } + val_str ++; + + // strtoul can take care of spaces. + val = strtoul(val_str, &end, 10); + if (errno == ERANGE || val > UINT32_MAX) { + perror("Convert string to uint32_t"); + return -1; + } + if (*end != '\0') { + fprintf(stderr, "Failed to convert %s to unit32_t. Got trailing character %c\n", val_str, *end); + return -1; + } + + *value = (uint32_t)val; + + return 0; +} + +// get_string_val: Returns the string value of "KEY: VALUE" strings. +// +// Arguments: +// 1. str: The whole string in the form "KEY: VALUE" +// 2. value: A pointer which will point to the beginning of the VALUE +// +// Return value: +// On success 0 is returned and value points to the beginning of VALUE +// On failure, -1 is returned and value stays intact. +int get_string_val(char *str, char **value) { + size_t str_sz = strlen(str); + char *val_str = strchr(str, ':'); + + if (val_str == NULL) { + // We could not find the beginning of the value string. + fprintf(stderr, "Failed to find ':' character in %s\n", str); + return -1; + } + + // strchr will return a pointer to ':', but we need to move pass this character + // and until we find a non-space value. + val_str++; + while ((val_str < str + str_sz) && *val_str != '\0') { + if (!isspace(*val_str)) { + *value = val_str; + + return 0; + } + val_str++; + } + + // We can not go over the string. Something is wrong + fprintf(stderr, "Failed to find value after ':' in %s\n", str); + + return -1; +} + +// parse_process_config: Parses a list with the following format: +// UCS +// UID: +// GID: +// WD: +// UCE +// It is important to note, that this function will alter the given list, +// replacing the new line characters with the end of string '\0' character. +// The funtion returns a dynamically allocated memory and the caller is +// responsible to free that memory. +// +// Arguments: +// 1. string_area: The list with in the aformentioned format. +// +// Return value: +// On success it returns a pointer to a dynamically allocated memory that +// contains a process_config struct filled with the information +// from the configuration. +// Otherwise, NULL is returned +struct process_config *parse_process_config(char **string_area) { + struct process_config *conf = NULL; + char *tmp_field = NULL; + + conf = malloc(sizeof(struct process_config)); + if (!conf) { + fprintf(stderr, "Failed to allocate memory for app execution environment config\n"); + return NULL; + } + memset(conf, 0, sizeof(struct process_config)); + conf->wdir = NULL; // Sanity + + tmp_field = strtok(*string_area, "\n"); + // Discard the first string since it is the special string "UCS" + // Also, it is safe to call strtok, even if there was no '\n', since it will + // return NULL again. + tmp_field = strtok(NULL, "\n"); + while (tmp_field) { + int ret = 0; + + if (memcmp(tmp_field, "UID", 3) == 0) { + ret = get_uint_val(tmp_field, &(conf->uid)); + if (ret != 0) { + fprintf(stderr, "Failed to retreive UID information from %s\n", tmp_field); + break; + } + } else if (memcmp(tmp_field, "GID", 3) == 0) { + ret = get_uint_val(tmp_field, &(conf->gid)); + if (ret != 0) { + fprintf(stderr, "Failed to retreive GID information from %s\n", tmp_field); + break; + } + } else if (memcmp(tmp_field, "WD", 2) == 0) { + ret = get_string_val(tmp_field, &(conf->wdir)); + if (ret != 0) { + fprintf(stderr, "Failed to retreive WD information from %s\n", tmp_field); + break; + } + } else if (memcmp(tmp_field, "UCE", 3) == 0) { + *string_area = tmp_field + 4; // 4 bytes for the "UCE" string + return conf; + } + + tmp_field = strtok(NULL, "\n"); + } + + free(conf); + return NULL; +} + // get_config_from_file: Reads the contents of argumen and it parses the // app execution configuration and environment variables list. // The app execution configuration list starts with the line "UCS" and ends with the @@ -433,6 +593,7 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { char *buf = NULL; char *path_env = NULL; struct app_exec_config *econf = NULL; + struct process_config *pconf = NULL; char *conf_area = NULL; buf = read_file_and_size(file, &size); @@ -442,6 +603,7 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { } conf_area = buf; + DEBUG_PRINT("Checking for environment variables list\n"); // Check if the special string "UES" is present // which means that now starts the environment variable // list. @@ -462,6 +624,27 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { } } + DEBUG_PRINT("Checking for execution environment configuration\n"); + // Check if the special string "UCS" is present + // which means that now starts the configuration for the application + // execution environment + if (memcmp(conf_area, "UCS", 3) == 0) { + char *init_conf_area = conf_area; + // Extract the environment variables from the list + pconf = parse_process_config(&conf_area); + if (!pconf ) { + fprintf(stderr, "Warning: No configuration for the application execution environment was found\n"); + } + // If the list was properly formatted, ending with "UCE" + // then string_area should differ from init_string_area + // Otherwise, the list was not properly formatted and + // we abort the parsing. + if (conf_area == init_conf_area) { + fprintf(stderr, "Invalid format of application execution environment configuration\n"); + goto get_env_vars_error_free; + } + } + econf = malloc(sizeof(struct app_exec_config)); if (!econf) { fprintf(stderr, "Could not allocate memory for app exec config struct\n"); @@ -471,6 +654,7 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { *sbuf = buf; econf->envs = env_vars; econf->path_env = path_env; + econf->pr_conf = pconf; return econf; get_env_vars_error_free: @@ -621,10 +805,53 @@ int manual_execvpe(const char *env_path, const char *file_bin, char *const argv[ return status; } +// setup_exec_env: Sets up the process execution environment as defined by +// the process_conf argument. +// +// Arguments: +// 1. process_conf: The config to apply with uid/gid and CWD. +// +// Return value: +// On success 0 is returned. +// Otherwise 1 is returned. +int setup_exec_env(struct process_config *process_conf) { + int ret = 0; + + if (!process_conf) { + DEBUG_PRINT("Empty config, nothing to be done\n"); + return 0; + } + + DEBUG_PRINTF("Setting gid to %d\n", process_conf->gid); + ret = setgid(process_conf->gid); + if (ret < 0) { + perror("set GID"); + return 1; + } + + DEBUG_PRINTF("Setting uid to %d\n", process_conf->uid); + ret = setuid(process_conf->uid); + if (ret < 0) { + // No need for reverting gid, since we will exit. + perror("set UID"); + return 1; + } + + DEBUG_PRINTF("Switching to directory %s\n", process_conf->wdir); + ret = chdir(process_conf->wdir); + if (ret < 0) { + // No need for reverting gid/uid, since we will exit. + perror("set CWD"); + return 1; + } + + return 0; +} + int child_func(char *argv[]) { char *config_file = NULL; char *config_buf = NULL; - struct app_exec_config *app_envs = NULL; + struct app_exec_config *app_config = NULL; int ret = 0; DEBUG_PRINT("Isolating child\n"); @@ -643,17 +870,25 @@ int child_func(char *argv[]) { fprintf(stderr, "Failed to mount special filesystems\n"); return 1; } - app_envs = get_config_from_file(config_file, &config_buf); + app_config = get_config_from_file(config_file, &config_buf); } - if (app_envs) { - ret = manual_execvpe(app_envs->path_env, argv[0], argv, app_envs->envs); + if (app_config) { + ret = setup_exec_env(app_config->pr_conf); + if (ret != 0) { + fprintf(stderr, "Failed to set up the process execution environment\n"); + goto child_func_free; + } + ret = manual_execvpe(app_config->path_env, argv[0], argv, app_config->envs); } else { + DEBUG_PRINT("No configuration, simply execvp\n"); ret = manual_execvpe(NULL, argv[0], argv, NULL); } // If we returned something went wrong +child_func_free: free(config_buf); - free(app_envs->envs); - free(app_envs); + free(app_config->envs); + free(app_config->pr_conf); + free(app_config); return ret; }