Skip to content

Commit 2323a58

Browse files
committed
dissect: add --manifest switch to generate a UAPI.16 manifest file from a directory tree
The UAPI.16 spec is being discussed here: uapi-group/specifications#213
1 parent 79c4e5d commit 2323a58

1 file changed

Lines changed: 286 additions & 5 deletions

File tree

src/dissect/dissect.c

Lines changed: 286 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
#include "pretty-print.h"
5050
#include "process-util.h"
5151
#include "recurse-dir.h"
52+
#include "rm-rf.h"
5253
#include "runtime-scope.h"
5354
#include "sha256.h"
5455
#include "shift-uid.h"
@@ -70,6 +71,7 @@ static enum {
7071
ACTION_DETACH,
7172
ACTION_LIST,
7273
ACTION_MTREE,
74+
ACTION_MANIFEST,
7375
ACTION_WITH,
7476
ACTION_COPY_FROM,
7577
ACTION_COPY_TO,
@@ -150,6 +152,7 @@ static int help(void) {
150152
"%1$s [OPTIONS...] --detach PATH\n"
151153
"%1$s [OPTIONS...] --list IMAGE\n"
152154
"%1$s [OPTIONS...] --mtree IMAGE\n"
155+
"%1$s [OPTIONS...] --manifest IMAGE\n"
153156
"%1$s [OPTIONS...] --with IMAGE [COMMAND…]\n"
154157
"%1$s [OPTIONS...] --copy-from IMAGE PATH [TARGET]\n"
155158
"%1$s [OPTIONS...] --copy-to IMAGE [SOURCE] PATH\n"
@@ -478,6 +481,11 @@ static int parse_argv(int argc, char *argv[]) {
478481
arg_flags |= DISSECT_IMAGE_READ_ONLY;
479482
break;
480483

484+
OPTION_LONG("manifest", NULL, "Show UAPI.16 manifest of OS image"):
485+
arg_action = ACTION_MANIFEST;
486+
arg_flags |= DISSECT_IMAGE_READ_ONLY;
487+
break;
488+
481489
OPTION_FULL(OPTION_STOPS_PARSING, /* sc= */ 0, "with", NULL, "Mount, run command, unmount"):
482490
arg_action = ACTION_WITH;
483491
break;
@@ -581,6 +589,7 @@ static int parse_argv(int argc, char *argv[]) {
581589

582590
case ACTION_LIST:
583591
case ACTION_MTREE:
592+
case ACTION_MANIFEST:
584593
if (strv_length(args) != 1)
585594
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
586595
"Expected an image file or directory path as only argument.");
@@ -1328,13 +1337,272 @@ static int mtree_print_item(
13281337
return RECURSE_DIR_CONTINUE;
13291338
}
13301339

1331-
static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopDevice *d, int userns_fd) {
1340+
typedef struct HardlinkData {
1341+
int db_fd;
1342+
char *db_path;
1343+
uint64_t counter;
1344+
} HardlinkData;
1345+
1346+
static void hardlink_data_done(HardlinkData *hd) {
1347+
assert(hd);
1348+
1349+
if (hd->db_fd >= 0)
1350+
(void) rm_rf_children(TAKE_FD(hd->db_fd), REMOVE_PHYSICAL|REMOVE_REWIND, /* root_dev= */ NULL);
1351+
1352+
if (hd->db_path)
1353+
hd->db_path = rmdir_and_free(hd->db_path);
1354+
}
1355+
1356+
static int hardlink_data_lookup(
1357+
HardlinkData *hd,
1358+
const char *path,
1359+
int inode_fd,
1360+
const struct statx *sx,
1361+
uint64_t *ret_hardlink_counter,
1362+
uint8_t ret_hash[static SHA256_DIGEST_SIZE],
1363+
bool *ret_hash_valid) {
1364+
1365+
_cleanup_free_ struct file_handle *handle = NULL;
1366+
_cleanup_free_ char *slt = NULL, *fhs = NULL;
1367+
int r;
1368+
1369+
assert(hd);
1370+
assert(sx);
1371+
assert(ret_hardlink_counter);
1372+
assert(ret_hash);
1373+
assert(ret_hash_valid);
1374+
1375+
if (!FLAGS_SET(sx->stx_mask, STATX_NLINK|STATX_TYPE))
1376+
goto nope;
1377+
1378+
if (S_ISDIR(sx->stx_mode) || sx->stx_nlink <= 1)
1379+
goto nope;
1380+
1381+
if (inode_fd < 0)
1382+
goto nope;
1383+
1384+
/* This is a hardlinked inode, apparently. Let's figure out its inode token */
1385+
1386+
/* Generate a unique identifier for this inode via the kernel's FID logic. */
1387+
uint64_t unique_mnt_id = 0;
1388+
int mnt_id = 0;
1389+
r = name_to_handle_at_try_fid(inode_fd, /* path= */ NULL, &handle, &mnt_id, &unique_mnt_id, AT_EMPTY_PATH);
1390+
if (ERRNO_IS_NEG_NOT_SUPPORTED(r)) {
1391+
log_debug_errno(r, "File system does not support FIDs, not checking for hardlinks: %m");
1392+
goto nope;
1393+
}
1394+
if (r < 0)
1395+
return log_error_errno(r, "Failed to acquire FID of path: %m");
1396+
1397+
struct sha256_ctx ctx;
1398+
sha256_init_ctx(&ctx);
1399+
sha256_process_bytes(&handle->handle_bytes, sizeof(handle->handle_bytes), &ctx);
1400+
sha256_process_bytes(&handle->handle_type, sizeof(handle->handle_type), &ctx);
1401+
sha256_process_bytes(&handle->f_handle, handle->handle_bytes, &ctx);
1402+
sha256_process_bytes(&mnt_id, sizeof(mnt_id), &ctx);
1403+
sha256_process_bytes(&unique_mnt_id, sizeof(unique_mnt_id), &ctx);
1404+
1405+
uint8_t fid_hash[SHA256_DIGEST_SIZE];
1406+
sha256_finish_ctx(&ctx, fid_hash);
1407+
1408+
fhs = hexmem(fid_hash, sizeof(fid_hash));
1409+
if (!fhs)
1410+
return log_oom();
1411+
1412+
bool is_empty = false;
1413+
if (hd->db_fd < 0) {
1414+
assert(!hd->db_path);
1415+
1416+
/* Let's use a temporary directory on disk as hardlink database, so that we can
1417+
* process very large images without allocating ridiculous amounts of memory. */
1418+
1419+
const char *vtd = NULL;
1420+
r = var_tmp_dir(&vtd);
1421+
if (r < 0)
1422+
return log_error_errno(r, "Failed to determine /var/tmp/ directory: %m");
1423+
1424+
_cleanup_free_ char *template = path_join(vtd, "dissect-hardlink-db-XXXXXX");
1425+
if (!template)
1426+
return log_oom();
1427+
1428+
hd->db_fd = mkdtemp_open(template, /* flags= */ 0, &hd->db_path);
1429+
if (hd->db_fd < 0)
1430+
return log_error_errno(hd->db_fd, "Failed to create temporary directory: %m");
1431+
1432+
is_empty = true;
1433+
}
1434+
1435+
uint64_t hardlink_counter;
1436+
uint8_t hash[SHA256_DIGEST_SIZE];
1437+
bool have_hash = false;
1438+
1439+
r = is_empty ? -ENOENT : readlinkat_malloc(hd->db_fd, fhs, &slt);
1440+
if (r < 0) {
1441+
if (r != -ENOENT)
1442+
return log_error_errno(r, "Failed to read hardlink database symlink '%s': %m", fhs);
1443+
1444+
if (hd->counter >= UINT64_MAX) /* Paranoia */
1445+
return log_error_errno(SYNTHETIC_ERRNO(ERANGE), "Hardlink overflow.");
1446+
1447+
hardlink_counter = ++hd->counter;
1448+
1449+
slt = asprintf_safe("%" PRIu64, hardlink_counter);
1450+
if (!slt)
1451+
return log_oom();
1452+
1453+
if (S_ISREG(sx->stx_mode) && arg_mtree_hash) {
1454+
r = get_file_sha256(inode_fd, hash);
1455+
if (r < 0)
1456+
log_warning_errno(r, "Failed to calculate file SHA256 sum for '%s', ignoring: %m", empty_to_root(path));
1457+
else {
1458+
_cleanup_free_ char *hs = hexmem(hash, sizeof(hash));
1459+
if (!hs)
1460+
return log_oom();
1461+
1462+
if (!strextend(&slt, ":", hs))
1463+
return log_oom();
1464+
1465+
have_hash = true;
1466+
}
1467+
}
1468+
1469+
if (symlinkat(slt, hd->db_fd, fhs) < 0)
1470+
return log_error_errno(errno, "Failed to create hardlink database symlink '%s': %m", fhs);
1471+
} else {
1472+
char *colon = strchr(slt, ':');
1473+
1474+
if (colon) {
1475+
_cleanup_free_ void *unhexxed = NULL;
1476+
size_t unhexxed_size = 0;
1477+
r = unhexmem(colon + 1, &unhexxed, &unhexxed_size);
1478+
if (r < 0)
1479+
return log_error_errno(r, "Failed to unhex file hash: %m");
1480+
if (unhexxed_size != sizeof(hash))
1481+
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed hash entry: %m");
1482+
1483+
memcpy(hash, unhexxed, unhexxed_size);
1484+
have_hash = true;
1485+
*colon = 0;
1486+
}
1487+
1488+
r = safe_atou64(slt, &hardlink_counter);
1489+
if (r < 0)
1490+
return log_error_errno(r, "Failed to decode hardlink counter: %m");
1491+
}
1492+
1493+
*ret_hardlink_counter = hardlink_counter;
1494+
*ret_hash_valid = have_hash;
1495+
if (have_hash)
1496+
memcpy(ret_hash, hash, sizeof(hash));
1497+
return 1;
1498+
1499+
nope:
1500+
*ret_hardlink_counter = 0;
1501+
*ret_hash_valid = false;
1502+
return 0;
1503+
}
1504+
1505+
static int manifest_print_item(
1506+
RecurseDirEvent event,
1507+
const char *path,
1508+
int dir_fd,
1509+
int inode_fd,
1510+
const struct dirent *de,
1511+
const struct statx *sx,
1512+
void *userdata) {
1513+
1514+
HardlinkData *hd = ASSERT_PTR(userdata);
1515+
int r;
1516+
1517+
/* Generates a UAPI.16 Manifest of the directory tree */
1518+
1519+
if (!IN_SET(event, RECURSE_DIR_ENTER, RECURSE_DIR_ENTRY))
1520+
return RECURSE_DIR_CONTINUE;
1521+
1522+
bool is_root = isempty(path);
1523+
1524+
/* We ignore this to avoid input-as-output cycles (only on top-level though!) */
1525+
if (!is_root && (streq(path, "Uapi16Manifest") || startswith(path, "Uapi16Manifest.")))
1526+
return RECURSE_DIR_CONTINUE;
1527+
1528+
_cleanup_free_ char *data = NULL;
1529+
uint8_t hash[SHA256_DIGEST_SIZE];
1530+
uint64_t size = UINT64_MAX;
1531+
bool have_size = false, have_hash = false;
1532+
const char *type = NULL;
1533+
uint64_t hardlink_counter = 0;
1534+
1535+
r = hardlink_data_lookup(hd, path, inode_fd, sx, &hardlink_counter, hash, &have_hash);
1536+
if (r < 0)
1537+
return r;
1538+
1539+
if (FLAGS_SET(sx->stx_mask, STATX_TYPE)) {
1540+
switch (sx->stx_mode & S_IFMT) {
1541+
1542+
case S_IFREG:
1543+
size = sx->stx_size;
1544+
have_size = true;
1545+
1546+
if (!have_hash && arg_mtree_hash && inode_fd >= 0) {
1547+
r = get_file_sha256(inode_fd, hash);
1548+
if (r < 0)
1549+
log_warning_errno(r, "Failed to calculate file SHA256 sum for '%s', ignoring: %m", empty_to_root(path));
1550+
else
1551+
have_hash = true;
1552+
}
1553+
1554+
break;
1555+
1556+
case S_IFLNK:
1557+
if (inode_fd >= 0) {
1558+
r = readlinkat_malloc(inode_fd, "", &data);
1559+
if (r < 0)
1560+
log_warning_errno(r, "Failed to read symlink '%s', ignoring: %m", empty_to_root(path));
1561+
else {
1562+
size = strlen(data);
1563+
have_size = true;
1564+
}
1565+
}
1566+
1567+
break;
1568+
}
1569+
1570+
/* Suppress the type on the top-level entry and on regular files. */
1571+
type = !is_root && !S_ISREG(sx->stx_mode) ? inode_type_to_string(sx->stx_mode) : NULL;
1572+
}
1573+
1574+
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
1575+
r = sd_json_buildo(
1576+
&j,
1577+
SD_JSON_BUILD_PAIR_CONDITION(is_root, "mediaType", JSON_BUILD_CONST_STRING("application/vnd.uapi.16.manifest")),
1578+
SD_JSON_BUILD_PAIR_CONDITION(!is_root, "name", SD_JSON_BUILD_STRING(path)),
1579+
SD_JSON_BUILD_PAIR_CONDITION(!!type, "type", SD_JSON_BUILD_STRING(type)),
1580+
SD_JSON_BUILD_PAIR_CONDITION(have_size, "size", SD_JSON_BUILD_UNSIGNED(size)),
1581+
SD_JSON_BUILD_PAIR_CONDITION(!!data, "contents", SD_JSON_BUILD_ARRAY(SD_JSON_BUILD_OBJECT(SD_JSON_BUILD_PAIR("literal", SD_JSON_BUILD_BASE64(data, size))))),
1582+
SD_JSON_BUILD_PAIR_CONDITION(have_hash, "sha256", SD_JSON_BUILD_HEX(hash, sizeof(hash))),
1583+
SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(sx->stx_mask, STATX_TYPE) && (S_ISBLK(sx->stx_mode) || S_ISCHR(sx->stx_mode)), "major", SD_JSON_BUILD_UNSIGNED(sx->stx_rdev_major)),
1584+
SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(sx->stx_mask, STATX_TYPE) && (S_ISBLK(sx->stx_mode) || S_ISCHR(sx->stx_mode)), "minor", SD_JSON_BUILD_UNSIGNED(sx->stx_rdev_minor)),
1585+
SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(sx->stx_mask, STATX_TYPE|STATX_MODE) && !S_ISLNK(sx->stx_mode), "mode", SD_JSON_BUILD_UNSIGNED(sx->stx_mode & 07777)),
1586+
SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(sx->stx_mask, STATX_UID), "uid", SD_JSON_BUILD_UNSIGNED(sx->stx_uid)),
1587+
SD_JSON_BUILD_PAIR_CONDITION(FLAGS_SET(sx->stx_mask, STATX_GID), "gid", SD_JSON_BUILD_UNSIGNED(sx->stx_gid)),
1588+
SD_JSON_BUILD_PAIR_CONDITION(hardlink_counter > 0, "inodeToken", SD_JSON_BUILD_UNSIGNED(hardlink_counter)));
1589+
if (r < 0)
1590+
return log_error_errno(r, "Failed to build JSON object: %m");
1591+
1592+
r = sd_json_variant_dump(j, arg_json_format_flags | SD_JSON_FORMAT_SEQ | SD_JSON_FORMAT_FLUSH, stdout, /* prefix= */ NULL);
1593+
if (r < 0)
1594+
return log_error_errno(r, "Failed to write JSON output: %m");
1595+
1596+
return RECURSE_DIR_CONTINUE;
1597+
}
1598+
1599+
static int action_list_or_mtree_or_manifest_or_copy_or_make_archive(DissectedImage *m, LoopDevice *d, int userns_fd) {
13321600
_cleanup_(umount_and_freep) char *mounted_dir = NULL;
13331601
_cleanup_free_ char *t = NULL;
13341602
const char *root;
13351603
int r;
13361604

1337-
assert(IN_SET(arg_action, ACTION_LIST, ACTION_MTREE, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_MAKE_ARCHIVE, ACTION_SHIFT));
1605+
assert(IN_SET(arg_action, ACTION_LIST, ACTION_MTREE, ACTION_MANIFEST, ACTION_COPY_FROM, ACTION_COPY_TO, ACTION_MAKE_ARCHIVE, ACTION_SHIFT));
13381606

13391607
/* Determine whether to copy ownership:
13401608
* --copy-ownership=yes: always try to preserve ownership
@@ -1535,7 +1803,8 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
15351803
}
15361804

15371805
case ACTION_LIST:
1538-
case ACTION_MTREE: {
1806+
case ACTION_MTREE:
1807+
case ACTION_MANIFEST: {
15391808
_cleanup_close_ int dfd = -EBADF;
15401809

15411810
dfd = open(root, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
@@ -1548,7 +1817,18 @@ static int action_list_or_mtree_or_copy_or_make_archive(DissectedImage *m, LoopD
15481817
r = recurse_dir(dfd, NULL, 0, UINT_MAX, RECURSE_DIR_SORT, list_print_item, NULL);
15491818
else if (arg_action == ACTION_MTREE)
15501819
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);
1551-
else
1820+
else if (arg_action == ACTION_MANIFEST) {
1821+
_cleanup_(hardlink_data_done) HardlinkData hd = {
1822+
.db_fd = -EBADF,
1823+
};
1824+
r = recurse_dir(dfd,
1825+
/* path= */ NULL,
1826+
STATX_TYPE|STATX_MODE|STATX_UID|STATX_GID|STATX_SIZE|STATX_NLINK,
1827+
/* n_depth_max= */ UINT_MAX,
1828+
RECURSE_DIR_SORT|RECURSE_DIR_INODE_FD|RECURSE_DIR_TOPLEVEL,
1829+
manifest_print_item,
1830+
&hd);
1831+
} else
15521832
assert_not_reached();
15531833
if (r < 0)
15541834
return log_error_errno(r, "Failed to list image: %m");
@@ -2145,11 +2425,12 @@ static int run(int argc, char *argv[]) {
21452425

21462426
case ACTION_LIST:
21472427
case ACTION_MTREE:
2428+
case ACTION_MANIFEST:
21482429
case ACTION_COPY_FROM:
21492430
case ACTION_COPY_TO:
21502431
case ACTION_MAKE_ARCHIVE:
21512432
case ACTION_SHIFT:
2152-
return action_list_or_mtree_or_copy_or_make_archive(m, d, userns_fd);
2433+
return action_list_or_mtree_or_manifest_or_copy_or_make_archive(m, d, userns_fd);
21532434

21542435
case ACTION_WITH:
21552436
return action_with(m, d);

0 commit comments

Comments
 (0)