/* 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; }
/* 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; }