From 68461b35f827f6edede64896a4cf261881c57d5a Mon Sep 17 00:00:00 2001 From: Quentin Kaiser Date: Tue, 1 Sep 2026 09:13:25 +0200 Subject: [PATCH 1/2] fix: do not follow symlinks when setting permissions --- ubireader/ubifs/output.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ubireader/ubifs/output.py b/ubireader/ubifs/output.py index 53ad86a..ec61cd9 100755 --- a/ubireader/ubifs/output.py +++ b/ubireader/ubifs/output.py @@ -169,8 +169,9 @@ def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.de def _set_file_perms(path, inode): - os.chown(path, inode['ino'].uid, inode['ino'].gid) - os.chmod(path, inode['ino'].mode) + os.chown(path, inode['ino'].uid, inode['ino'].gid, follow_symlinks=False) + if not os.path.islink(path): + os.chmod(path, inode['ino'].mode) verbose_log(_set_file_perms, 'perms:%s, owner: %s.%s, path: %s' % (inode['ino'].mode, inode['ino'].uid, inode['ino'].gid, path)) def _set_file_timestamps(path, inode): From b8b8bad0e79cdcc2c8a7cbe11f1688886a068dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rikard=20S=C3=B6derstr=C3=B6m?= Date: Mon, 31 Aug 2026 12:26:39 +0200 Subject: [PATCH 2/2] feat: restore extended attributes during file extraction ubireader_extract_files already parses each inode's xent (xattr entry) nodes while walking the filesystem, but silently discards them - no extended attribute is ever written to an extracted file, regardless of what the source UBIFS image actually contains (e.g. security.ima/ security.evm signatures, SMACK labels). Add a --preserve-xattr flag (mirroring the existing --keep-permissions one, also root-only) that resolves each xent to its target inode and writes it via os.setxattr(). Xattr values are stored inline in their own inode's data field rather than as separate UBIFS_DATA_KEY nodes - the same mechanism decrypt_symlink_target() already relies on for symlink targets - so extraction reuses that same access pattern rather than the regular-file data-node reader. The extraction root's timestamps, ownership, and mode are restored before its xattrs. security.evm is always written after all other xattrs. --- ubireader/scripts/ubireader_extract_files.py | 13 ++- ubireader/ubifs/output.py | 88 ++++++++++++++++++-- 2 files changed, 92 insertions(+), 9 deletions(-) diff --git a/ubireader/scripts/ubireader_extract_files.py b/ubireader/scripts/ubireader_extract_files.py index a5bc154..20db4d6 100755 --- a/ubireader/scripts/ubireader_extract_files.py +++ b/ubireader/scripts/ubireader_extract_files.py @@ -53,6 +53,9 @@ def main(): parser.add_argument('-k', '--keep-permissions', action='store_true', dest='permissions', help='Maintain file permissions, requires running as root. (default: False)') + parser.add_argument('-x', '--preserve-xattr', action='store_true', dest='xattrs', + help='Restore extended attributes and file metadata (e.g. security.ima/security.evm), requires running as root. (default: False)') + parser.add_argument('-l', '--log', action='store_true', dest='log', help='Print extraction information to screen.') @@ -157,7 +160,11 @@ def main(): if not block_size: parser.error('Block size could not be determined.') - perms = args.permissions + # EVM attributes authenticate ownership and mode. Enable metadata + # preservation whenever xattrs are requested so a restored EVM signature + # is valid even if --keep-permissions was omitted. + perms = args.permissions or args.xattrs + xattrs = args.xattrs # Create file object. ufile_obj = ubi_file(path, block_size, start_offset, end_offset) @@ -197,7 +204,7 @@ def main(): # Extract files from UBI image. ubifs_obj = ubifs(lebv_file, master_key=master_key) print('Extracting files to: %s' % vol_outpath) - extract_files(ubifs_obj, vol_outpath, perms) + extract_files(ubifs_obj, vol_outpath, perms, xattrs) elif filetype == UBIFS_NODE_MAGIC: @@ -209,7 +216,7 @@ def main(): # Extract files from UBIFS image. print('Extracting files to: %s' % outpath) - extract_files(ubifs_obj, outpath, perms) + extract_files(ubifs_obj, outpath, perms, xattrs) else: print('Something went wrong to get here.') diff --git a/ubireader/ubifs/output.py b/ubireader/ubifs/output.py index ec61cd9..380261e 100755 --- a/ubireader/ubifs/output.py +++ b/ubireader/ubifs/output.py @@ -40,14 +40,24 @@ def is_safe_path(basedir: str, path: str) -> bool: return True if path.startswith(basedir) else False -def extract_files(ubifs: Ubifs, out_path: str, perms: bool = False) -> None: +def extract_files(ubifs: Ubifs, out_path: str, perms: bool = False, xattrs: bool = False) -> None: """Extract UBIFS contents to_path/ Arguments: Obj:ubifs -- UBIFS object. Str:out_path -- Path to extract contents to. + Bool:perms -- Restore file owner/group/mode, requires running as root. + Bool:xattrs -- Restore extended attributes (e.g. security.*). This also + restores file metadata, because security.evm signs it; + requires running as root. """ try: + # security.evm covers inode metadata, so an EVM xattr is only useful + # when ownership and mode are restored as well. Keep this invariant + # for callers of this API in addition to the command-line interface. + if xattrs: + perms = True + inodes: dict[int, Inode] = {} bad_blocks: list[int] = [] @@ -57,7 +67,16 @@ def extract_files(ubifs: Ubifs, out_path: str, perms: bool = False) -> None: raise Exception('No inodes found') for dent in inodes[1]['dent']: - extract_dents(ubifs, inodes, dent, out_path, perms) + extract_dents(ubifs, inodes, dent, out_path, perms, xattrs) + + # The output directory represents the UBIFS root inode. Its metadata + # must be in place before security.evm is written. + _set_file_timestamps(out_path, inodes[1]) + if perms: + _set_file_perms(out_path, inodes[1]) + + if xattrs: + _write_xattrs(ubifs, out_path, inodes[1], inodes) if len(bad_blocks): error(extract_files, 'Warn', 'Data may be missing or corrupted, bad blocks, LEB [%s]' % ','.join(map(str, bad_blocks))) @@ -66,7 +85,7 @@ def extract_files(ubifs: Ubifs, out_path: str, perms: bool = False) -> None: error(extract_files, 'Error', '%s' % e) -def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.dent_node, path: str = '', perms: bool = False) -> None: +def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.dent_node, path: str = '', perms: bool = False, xattrs: bool = False) -> None: if dent_node.inum not in inodes: error(extract_dents, 'Error', 'inum: %s not found in inodes' % (dent_node.inum)) return @@ -91,10 +110,13 @@ def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.de if 'dent' in inode: for dnode in inode['dent']: - extract_dents(ubifs, inodes, dnode, dent_path, perms) + extract_dents(ubifs, inodes, dnode, dent_path, perms, xattrs) _set_file_timestamps(dent_path, inode) + if xattrs: + _write_xattrs(ubifs, dent_path, inode, inodes) + elif dent_node.type == UBIFS_ITYPE_REG: try: if inode['ino'].nlink > 1: @@ -114,6 +136,9 @@ def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.de if perms: _set_file_perms(dent_path, inode) + if xattrs: + _write_xattrs(ubifs, dent_path, inode, inodes) + except Exception as e: error(extract_dents, 'Warn', 'FILE Fail: %s' % e) @@ -126,8 +151,14 @@ def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.de _set_file_timestamps(dent_path, inode) log(extract_dents, 'Make Symlink: %s > %s' % (dent_path, inode['ino'].data)) + if perms: + _set_file_perms(dent_path, inode) + + if xattrs: + _write_xattrs(ubifs, dent_path, inode, inodes) + except Exception as e: - error(extract_dents, 'Warn', 'SYMLINK Fail: %s' % e) + error(extract_dents, 'Warn', 'SYMLINK Fail: %s' % e) elif dent_node.type in [UBIFS_ITYPE_BLK, UBIFS_ITYPE_CHR]: try: @@ -136,15 +167,25 @@ def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.de os.mknod(dent_path, inode['ino'].mode, dev) log(extract_dents, 'Make Device Node: %s' % (dent_path)) + _set_file_timestamps(dent_path, inode) + if perms: _set_file_perms(dent_path, inode) + + if xattrs: + _write_xattrs(ubifs, dent_path, inode, inodes) else: log(extract_dents, 'Create dummy device.') _write_reg_file(dent_path, str(dev)) + _set_file_timestamps(dent_path, inode) + if perms: _set_file_perms(dent_path, inode) - + + if xattrs: + _write_xattrs(ubifs, dent_path, inode, inodes) + except Exception as e: error(extract_dents, 'Warn', 'DEV Fail: %s' % e) @@ -153,8 +194,13 @@ def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.de os.mkfifo(dent_path, inode['ino'].mode) log(extract_dents, 'Make FIFO: %s' % (path)) + _set_file_timestamps(dent_path, inode) + if perms: _set_file_perms(dent_path, inode) + + if xattrs: + _write_xattrs(ubifs, dent_path, inode, inodes) except Exception as e: error(extract_dents, 'Warn', 'FIFO Fail: %s : %s' % (dent_path, e)) @@ -162,12 +208,42 @@ def extract_dents(ubifs: Ubifs, inodes: Mapping[int, Inode], dent_node: nodes.de try: if settings.use_dummy_socket_file: _write_reg_file(dent_path, '') + _set_file_timestamps(dent_path, inode) if perms: _set_file_perms(dent_path, inode) + + if xattrs: + _write_xattrs(ubifs, dent_path, inode, inodes) except Exception as e: error(extract_dents, 'Warn', 'SOCK Fail: %s : %s' % (dent_path, e)) +def _write_xattrs(ubifs, path, inode, inodes): + if 'xent' not in inode: + return + + # EVM authenticates other protected xattrs (for example security.ima and + # security.capability). UBIFS xent order is not significant, therefore + # always install its signature last. + xents = sorted(inode['xent'], key=lambda xent: xent.name == 'security.evm') + + for xent in xents: + if xent.inum not in inodes: + error(_write_xattrs, 'Warn', 'xattr inum: %s not found in inodes' % (xent.inum)) + continue + + # Xattr values are stored inline in their own inode's data field, + # the same way symlink targets are (see decrypt_symlink_target), + # rather than as separate UBIFS_DATA_KEY nodes. + xattr_inode = inodes[xent.inum] + value = xattr_inode['ino'].data[:xattr_inode['ino'].size] + + try: + os.setxattr(path, xent.name, value, follow_symlinks=False) + verbose_log(_write_xattrs, 'xattr: %s (%s bytes), path: %s' % (xent.name, len(value), path)) + except OSError as e: + error(_write_xattrs, 'Warn', 'XATTR Fail: %s: %s' % (xent.name, e)) + def _set_file_perms(path, inode): os.chown(path, inode['ino'].uid, inode['ino'].gid, follow_symlinks=False) if not os.path.islink(path):