/* Ensure that a directory either doesn't exist * or does exist and belongs to root. * If it exists, but is not a directory or belonging to root, remove it * returns true if it doesn't exist */ static int ensure_root_owned_dir(const char *dirname) { struct stat sb; int ret = stat(dirname, &sb); if (ret && (errno == ENOENT)) { return true; } if ((sb.st_uid == 0) && (S_ISDIR(sb.st_mode)) && ((sb.st_mode & 0777) == 0700)) { return false; } /* Oops, not owned by root or * not a directory or wrong perms */ swupd_rm(dirname); errno = 0; ret = stat(dirname, &sb); if ((ret != -1) || (errno != ENOENT)) { fprintf(stderr, "Error \"%s\" not owned by root, is not a directory, " "or has the wrong permissions.\n" "However it couldn't be deleted. stat gives error '%s'\n", dirname, strerror(errno)); exit(100); } return true; /* doesn't exist now */ }
static int swupd_rm_dir(const char *path) { DIR *dir; struct dirent *entry; char *filename = NULL; int ret = 0; int err; dir = opendir(path); if (dir == NULL) { if (errno == ENOENT) { ret = 0; goto exit; } else { ret = -1; goto exit; } } while (true) { errno = 0; entry = readdir(dir); if (!entry) { if (!errno) { ret = errno; } break; } if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) { continue; } free_string(&filename); string_or_die(&filename, "%s/%s", path, entry->d_name); err = swupd_rm(filename); if (err) { ret = -1; goto exit; } } /* Delete directory once it's empty */ err = rmdir(path); if (err) { if (errno == ENOENT) { } else { ret = -1; goto exit; } } exit: closedir(dir); free_string(&filename); return ret; }
static int swupd_rm_dir(const char *path) { DIR *dir; struct dirent entry; struct dirent *result; char *filename = NULL; int ret, err; dir = opendir(path); if (dir == NULL) { if (errno == ENOENT) { ret = 0; goto exit; } else { ret = -1; goto exit; } } while ((ret = readdir_r(dir, &entry, &result)) == 0) { if (result == NULL) { break; } if (!strcmp(entry.d_name, ".") || !strcmp(entry.d_name, "..")) { continue; } free(filename); string_or_die(&filename, "%s/%s", path, entry.d_name); err = swupd_rm(filename); if (err) { ret = -1; goto exit; } } /* Delete directory once it's empty */ err = rmdir(path); if (err) { if (errno == ENOENT) { } else { ret = -1; goto exit; } } exit: closedir(dir); free(filename); return ret; }
/* Consider adding a remove_leftovers() that runs in verify/fix in order to * allow this function to mkdtemp create folders for parallel build */ int do_staging(struct file *file) { char *statfile = NULL, *tmp = NULL, *tmp2 = NULL; char *dir, *base, *rel_dir; char *tarcommand = NULL; char *original = NULL; char *target = NULL; char *targetpath = NULL; char *symbase = NULL; char *rename_target = NULL; char *rename_tmpdir = NULL; int ret; struct stat s; tmp = strdup(file->filename); tmp2 = strdup(file->filename); dir = dirname(tmp); base = basename(tmp2); rel_dir = dir; if (*dir == '/') { rel_dir = dir + 1; } string_or_die(&original, "%s/staged/%s", STATE_DIR, file->hash); string_or_die(&targetpath, "%s%s", path_prefix, rel_dir); ret = stat(targetpath, &s); if (S_ISLNK(s.st_mode)) { /* Follow symlink to ultimate target and redo stat */ symbase = realpath(targetpath, NULL); if (symbase != NULL) { free(targetpath); targetpath = strdup(symbase); ret = stat(targetpath, &s); free(symbase); } } /* For now, just report on error conditions. Once we implement * verify_fix_path(char *path, int targetversion), we'll want to call it here */ if ((ret == -1) && (errno == ENOENT)) { printf("Error: Update target directory does not exist: %s\n", targetpath); } else if (!S_ISDIR(s.st_mode)) { printf("Error: Update target exists but is NOT a directory: %s\n", targetpath); } free(targetpath); string_or_die(&target, "%s%s/.update.%s", path_prefix, rel_dir, base); ret = swupd_rm(target); if (ret < 0 && ret != -ENOENT) { printf("Error: Failed to remove %s\n", target); } string_or_die(&statfile, "%s%s", path_prefix, file->filename); memset(&s, 0, sizeof(struct stat)); ret = lstat(statfile, &s); if (ret == 0) { if ((file->is_dir && !S_ISDIR(s.st_mode)) || (file->is_link && !S_ISLNK(s.st_mode)) || (file->is_file && !S_ISREG(s.st_mode))) { //file type changed, move old out of the way for new ret = swupd_rm(statfile); if (ret < 0) { ret = -ETYPE_CHANGED_FILE_RM; goto out; } } } free(statfile); if (file->is_dir || S_ISDIR(s.st_mode)) { /* In the btrfs only scenario there was an implicit * "create_or_update_dir()" via un-tar-ing a directory.tar after * download and the untar happens in the staging subvolume which * then gets promoted to a "real" usable subvolume. But for * a live rootfs the directory needs copied out of staged * and into the rootfs. Tar is a way to copy with * attributes and it includes internal logic that does the * right thing to overlay a directory onto something * pre-existing: */ /* In order to avoid tar transforms with directories, rename * the directory before and after the tar command */ string_or_die(&rename_tmpdir, "%s/tmprenamedir", STATE_DIR); ret = create_staging_renamedir(rename_tmpdir); if (ret) { goto out; } string_or_die(&rename_target, "%s/%s", rename_tmpdir, base); if (rename(original, rename_target)) { ret = -errno; goto out; } string_or_die(&tarcommand, "tar -C %s " TAR_PERM_ATTR_ARGS " -cf - '%s' 2> /dev/null | " "tar -C %s%s " TAR_PERM_ATTR_ARGS " -xf - 2> /dev/null", rename_tmpdir, base, path_prefix, rel_dir); ret = system(tarcommand); if (WIFEXITED(ret)) { ret = WEXITSTATUS(ret); } free(tarcommand); if (rename(rename_target, original)) { ret = -errno; goto out; } if (ret < 0) { ret = -EDIR_OVERWRITE; goto out; } } else { /* (!file->is_dir && !S_ISDIR(stat.st_mode)) */ /* can't naively hard link(): Non-read-only files with same hash must remain * separate copies otherwise modifications to one instance of the file * propagate to all instances of the file perhaps causing subtle data corruption from * a user's perspective. In practice the rootfs is stateless and owned by us. * Additionally cross-mount hardlinks fail and it's hard to know what an admin * might have for overlaid mounts. The use of tar is a simple way to copy, but * inefficient. So prefer hardlink and fall back if needed: */ ret = -1; if (!file->is_config && !file->is_state && !file->use_xattrs) { ret = link(original, target); } if (ret < 0) { /* either the hardlink failed, or it was undesirable (config), do a tar-tar dance */ /* In order to avoid tar transforms, rename the file * before and after the tar command */ string_or_die(&rename_target, "%s/staged/.update.%s", STATE_DIR, base); ret = rename(original, rename_target); if (ret) { ret = -errno; goto out; } string_or_die(&tarcommand, "tar -C %s/staged " TAR_PERM_ATTR_ARGS " -cf - '.update.%s' 2> /dev/null | " "tar -C %s%s " TAR_PERM_ATTR_ARGS " -xf - 2> /dev/null", STATE_DIR, base, path_prefix, rel_dir); ret = system(tarcommand); if (WIFEXITED(ret)) { ret = WEXITSTATUS(ret); } free(tarcommand); ret = rename(rename_target, original); if (ret) { ret = -errno; goto out; } } struct stat buf; int err; if (file->staging) { /* this must never happen...full file never finished download */ free(file->staging); file->staging = NULL; } string_or_die(&file->staging, "%s%s/.update.%s", path_prefix, rel_dir, base); err = lstat(file->staging, &buf); if (err != 0) { free(file->staging); file->staging = NULL; ret = -EDOTFILE_WRITE; goto out; } } out: free(target); free(original); free(rename_target); free(rename_tmpdir); free(tmp); free(tmp2); return ret; }
/* caller should not call this function for do_not_update marked files */ int rename_staged_file_to_final(struct file *file) { int ret; char *target; string_or_die(&target, "%s%s", path_prefix, file->filename); if (!file->staging && !file->is_deleted && !file->is_dir) { free(target); return -1; } if (file->is_deleted) { ret = swupd_rm(target); /* don't count missing ones as errors... * if somebody already deleted them for us then all is well */ if ((ret == -ENOENT) || (ret == -ENOTDIR)) { ret = 0; } } else if (file->is_dir) { ret = 0; } else { struct stat stat; ret = lstat(target, &stat); /* If the file was previously a directory but no longer, then * we need to move it out of the way. * This should not happen because the server side complains * when creating update content that includes such a state * change. But...you never know. */ if ((ret == 0) && (S_ISDIR(stat.st_mode))) { char *lostnfound; char *base; string_or_die(&lostnfound, "%slost+found", path_prefix); ret = mkdir(lostnfound, S_IRWXU); if ((ret != 0) && (errno != EEXIST)) { free(lostnfound); free(target); return ret; } free(lostnfound); base = basename(file->filename); string_or_die(&lostnfound, "%slost+found/%s", path_prefix, base); /* this will fail if the directory was not already emptied */ ret = rename(target, lostnfound); if (ret < 0 && errno != ENOTEMPTY && errno != EEXIST) { printf("Error: failed to move %s to lost+found: %s\n", base, strerror(errno)); } free(lostnfound); } else { ret = rename(file->staging, target); if (ret < 0) { printf("Error: failed to rename staged %s to final: %s\n", file->hash, strerror(errno)); } unlink(file->staging); } } free(target); return ret; }
/* Consider adding a remove_leftovers() that runs in verify/fix in order to * allow this function to mkdtemp create folders for parallel build */ enum swupd_code do_staging(struct file *file, struct manifest *MoM) { char *statfile = NULL, *tmp = NULL, *tmp2 = NULL; char *dir, *base, *rel_dir; char *tarcommand = NULL; char *original = NULL; char *target = NULL; char *targetpath = NULL; char *rename_target = NULL; char *rename_tmpdir = NULL; char real_path[4096] = { 0 }; int ret; struct stat s; tmp = strdup_or_die(file->filename); tmp2 = strdup_or_die(file->filename); dir = dirname(tmp); base = basename(tmp2); rel_dir = dir; if (*dir == '/') { rel_dir = dir + 1; } string_or_die(&original, "%s/staged/%s", state_dir, file->hash); string_or_die(&targetpath, "%s%s", path_prefix, rel_dir); ret = stat(targetpath, &s); if ((ret == -1) && (errno == ENOENT)) { if (MoM) { warn("Update target directory does not exist: %s. Trying to fix it\n", targetpath); verify_fix_path(dir, MoM); } else { warn("Update target directory does not exist: %s. Auto-fix disabled\n", targetpath); } } else if (!S_ISDIR(s.st_mode)) { error("Update target exists but is NOT a directory: %s\n", targetpath); } if (!realpath(targetpath, real_path)) { ret = -1; goto out; } else if (strcmp(path_prefix, targetpath) != 0 && strcmp(targetpath, real_path) != 0) { /* * targetpath and real_path should always be equal but * in the case of the targetpath being the path_prefix * there is a trailing '/' in path_prefix but realpath * doesn't keep the trailing '/' so check for that case * specifically. */ ret = -1; goto out; } string_or_die(&target, "%s%s/.update.%s", path_prefix, rel_dir, base); ret = swupd_rm(target); if (ret < 0 && ret != -ENOENT) { error("Failed to remove %s\n", target); } string_or_die(&statfile, "%s%s", path_prefix, file->filename); memset(&s, 0, sizeof(struct stat)); ret = lstat(statfile, &s); if (ret == 0) { if ((file->is_dir && !S_ISDIR(s.st_mode)) || (file->is_link && !S_ISLNK(s.st_mode)) || (file->is_file && !S_ISREG(s.st_mode))) { //file type changed, move old out of the way for new ret = swupd_rm(statfile); if (ret < 0) { ret = SWUPD_COULDNT_REMOVE_FILE; goto out; } } } free_string(&statfile); if (file->is_dir || S_ISDIR(s.st_mode)) { /* In the btrfs only scenario there was an implicit * "create_or_update_dir()" via un-tar-ing a directory.tar after * download and the untar happens in the staging subvolume which * then gets promoted to a "real" usable subvolume. But for * a live rootfs the directory needs copied out of staged * and into the rootfs. Tar is a way to copy with * attributes and it includes internal logic that does the * right thing to overlay a directory onto something * pre-existing: */ /* In order to avoid tar transforms with directories, rename * the directory before and after the tar command */ string_or_die(&rename_tmpdir, "%s/tmprenamedir", state_dir); ret = create_staging_renamedir(rename_tmpdir); if (ret) { ret = SWUPD_COULDNT_CREATE_DIR; goto out; } string_or_die(&rename_target, "%s/%s", rename_tmpdir, base); if (rename(original, rename_target)) { ret = SWUPD_COULDNT_RENAME_DIR; goto out; } string_or_die(&tarcommand, TAR_COMMAND " -C '%s' " TAR_PERM_ATTR_ARGS " -cf - './%s' 2> /dev/null | " TAR_COMMAND " -C '%s%s' " TAR_PERM_ATTR_ARGS " -xf - 2> /dev/null", rename_tmpdir, base, path_prefix, rel_dir); ret = system(tarcommand); if (ret == -1) { ret = SWUPD_SUBPROCESS_ERROR; } if (WIFEXITED(ret)) { ret = WEXITSTATUS(ret); } free_string(&tarcommand); if (rename(rename_target, original)) { ret = SWUPD_COULDNT_RENAME_DIR; goto out; } if (ret) { ret = SWUPD_COULDNT_RENAME_DIR; goto out; } } else { /* (!file->is_dir && !S_ISDIR(stat.st_mode)) */ /* can't naively hard link(): Non-read-only files with same hash must remain * separate copies otherwise modifications to one instance of the file * propagate to all instances of the file perhaps causing subtle data corruption from * a user's perspective. In practice the rootfs is stateless and owned by us. * Additionally cross-mount hardlinks fail and it's hard to know what an admin * might have for overlaid mounts. The use of tar is a simple way to copy, but * inefficient. So prefer hardlink and fall back if needed: */ ret = -1; if (!file->is_config && !file->is_state && !file->use_xattrs) { ret = link(original, target); } if (ret < 0) { /* either the hardlink failed, or it was undesirable (config), do a tar-tar dance */ /* In order to avoid tar transforms, rename the file * before and after the tar command */ string_or_die(&rename_target, "%s/staged/.update.%s", state_dir, base); ret = rename(original, rename_target); if (ret) { ret = SWUPD_COULDNT_RENAME_FILE; goto out; } string_or_die(&tarcommand, TAR_COMMAND " -C '%s/staged' " TAR_PERM_ATTR_ARGS " -cf - '.update.%s' 2> /dev/null | " TAR_COMMAND " -C '%s%s' " TAR_PERM_ATTR_ARGS " -xf - 2> /dev/null", state_dir, base, path_prefix, rel_dir); ret = system(tarcommand); if (ret == -1) { ret = SWUPD_SUBPROCESS_ERROR; } if (WIFEXITED(ret)) { ret = WEXITSTATUS(ret); } free_string(&tarcommand); ret = rename(rename_target, original); if (ret) { ret = SWUPD_COULDNT_RENAME_FILE; goto out; } } struct stat buf; int err; free_string(&file->staging); string_or_die(&file->staging, "%s%s/.update.%s", path_prefix, rel_dir, base); err = lstat(file->staging, &buf); if (err != 0) { free_string(&file->staging); ret = SWUPD_COULDNT_CREATE_FILE; goto out; } } out: free_string(&target); free_string(&targetpath); free_string(&original); free_string(&rename_target); free_string(&rename_tmpdir); free_string(&tmp); free_string(&tmp2); return ret; }