diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml index f857ecacc7..0b6bf793ce 100644 --- a/man/systemd-dissect.xml +++ b/man/systemd-dissect.xml @@ -34,6 +34,9 @@ systemd-dissect OPTIONS IMAGE + + systemd-dissect OPTIONS IMAGE + systemd-dissect OPTIONS IMAGE COMMAND @@ -163,6 +166,23 @@ standard output. + + + + + Generates a BSD mtree8 + compatible file manifest of the specified disk image. This is useful for comparing disk image + contents in detail, including inode information and other metadata. While the generated manifest will + contain detailed inode information, it currently excludes extended attributes, file system + capabilities, MAC labels, chattr1 file + flags, btrfs subvolume information, and various other file metadata. File content information is + shown via a SHA256 digest. Additional fields might be added in future. Note that inode information + such as link counts, inode numbers and timestamps is excluded from the output on purpose, as it + typically complicates reproducibility. + + diff --git a/src/basic/recurse-dir.c b/src/basic/recurse-dir.c index d16ca98f67..908e833501 100644 --- a/src/basic/recurse-dir.c +++ b/src/basic/recurse-dir.c @@ -126,6 +126,7 @@ int recurse_dir( void *userdata) { _cleanup_free_ DirectoryEntries *de = NULL; + STRUCT_STATX_DEFINE(root_sx); int r; assert(dir_fd >= 0); @@ -139,6 +140,26 @@ int recurse_dir( if (n_depth_max == UINT_MAX) /* special marker for "default" */ n_depth_max = DEFAULT_RECURSION_MAX; + if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) { + if (statx_mask != 0) { + r = statx_fallback(dir_fd, "", AT_EMPTY_PATH, statx_mask, &root_sx); + if (r < 0) + return r; + } + + r = func(RECURSE_DIR_ENTER, + path, + -1, /* we have no parent fd */ + dir_fd, + NULL, /* we have no dirent */ + statx_mask != 0 ? &root_sx : NULL, + userdata); + if (IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY)) + return 0; + if (r != RECURSE_DIR_CONTINUE) + return r; + } + r = readdir_all(dir_fd, flags, &de); if (r < 0) return r; @@ -397,7 +418,7 @@ int recurse_dir( p, statx_mask, n_depth_max - 1, - flags, + flags &~ RECURSE_DIR_TOPLEVEL, /* we already called the callback for this entry */ func, userdata); if (r != 0) @@ -427,6 +448,19 @@ int recurse_dir( return r; } + if (FLAGS_SET(flags, RECURSE_DIR_TOPLEVEL)) { + + r = func(RECURSE_DIR_LEAVE, + path, + -1, + dir_fd, + NULL, + statx_mask != 0 ? &root_sx : NULL, + userdata); + if (!IN_SET(r, RECURSE_DIR_LEAVE_DIRECTORY, RECURSE_DIR_SKIP_ENTRY, RECURSE_DIR_CONTINUE)) + return r; + } + return 0; } diff --git a/src/basic/recurse-dir.h b/src/basic/recurse-dir.h index 779c91e905..c10c8ddc9d 100644 --- a/src/basic/recurse-dir.h +++ b/src/basic/recurse-dir.h @@ -65,6 +65,7 @@ typedef enum RecurseDirFlags { RECURSE_DIR_ENSURE_TYPE = 1 << 2, /* guarantees that 'd_type' field of 'de' is not DT_UNKNOWN */ RECURSE_DIR_SAME_MOUNT = 1 << 3, /* skips over subdirectories that are submounts */ RECURSE_DIR_INODE_FD = 1 << 4, /* passes an opened inode fd (O_DIRECTORY fd in case of dirs, O_PATH otherwise) */ + RECURSE_DIR_TOPLEVEL = 1 << 5, /* call RECURSE_DIR_ENTER/RECURSE_DIR_LEAVE once for top-level dir, too, with dir_fd=-1 and NULL dirent */ } RecurseDirFlags; typedef struct DirectoryEntries { diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index a1e9cb6add..81764690e9 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -19,6 +19,7 @@ #include "devnum-util.h" #include "dissect-image.h" #include "env-util.h" +#include "escape.h" #include "fd-util.h" #include "fileio.h" #include "format-table.h" @@ -38,6 +39,7 @@ #include "pretty-print.h" #include "process-util.h" #include "recurse-dir.h" +#include "sha256.h" #include "stat-util.h" #include "string-util.h" #include "strv.h" @@ -50,6 +52,7 @@ static enum { ACTION_MOUNT, ACTION_UMOUNT, ACTION_LIST, + ACTION_MTREE, ACTION_WITH, ACTION_COPY_FROM, ACTION_COPY_TO, @@ -87,6 +90,7 @@ static int help(void) { "%1$s [OPTIONS...] --mount IMAGE PATH\n" "%1$s [OPTIONS...] --umount PATH\n" "%1$s [OPTIONS...] --list IMAGE\n" + "%1$s [OPTIONS...] --mtree IMAGE\n" "%1$s [OPTIONS...] --with IMAGE [COMMAND…]\n" "%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n" "%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n\n" @@ -118,6 +122,7 @@ static int help(void) { " -U Shortcut for --umount --rmdir\n" " -l --list List all the files and directories of the specified\n" " OS image\n" + " --mtree Show BSD mtree manifest of OS image\n" " --with Mount, run command, unmount\n" " -x --copy-from Copy files from image to host\n" " -a --copy-to Copy files from host to image\n" @@ -191,6 +196,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_MKDIR, ARG_RMDIR, ARG_JSON, + ARG_MTREE, }; static const struct option options[] = { @@ -211,6 +217,7 @@ static int parse_argv(int argc, char *argv[]) { { "mkdir", no_argument, NULL, ARG_MKDIR }, { "rmdir", no_argument, NULL, ARG_RMDIR }, { "list", no_argument, NULL, 'l' }, + { "mtree", no_argument, NULL, ARG_MTREE }, { "copy-from", no_argument, NULL, 'x' }, { "copy-to", no_argument, NULL, 'a' }, { "json", required_argument, NULL, ARG_JSON }, @@ -278,6 +285,11 @@ static int parse_argv(int argc, char *argv[]) { arg_flags |= DISSECT_IMAGE_READ_ONLY; break; + case ARG_MTREE: + arg_action = ACTION_MTREE; + arg_flags |= DISSECT_IMAGE_READ_ONLY; + break; + case ARG_WITH: arg_action = ACTION_WITH; break; @@ -424,6 +436,7 @@ static int parse_argv(int argc, char *argv[]) { break; case ACTION_LIST: + case ACTION_MTREE: if (optind + 1 != argc) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected an image file path as only argument."); @@ -815,7 +828,167 @@ static int list_print_item( return RECURSE_DIR_CONTINUE; } -static int action_list_or_copy(DissectedImage *m, LoopDevice *d) { +static int get_file_sha256(int inode_fd, uint8_t ret[static SHA256_DIGEST_SIZE]) { + _cleanup_close_ int fd = -1; + struct sha256_ctx ctx; + + /* convert O_PATH fd into a regular one */ + fd = fd_reopen(inode_fd, O_RDONLY|O_CLOEXEC); + if (fd < 0) + return fd; + + /* Calculating the SHA sum might be slow, hence let's flush STDOUT first, to give user an idea where we are slow. */ + fflush(stdout); + + sha256_init_ctx(&ctx); + + for (;;) { + uint8_t buffer[64 * 1024]; + ssize_t n; + + n = read(fd, buffer, sizeof(buffer)); + if (n < 0) + return -errno; + if (n == 0) + break; + + sha256_process_bytes(buffer, n, &ctx); + } + + sha256_finish_ctx(&ctx, ret); + return 0; +} + +static int mtree_print_item( + RecurseDirEvent event, + const char *path, + int dir_fd, + int inode_fd, + const struct dirent *de, + const struct statx *sx, + void *userdata) { + + int r; + + assert_se(path); + assert_se(sx); + + if (IN_SET(event, RECURSE_DIR_ENTER, RECURSE_DIR_ENTRY)) { + _cleanup_free_ char *escaped = NULL; + + if (isempty(path)) + path = "."; + else { + /* BSD mtree uses either C or octal escaping, and covers whitespace, comments and glob characters. We use C style escaping and follow suit */ + escaped = xescape(path, WHITESPACE COMMENTS GLOB_CHARS); + if (!escaped) + return log_oom(); + + path = escaped; + } + + printf("%s", isempty(path) ? "." : path); + + if (FLAGS_SET(sx->stx_mask, STATX_TYPE)) { + if (S_ISDIR(sx->stx_mode)) + printf("%s/%s", ansi_grey(), ansi_normal()); + + printf(" %stype=%s%s%s%s", + ansi_grey(), + ansi_normal(), + S_ISDIR(sx->stx_mode) ? ansi_highlight_blue() : + S_ISLNK(sx->stx_mode) ? ansi_highlight_cyan() : + (S_ISFIFO(sx->stx_mode) || S_ISCHR(sx->stx_mode) || S_ISBLK(sx->stx_mode)) ? ansi_highlight_yellow4() : + S_ISSOCK(sx->stx_mode) ? ansi_highlight_magenta() : "", + ASSERT_PTR(S_ISDIR(sx->stx_mode) ? "dir" : + S_ISREG(sx->stx_mode) ? "file" : + S_ISLNK(sx->stx_mode) ? "link" : + S_ISFIFO(sx->stx_mode) ? "fifo" : + S_ISBLK(sx->stx_mode) ? "block" : + S_ISCHR(sx->stx_mode) ? "char" : + S_ISSOCK(sx->stx_mode) ? "socket" : NULL), + ansi_normal()); + } + + if (FLAGS_SET(sx->stx_mask, STATX_MODE) && (!FLAGS_SET(sx->stx_mask, STATX_TYPE) || !S_ISLNK(sx->stx_mode))) + printf(" %smode=%s%04o", + ansi_grey(), + ansi_normal(), + (unsigned) (sx->stx_mode & 0777)); + + if (FLAGS_SET(sx->stx_mask, STATX_UID)) + printf(" %suid=%s" UID_FMT, + ansi_grey(), + ansi_normal(), + sx->stx_uid); + + if (FLAGS_SET(sx->stx_mask, STATX_GID)) + printf(" %sgid=%s" GID_FMT, + ansi_grey(), + ansi_normal(), + sx->stx_gid); + + if (FLAGS_SET(sx->stx_mask, STATX_TYPE|STATX_SIZE) && S_ISREG(sx->stx_mode)) { + printf(" %ssize=%s%" PRIu64, + ansi_grey(), + ansi_normal(), + (uint64_t) sx->stx_size); + + if (inode_fd >= 0 && sx->stx_size > 0) { + uint8_t hash[SHA256_DIGEST_SIZE]; + + r = get_file_sha256(inode_fd, hash); + if (r < 0) + log_warning_errno(r, "Failed to calculate file SHA256 sum for '%s', ignoring: %m", path); + else { + _cleanup_free_ char *h = NULL; + + h = hexmem(hash, sizeof(hash)); + if (!h) + return log_oom(); + + printf(" %ssha256sum=%s%s", + ansi_grey(), + ansi_normal(), + h); + } + } + } + + if (FLAGS_SET(sx->stx_mask, STATX_TYPE) && S_ISLNK(sx->stx_mode) && inode_fd >= 0) { + _cleanup_free_ char *target = NULL; + + r = readlinkat_malloc(inode_fd, "", &target); + if (r < 0) + log_warning_errno(r, "Failed to read symlink '%s', ignoring: %m", path); + else { + _cleanup_free_ char *target_escaped = NULL; + + target_escaped = xescape(target, WHITESPACE COMMENTS GLOB_CHARS); + if (!target_escaped) + return log_oom(); + + printf(" %slink=%s%s", + ansi_grey(), + ansi_normal(), + target_escaped); + } + } + + if (FLAGS_SET(sx->stx_mask, STATX_TYPE) && (S_ISBLK(sx->stx_mode) || S_ISCHR(sx->stx_mode))) + printf(" %sdevice=%slinux,%" PRIu64 ",%" PRIu64, + ansi_grey(), + ansi_normal(), + (uint64_t) sx->stx_rdev_major, + (uint64_t) sx->stx_rdev_minor); + + printf("\n"); + } + + return RECURSE_DIR_CONTINUE; +} + +static int action_list_or_mtree_or_copy(DissectedImage *m, LoopDevice *d) { _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; _cleanup_(rmdir_and_freep) char *created_dir = NULL; _cleanup_free_ char *temp = NULL; @@ -976,15 +1149,18 @@ static int action_list_or_copy(DissectedImage *m, LoopDevice *d) { } else { _cleanup_close_ int dfd = -1; - assert(arg_action == ACTION_LIST); - dfd = open(mounted_dir, O_DIRECTORY|O_CLOEXEC|O_RDONLY); if (dfd < 0) return log_error_errno(errno, "Failed to open mount directory: %m"); pager_open(arg_pager_flags); - r = recurse_dir(dfd, NULL, 0, UINT_MAX, RECURSE_DIR_SORT, list_print_item, NULL); + if (arg_action == ACTION_LIST) + r = recurse_dir(dfd, NULL, 0, UINT_MAX, RECURSE_DIR_SORT, list_print_item, NULL); + else if (arg_action == ACTION_MTREE) + r = recurse_dir(dfd, ".", STATX_TYPE|STATX_MODE|STATX_UID|STATX_GID|STATX_SIZE, UINT_MAX, RECURSE_DIR_SORT|RECURSE_DIR_INODE_FD|RECURSE_DIR_TOPLEVEL, mtree_print_item, NULL); + else + assert_not_reached(); if (r < 0) return log_error_errno(r, "Failed to list image: %m"); } @@ -1198,9 +1374,10 @@ static int run(int argc, char *argv[]) { break; case ACTION_LIST: + case ACTION_MTREE: case ACTION_COPY_FROM: case ACTION_COPY_TO: - r = action_list_or_copy(m, d); + r = action_list_or_mtree_or_copy(m, d); break; case ACTION_WITH: diff --git a/test/units/testsuite-50.sh b/test/units/testsuite-50.sh index 10634a6ba9..a5c1eaba26 100755 --- a/test/units/testsuite-50.sh +++ b/test/units/testsuite-50.sh @@ -42,6 +42,7 @@ systemd-dissect "${image}.raw" | grep -q -F "MARKER=1" systemd-dissect "${image}.raw" | grep -q -F -f <(sed 's/"//g' "$os_release") systemd-dissect --list "${image}.raw" | grep -q '^etc/os-release$' +systemd-dissect --mtree "${image}.raw" | grep -q "./usr/bin/cat type=file mode=0755 uid=0 gid=0" read -r SHA256SUM1 _ < <(systemd-dissect --copy-from "${image}.raw" etc/os-release | sha256sum) test "$SHA256SUM1" != ""