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