From 5a847fd805cb341b69b5bd1a756df9b01515e6cc Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Tue, 26 Aug 2025 17:53:20 +0000 Subject: [PATCH 1/4] Update README with urunit config and reduce search area for app config Add information to README regarding the new changes in urunit and specifically for the support of a configuration file for the environment variables and the proccess execution environment. Furthermore, rephrase some comments in the code and reduce the search area in the config buffer for searching the configuration for the application execution environment. Signed-off-by: Charalampos Mainas --- README.md | 35 ++++++++++++++++++++++++++++++++--- main.c | 14 ++++++++++---- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aaa49f8..0419d84 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,13 @@ to the target application. The key features of `urunit` are: -- Parsing and grouping multi-word arguments from Linux kernel boot parameters -- Launching and waiting for the target application until it terminates -- Reaping all zombie processes +- Parsing and grouping multi-word arguments from Linux kernel boot parameters. +- Launching and waiting for the target application until it terminates. +- Reaping all zombie processes. +- Setting the default route to eth0, if `URUNIT_DEFROUTE` environment variable + is set. +- Reading and setting the environment variables for an application from a file. +- Reading and setting the execution environment configuration for a process from a file. ## Building @@ -57,6 +61,31 @@ urunit echo `hello world` will result in `echo` receiving a single argument: `hello world`. +### Urunit configuration file + +In order to configure the execution environment for an application, `urunit` +accepts a specific configuration file. The configuration file can contain the +following information: + +- The list of the environment variables to set for the application +- The configuration for the process configuration. + +The file can be specified to `urunit` setting the `URUNIT_CONFIG` +environment variable with the path to the configuration file. + +The supported format of the configuration file is the following: + +``` +UES +/* list of environment variables */ +UEE +UCS +UID: +GID: +WD: +UCE +``` + ## Installation Using one of the following methods, we can install `urunit` either in a diff --git a/main.c b/main.c index 9b7347d..aa5aa80 100644 --- a/main.c +++ b/main.c @@ -357,6 +357,8 @@ size_t measure_tokens(char *str_buf, size_t max_size, char tok) { // 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; + // TODO: We might need to retrun a list here with the first + // element being NULL instead of returning NULL char **env_vars = NULL; uint8_t path_found = 0; char *tmp_env = NULL; @@ -413,7 +415,7 @@ char **parse_envs(char **string_area, size_t max_sz, char **path_env) { free(env_vars); return NULL; } - // Add nULL to indicate the end of the table with environment variables. + // Add NULL to indicate the end of the table with environment variables. env_vars[i] = NULL; return env_vars; @@ -515,13 +517,14 @@ int get_string_val(char *str, char **value) { // // Arguments: // 1. string_area: The list with in the aformentioned format. +// 2. max_sz: The max possible size of the list. // // 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 *parse_process_config(char **string_area, size_t max_sz) { struct process_config *conf = NULL; char *tmp_field = NULL; @@ -538,7 +541,7 @@ struct process_config *parse_process_config(char **string_area) { // 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) { + while (tmp_field && ((size_t)(tmp_field - *string_area) < max_sz)) { int ret = 0; if (memcmp(tmp_field, "UID", 3) == 0) { @@ -622,6 +625,9 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { fprintf(stderr, "Invalid format of environment variable list. \"UEE\" was not found\n"); goto get_env_vars_error_free; } + // Reduce the size of the config by the bytes parsed + // for the environment variables list. + size -= conf_area - init_conf_area; } DEBUG_PRINT("Checking for execution environment configuration\n"); @@ -631,7 +637,7 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { 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); + pconf = parse_process_config(&conf_area, size); if (!pconf ) { fprintf(stderr, "Warning: No configuration for the application execution environment was found\n"); } From 49f39785caec82e2ac3e58a1df169c68ae502494 Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Tue, 2 Sep 2025 11:00:49 +0000 Subject: [PATCH 2/4] Add support for reading the block mount configuration Read from the configuration the respective info for mounting attached block devices to the respective mountpoint. In particular the configuration for the block mounts has the following format: UBS ID: MP: ... UBE Therefore, for every block device there is a pair of ID and MP where ID refers to the serial ID of the device and MP refers to the mountpoint of that block device. Signed-off-by: Charalampos Mainas --- README.md | 10 +++- main.c | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0419d84..a38e61a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ The key features of `urunit` are: is set. - Reading and setting the environment variables for an application from a file. - Reading and setting the execution environment configuration for a process from a file. +- Reading and mounting attached block devices defined in the configuration file. ## Building @@ -68,7 +69,9 @@ accepts a specific configuration file. The configuration file can contain the following information: - The list of the environment variables to set for the application -- The configuration for the process configuration. +- The configuration for the process execution environment. +- The list of mounts of block devices. Each block device is defined by its + serial id and it will get mounted in the defined mountpoint. The file can be specified to `urunit` setting the `URUNIT_CONFIG` environment variable with the path to the configuration file. @@ -84,6 +87,11 @@ UID: GID: WD: UCE +UBS +ID: +MP: +... +UBE ``` ## Installation diff --git a/main.c b/main.c index aa5aa80..a672aad 100644 --- a/main.c +++ b/main.c @@ -82,10 +82,16 @@ struct process_config { char *wdir; }; +struct block_config { + char *id; + char *mountpoint; +}; + struct app_exec_config { char **envs; char *path_env; struct process_config *pr_conf; + struct block_config **blk_conf; }; extern char **environ; @@ -574,6 +580,131 @@ struct process_config *parse_process_config(char **string_area, size_t max_sz) { return NULL; } +// parse_block_config Parses a list with the following format: +// UBS +// ID: +// MP: +// ... +// UBE +// 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 the aformentioned format. +// 2. max_sz: The maximum size of the area to look for block config +// +// Return value: +// On success it returns an array of block_config structs filled with the information +// from the list. +// Otherwise, NULL is returned +struct block_config **parse_block_config(char **string_area, size_t max_sz) { + // TODO: We might need to retrun a list here with the first + // element being NULL instead of returning NULL + struct block_config **bentries = NULL; + char *tmp_field = NULL; + size_t i = 0; + size_t total_entries = 0; + + // Count the new line characters we have in the list. + // Since every block entry consist of 2 fields, the total number + // of entries derives from diving the number of new lines by 2. + total_entries = measure_tokens(*string_area, max_sz, '\n') / 2; + // If the list is correctly formatted it will start with "UBS" + // and end with "UBE". These special strings will not be stored, + // but they add up in the overall size, since they occupy one line each. + // However, we can use this extra entry in the array to mark the end of + // the array with NULL. + bentries = malloc(total_entries * sizeof(struct block_config *)); + if (!bentries) { + fprintf(stderr, "Failed to allocate memory for block entries\n"); + return NULL; + } + if (total_entries > 0) + bentries[0] = NULL; + DEBUG_PRINTF("Found %ld block entries\n", total_entries); + + tmp_field = strtok(*string_area, "\n"); + // Discard the first string since it is the special string "UBS" + // 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 && i < total_entries) { + int ret = 0; + + // The first string should be "ID:" + if (memcmp(tmp_field, "ID:", 3) == 0) { + // If bentries[i] is not NULL then we never reached found + // MP entry in the config for this ID. + if (bentries[i]) { + fprintf(stderr, "Multiple ID entries without MP\n"); + goto parse_block_config_free; + } + bentries[i] = malloc(sizeof(struct block_config)); + if (!bentries[i]) { + fprintf(stderr, "Failed to allocate memory for a block entry\n"); + goto parse_block_config_free; + } + bentries[i]->id = NULL; + bentries[i]->mountpoint = NULL; + + ret = get_string_val(tmp_field, &(bentries[i]->id)); + if (ret != 0) { + fprintf(stderr, "Failed to retrieve block ID from %s\n", tmp_field); + free(bentries[i]); + goto parse_block_config_free; + } + DEBUG_PRINTF("Found block entry with ID %s\n", bentries[i]->id); + } else if (memcmp(tmp_field, "MP:", 3) == 0) { + ret = get_string_val(tmp_field, &(bentries[i]->mountpoint)); + if (ret != 0) { + fprintf(stderr, "Failed to retrieve block mountpoint from %s\n", tmp_field); + // Remove the current entry + // because it was not properly formatted. + free(bentries[i]); + goto parse_block_config_free; + } + DEBUG_PRINTF("Found block entry with MP %s\n", bentries[i]->mountpoint); + i++; + bentries[i] = NULL; + } else if (memcmp(tmp_field, "UBE", 3) == 0) { + // 4 bytes for the "UBE" string + *string_area = tmp_field + 4; + break; + } + tmp_field = strtok(NULL, "\n"); + } + + // Special case where malloc did not return NULL with 0 size, + // or none properly formatted block entries were found + // Both cases mean that we have no block entries and hence + // we should return NULL. + if (i == 0) { + // free is safe here, since bentries come from malloc and + // contains either NULL or an address. Both cases are fine for free. + free(bentries); + return NULL; + } + // In case of a malformed block config where we had an ID but no MP, + // then mountpoint will be NULL and we should free the allocated entry. + if (bentries[i] && !(bentries[i]->mountpoint)) { + free(bentries[i]); + } + // Add NULL to indicate the end of the table with block entries + bentries[i] = NULL; + + return bentries; + +parse_block_config_free: + for (size_t j = 0; j < i; j++) { + free(bentries[j]); + } + free(bentries); + + 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 @@ -597,6 +728,7 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { char *path_env = NULL; struct app_exec_config *econf = NULL; struct process_config *pconf = NULL; + struct block_config **bconf = NULL; char *conf_area = NULL; buf = read_file_and_size(file, &size); @@ -649,6 +781,30 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { fprintf(stderr, "Invalid format of application execution environment configuration\n"); goto get_env_vars_error_free; } + // Reduce the size of the config by the bytes parsed + // for the environment variables list. + size -= conf_area - init_conf_area; + } + + DEBUG_PRINT("Checking for block volumes mount configuration\n"); + // Check if the special string "UBS" is present + // which means that now starts the configuration for the block mounts + if (memcmp(conf_area, "UBS", 3) == 0) { + char *init_conf_area = conf_area; + // Extract the block configuration + bconf = parse_block_config(&conf_area, size); + if (!bconf ) { + fprintf(stderr, "Warning: No configuration for block mounts\n"); + } + // If the list was properly formatted, ending with "UBE" + // then conf_area should differ from init_conf_area + // Otherwise, the list was not properly formatted and + // we abort the parsing. + if (conf_area == init_conf_area) { + fprintf(stderr, "Invalid format of block volume mounts\n"); + goto get_env_vars_error_free; + } + size -= conf_area - init_conf_area; } econf = malloc(sizeof(struct app_exec_config)); @@ -661,6 +817,7 @@ struct app_exec_config *get_config_from_file(char *file, char **sbuf) { econf->envs = env_vars; econf->path_env = path_env; econf->pr_conf = pconf; + econf->blk_conf = bconf; return econf; get_env_vars_error_free: From 865594398cc23e8892f54569621aacd19ad9f2ed Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Fri, 5 Sep 2025 18:12:45 +0000 Subject: [PATCH 3/4] Mount the block devices to the correct mountpoint Discover every block device from its serial ID or its order in the config and create all the necessary directories for the mountpoint. Currently, urunit will only look for virtio block devices (vd[a-z]), since urunc will use virtIO for passing a block device to the VM. We might want to revisit this in case urunc uses other types of block devices apart virtio-block. Signed-off-by: Charalampos Mainas --- main.c | 366 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) diff --git a/main.c b/main.c index a672aad..f2001c5 100644 --- a/main.c +++ b/main.c @@ -52,6 +52,7 @@ #include #include +#include #include #include #include @@ -63,6 +64,7 @@ #define STATUS_MAX 255 #define STATUS_MIN 0 #define ETH0_IF "eth0" +#define SERIAL_MAX_SZ 10 #ifdef DEBUG #define SHOW_DEBUG 1 @@ -1011,6 +1013,365 @@ int setup_exec_env(struct process_config *process_conf) { return 0; } +// rm_empty_dirs: Removes the directory dir given as argument and all empty parent +// directories up to and including top_dir. +// +// Arguments: +// 1. dir: The directory to remove. +// 2. top_dir: The top-most directory to remove. It should not end in '/' +// +// Return value: +// On success 0 is returned. +// Otherwise -1 is returned. +int rm_empty_dirs(const char *dir, const char *top_dir) { + char current[PATH_MAX] = { 0 }; + int ret = 0; + + ret = snprintf(current, sizeof(current), "%s", dir); + if (ret <= 0 || (size_t)ret > sizeof(current)) { + fprintf(stderr, "Could not copy %s\n", dir); + return -1; + } + // Make sure the path does not end in '/' + if (current[ret - 1] == '/') { + current[ret - 1] = '\0'; + } + + DEBUG_PRINTF("Top most directory to remove: %s\n", top_dir); + while (strcmp(current, top_dir) != 0) { + char *last_slash = NULL; + + // Stop at root or common mount points + if (strcmp(current, "/") == 0 || + strcmp(current, "/mnt") == 0 || + strcmp(current, "/var") == 0 || + strcmp(current, "/home") == 0 || + strcmp(current, "/tmp") == 0) { + break; + } + + DEBUG_PRINTF("Trying to remove directory: %s\n", current); + ret = rmdir(current); + if (ret != 0) { + perror("rmdir"); + return -1; + } + DEBUG_PRINTF("Removed empty directory: %s\n", current); + + // Get parent directory + last_slash = strrchr(current, '/'); + if (!last_slash || last_slash == current) { + fprintf(stderr, "Could not get parent directory of %s\n", current); + return -1; + } + *last_slash = '\0'; + } + + DEBUG_PRINTF("Trying to remove directory: %s\n", current); + // Remove also top most directory + ret = rmdir(current); + if (ret != 0) { + perror("rmdir"); + return -1; + } + DEBUG_PRINTF("Removed empty directory: %s\n", current); + + return 0; +} + +// mkdir_all: Creates a directory path including all non-existing parent directories. +// Similar to MkdirAll in Go and "mkdir -p" command. +// +// Arguments: +// 1. path: The full path of the directory to create +// 2. mode: The permissions mode for the new directories +// +// Return value: +// On success 0 is returned. +// Otherwise -1 is returned. +int mkdir_all(const char *path, mode_t mode, char *first_dir) { + char tmp_path[PATH_MAX] = { 0 }; + char *next_slash = NULL; + int ret = 0; + struct stat st; + uint8_t is_first = 1; + size_t or_path_len = 0; + size_t tmp_len = 0; + + if (path == NULL || *path == '\0') { + fprintf(stderr, "Invalid path value\n"); + return -1; + } + + if (strcmp(path, "/") == 0) { + fprintf(stderr, "Invalid path value: %s\n", path); + return -1; + } + + // Check if path already exists + if (stat(path, &st) == 0) { + if (S_ISDIR(st.st_mode)) { + return 0; + } else { + fprintf(stderr, "%s exists and is not a directory\n", path); + return -1; + } + } + + ret = snprintf(tmp_path, sizeof(tmp_path), "%s", path); + if (ret <= 0 || (size_t)ret > sizeof(tmp_path)) { + fprintf(stderr, "Could not create a copy of %s\n", path); + return -1; + } + + or_path_len = strlen(tmp_path); + tmp_len = or_path_len; + // Remove trailing slashes + while (tmp_len > 1 && tmp_path[tmp_len - 1] == '/') { + tmp_path[tmp_len - 1] = '\0'; + tmp_len--; + } + // We will need to copy tmp_path later so we need to include + // the end of string character. + or_path_len++; + + // Iterate through the path and create directories + next_slash = strchr(tmp_path + 1, '/'); + while(next_slash) { + *next_slash = '\0'; // Temporarily truncate + + // Try to create the directory + DEBUG_PRINTF("Trying to create dir %s\n", tmp_path); + ret = mkdir(tmp_path, mode); + if (ret != 0 && errno != EEXIST) { + fprintf(stderr, "Could not create directory %s\n", tmp_path); + perror("mkdir"); + ret = -1; + goto mkdir_all_cleanup; + } + if (ret == 0 && is_first) { + ret = snprintf(first_dir, or_path_len, "%s", tmp_path); + if (ret < 0 || (size_t)ret > or_path_len) { + fprintf(stderr, "Could not copy first created path %s", tmp_path); + return -1; + } + is_first = 0; + } + *next_slash = '/'; // Restore the slash + next_slash = strchr(next_slash + 1, '/'); + } + + // Create the final directory + DEBUG_PRINTF("Trying to create dir %s\n", tmp_path); + ret = mkdir(tmp_path, mode); + if (ret != 0 && errno != EEXIST) { + fprintf(stderr, "Could not create directory %s\n", tmp_path); + perror("mkdir"); + ret = -1; + goto mkdir_all_cleanup; + } + if (ret == 0 && is_first) { + ret = snprintf(first_dir, or_path_len, "%s", tmp_path); + if (ret < 0 || (size_t)ret > or_path_len) { + fprintf(stderr, "Could not copy first created directory %s\n", tmp_path); + return -1; + } + } + DEBUG_PRINTF("Top most created dir %s\n", first_dir); + return 0; + +mkdir_all_cleanup: + // If we have not created any directory yet then is_first will be 1 + // and hence we do not have to remove any directory. + if (!is_first) { + int ret = 0; + // However, the fail took place for the tmp_path directory + // which was not created and hence we do not have to remove it. + // Therefore, move to the parent directory. + char *last_slash = strrchr(tmp_path, '/'); + if (!last_slash || last_slash == tmp_path) { + fprintf(stderr, "Could not get parent directory of %s\n", tmp_path); + return ret; + } + *last_slash = '\0'; + ret = rm_empty_dirs(tmp_path, first_dir); + if (ret != 0) { + fprintf(stderr, "Could not remove directories between %s and %s",first_dir, tmp_path ); + } + } + // creation of subdir failed + return ret; +} + +// read_block_dev_serial: Read the serial ID of a block device from the respective sysfs +// entry. +// +// Arguments: +// 1. device_name: The device name +// 2. serial: The buffer to hold the serial ID that was found +// 2. size: The max size of the buffer +// +// Return value: +// If the device exists, then 0 is returned. +// If the deivce doe snot exist 1 is returned. +// In all other cases or errors -1 is returned. +int read_block_dev_serial(const char *device_name, char *serial, const size_t size) { + char path[PATH_MAX]; + FILE *fp; + int ret = 0; + + ret = snprintf(path, sizeof(path), "/sys/block/%s/serial", device_name); + if (ret < 0 || (size_t)ret > sizeof(path)) { + fprintf(stderr, "Could not create sysfs path for %s\n", device_name); + return -1; + } + + fp = fopen(path, "r"); + if (!fp) { + if (errno == ENOENT) { + return 1; + } + perror("fopen"); + return -1; + } + + if (fgets(serial, size, fp) == NULL) { + fclose(fp); + return -1; + } + + // Remove trailing whitespace + serial[strcspn(serial, "\n\r \t")] = '\0'; + fclose(fp); + return 0; +} + +// find_vblock_device_by_order: Returns the nth virtio block device (vd*) if it +// exists. The order is based on the conventional naming of virtio block devices +// in Linux where usually the first attached is vda, second vdb etc. +// +// Arguments: +// 1. n: The order of the virtio blockd evice to retrun. +// 2. device_path: The buffer that will store the path to the block device. +// +// Return value: +// On success 0 is returned and device_path parameter will hold the path +// the the device with the specific ID. +// Otherwise -1 is returned. +int find_vblock_device_by_order(const uint32_t n, char *device_path) { + // TODO: Add support for more than 26 devices. + char suffix = 'a' + (n % 26); + char device_name[] = "/dev/vda"; + int ret = 0; + + device_name[7] = suffix; + ret = access(device_name, F_OK); + if (ret) + return -1; + + snprintf(device_path, PATH_MAX, "%s", device_name); + return 0; +} + +// find_vblock_device_by_serial: Search all virtio block devices (vd[a-z]) to find the +// one with a specific serial ID. +// +// Arguments: +// 1. target_serial: The serial ID to search for in the devices +// 2. device_path: The buffer that will store the path to the block device +// +// Return value: +// On success 0 is returned and device_path parameter will hold the path +// the the device with the specific ID. +// Otherwise -1 is returned. +int find_vblock_device_by_serial(const char *target_serial, char *device_path) { + char suffix = 0; + char serial[SERIAL_MAX_SZ]; + char device_name[] = "vda"; + + for (suffix = 'a'; suffix <= 'z'; suffix++) { + int ret = 0; + + device_name[2] = suffix; + ret = read_block_dev_serial(device_name, serial, sizeof(serial)); + if (ret < 0) { + fprintf(stderr, "Error getting serial id of %s\n", device_name); + continue; + } else if (ret > 0) { + // The device does not exist. Move to the next one. + continue; + } + if (strcmp(serial, target_serial) == 0) { + snprintf(device_path, PATH_MAX, "/dev/%s", device_name); + return 0; + } + } + + return -1; +} + +// mount_block_vols: Mounts all block devices using their info from the +// block_config parameter. +// +// Arguments: +// 1. vols: An array of struct block_config with information to mount +// block volumes +// +// Return value: +// On success 0 is returned. +// Otherwise 1 is returned. +int mount_block_vols(struct block_config **vols) { + struct block_config **iter_bc = NULL; + char first_new_dir[PATH_MAX] = { 0 }; + uint32_t blk_count = 0; + + if (vols == NULL) { + DEBUG_PRINT("No block volumes to mount, nothing to do\n"); + return 0; + } + + for (iter_bc = vols; *iter_bc != NULL; iter_bc++) { + struct block_config *tmp_bc = *iter_bc; + char block_dev[PATH_MAX] = { 0 }; + int ret = 0; + + blk_count++; + first_new_dir[0] = '\0'; + DEBUG_PRINTF("Searching block device with serial ID %s\n", tmp_bc->id); + if (strlen(tmp_bc->id) > 2 && tmp_bc->id[0] == 'F' && tmp_bc->id[1] == 'C') { + ret = find_vblock_device_by_order(blk_count, block_dev); + } else { + ret = find_vblock_device_by_serial(tmp_bc->id, block_dev); + } + if (ret) { + fprintf(stderr, "Could not find any virtio block device with serial ID %s\n", tmp_bc->id); + continue; + } + DEBUG_PRINTF("Found device %s\n", block_dev); + DEBUG_PRINTF("Setup the mountpoint %s\n", tmp_bc->mountpoint); + ret = mkdir_all(tmp_bc->mountpoint, 0755, first_new_dir); + if (ret != 0 ) { + fprintf(stderr, "Failed to create %s\n",tmp_bc->mountpoint); + continue; + } + DEBUG_PRINT("Mount device as ext4\n"); + // TODO: SUpport more filesystem types + ret = mount(block_dev, tmp_bc->mountpoint, "ext4", 0, ""); + if (ret != 0) { + perror("mount"); + // Remove previously created directories. + // NOTE: In case of an error we just print a warning + // We might want to revisit this in the future. + ret = rm_empty_dirs(tmp_bc->mountpoint, first_new_dir); + if (ret < 0) { + fprintf(stderr, "WARNING: Could not remove %s and its subdirs\n", tmp_bc->mountpoint); + } + } + } + + return 0; +} + int child_func(char *argv[]) { char *config_file = NULL; char *config_buf = NULL; @@ -1036,6 +1397,11 @@ int child_func(char *argv[]) { app_config = get_config_from_file(config_file, &config_buf); } if (app_config) { + ret = mount_block_vols(app_config->blk_conf); + if (ret != 0) { + fprintf(stderr, "Failed to mount block volumes\n"); + goto child_func_free; + } ret = setup_exec_env(app_config->pr_conf); if (ret != 0) { fprintf(stderr, "Failed to set up the process execution environment\n"); From d0a121a493cb85fbed9f882db3c0223c9941485b Mon Sep 17 00:00:00 2001 From: Charalampos Mainas Date: Thu, 6 Nov 2025 14:36:49 +0000 Subject: [PATCH 4/4] Unmount all external mounts before shutdown Make sure that all external filesystem mounts are unmounted before shuttidng down to avoid any corruptions. External means all mounts that use an external to the current VM source (e.g. block devices network filesystems). For the time being urunit checks the filesystem type in the mountinfo and compares it with some well known block, network and cloud-based filesystems. We might want to update this list in the future. Signed-off-by: Charalampos Mainas --- main.c | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/main.c b/main.c index f2001c5..1885eb1 100644 --- a/main.c +++ b/main.c @@ -1644,6 +1644,186 @@ int set_default_route() { return ret; } +// is_block_fs: Checks if the parameter belongs to a list of known block-based +// filesystems. +// +// Arguments: +// 1. fs_type: The filesystem type to check +// +// Return value: +// If the filesystem type is a known block-based filesystem type then 1 is returned. +// Otherwise 0 is returned. +int is_block_fs(const char *fs_type) { + const char *block_types[] = { + "ext2", "ext3", "ext4", "xfs", "btrfs", "f2fs", + "jfs", "reiserfs", "nilfs2", "vfat", "ntfs", "exfat", + "hfs", "hfsplus", "ufs", "minix", "iso9660", "udf", + NULL + }; + int i = 0; + + for (i = 0; block_types[i] != NULL; i++) { + if (strcmp(fs_type, block_types[i]) == 0) { + return 1; + } + } + return 0; +} + +// is_network_fs: Checks if the parameter belongs to a list of known network-based +// filesystems. +// +// Arguments: +// 1. fs_type: The filesystem type to check +// +// Return value: +// If the filesystem type is a known network-based filesystem type then 1 is returned. +// Otherwise 0 is returned. +int is_network_fs(const char *fs_type) { + const char *network_types[] = { + "nfs", "nfs4", "cifs", "smb", "smbfs", + "ncpfs", "coda", "afs", "9p", + "glusterfs", "lustre", "ceph", "ocfs2", + NULL + }; + + for (int i = 0; network_types[i] != NULL; i++) { + if (strcmp(fs_type, network_types[i]) == 0) { + return 1; + } + } + return 0; +} + +// is_cloud_storage_fs: Checks if the parameter belongs to a list of known cloud-based +// filesystems. +// +// Arguments: +// 1. fs_type: The filesystem type to check +// +// Return value: +// If the filesystem type is a known cloud-based filesystem type then 1 is returned. +// Otherwise 0 is returned. +int is_cloud_storage_fs(const char *fs_type) { + const char *cloud_types[] = { + "fuse.s3fs", "fuse.goofys", "fuse.s3backer", "fuse.gcsfuse", + "fuse.blobfuse", "fuse.rclone", "fuse.juicefs", "fuse.sshfs", + "fuse.curlftpfs", "fuse.davfs2", "fuse.httpfs", "fuse.s3ql", + "fuse.ossfs", "fuse.cosfs", "fuse.obsfs", "iscsi", "seaweedfs", + "minio", + NULL + }; + + for (int i = 0; cloud_types[i] != NULL; i++) { + if (strcmp(fs_type, cloud_types[i]) == 0) { + return 1; + } + } + + return 0; +} + +// skip_n_words: Returns a pointer after the first n words of a string +// +// Arguments: +// 1. str: The string +// 2. n: The number fo words to skip +// +// Return value: +// On success it returns a pointer right after the first n words inside the string str +// Otherwise str is returned. +char *skip_n_words(const char *str, size_t n) { + const char *c = str; + while (n > 0 && *c) { + // Skip multiple spaces + while (isspace((unsigned char)*c)) + c++; + // Walk the word till space or end of string + while (*c && !isspace((unsigned char)*c)) + c++; + n--; + } + + // Move to the beginning of the next word + while (isspace((unsigned char)*c)) + c++; + if (*c == 0) { + return (char *)str; + } + + return (char *)c; +} + +// unmount_external: Unmounts all the external filesstem mounts found in +// /proc/self/mountinfo. External means all the known block, network and cloud storage +// based filesystems. +// +// Arguments: +// +// Return value: +void unmount_external() { + FILE *mount_info_f = NULL; + char line[1024] = { 0 }; + + mount_info_f = fopen("/proc/self/mountinfo", "r"); + if (!mount_info_f) { + perror("Error opening /proc/self/mountinfo"); + return; + } + + while (fgets(line, sizeof(line), mount_info_f)) { + char *tmp = NULL; + char *mount_point = NULL; + char *mount_type = NULL; + + mount_point = skip_n_words(line, 4); + if (mount_point == line) { + fprintf(stderr, "Malformed line in mountinfo. Could not reach mountpoint: %s\n", line); + continue; + } + tmp = strchr(mount_point, ' '); + if (!tmp) { + fprintf(stderr, "Malformed line in mountinfo. Could not get mountpoint: %s\n", line); + continue; + } + *tmp = '\0'; + tmp++; + DEBUG_PRINTF("Found mountpoint %s\n", mount_point); + // SKip rootfs because we can not unmount it easily. + // Also, the rootfs will be be based on the container's image + // and hence even if somehting goes wrong, a new instance of it + // will get created for another container. Therefore, it will not + // get reused. + if (strcmp(mount_point, "/") == 0) + continue; + mount_type = strstr(tmp, " - "); + if (!mount_type) { + fprintf(stderr, "Malformed line in mountinfo. Could not reach mount type: %s%s\n", line, tmp); + continue; + } + mount_type += 3; + tmp = strchr(mount_type, ' '); + if (!tmp) { + fprintf(stderr, "Malformed line in mountinfo. Could not get mount type: %s%s\n", line, mount_type - 3); + continue; + } + *tmp = '\0'; + DEBUG_PRINTF("Found mount type %s\n", mount_type); + if (is_block_fs(mount_type) || + is_network_fs(mount_type) || + is_cloud_storage_fs(mount_type)) { + int ret = 0; + DEBUG_PRINTF("Trying to unmount %s\n", mount_point); + ret = umount2(mount_point, MNT_FORCE); + if (ret) { + perror("umount"); + } else { + DEBUG_PRINTF("Successful unmount of %s\n", mount_point); + } + } + } +} + int main(int argc, char *argv[]) { pid_t app_pid; int ret = 0; @@ -1688,6 +1868,7 @@ int main(int argc, char *argv[]) { DEBUG_PRINT("Exiting, will reboot in order to shutdown\n"); sync(); + unmount_external(); syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, LINUX_REBOOT_CMD_RESTART, NULL); }