Skip to content

Commit a2199c4

Browse files
authored
Fix support for long non-residential symlinks (#35)
Fixed issue where the targets of non-residential symlinks were not read completely.
1 parent 02c5531 commit a2199c4

File tree

4 files changed

+34
-17
lines changed

4 files changed

+34
-17
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ tests/data/xfs_symlink_test3.bin.gz filter=lfs diff=lfs merge=lfs -text
44
tests/data/xfs_bigtime.bin.gz filter=lfs diff=lfs merge=lfs -text
55
tests/data/xfs.bin.gz filter=lfs diff=lfs merge=lfs -text
66
tests/data/xfs_sparse.bin.gz filter=lfs diff=lfs merge=lfs -text
7+
tests/data/xfs_symlink_long.bin.gz filter=lfs diff=lfs merge=lfs -text

dissect/xfs/xfs.py

+26-16
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import stat
77
from datetime import datetime
88
from functools import lru_cache
9-
from typing import BinaryIO, Iterator, Optional, Union
9+
from typing import BinaryIO, Iterator
1010
from uuid import UUID
1111

1212
from dissect.util import ts
@@ -55,7 +55,7 @@ def __init__(self, fh: BinaryIO):
5555

5656
self.root = self.get_inode(self.sb.sb_rootino)
5757

58-
def get(self, path: Union[int, str], node: Optional[INode] = None) -> INode:
58+
def get(self, path: int | str, node: INode | None = None) -> INode:
5959
if isinstance(path, int):
6060
return self.get_inode(path)
6161

@@ -102,14 +102,14 @@ def walk_extents(self, block: int) -> Iterator[tuple[int, int, int, int]]:
102102
for record in self.walk_large_tree(block, 16, (c_xfs.XFS_BMAP_MAGIC, c_xfs.XFS_BMAP_CRC_MAGIC)):
103103
yield parse_fsblock(record)
104104

105-
def walk_large_tree(self, block: int, leaf_size: int, magic: Optional[list[int]] = None) -> Iterator[bytes]:
105+
def walk_large_tree(self, block: int, leaf_size: int, magic: list[int] | None = None) -> Iterator[bytes]:
106106
self.fh.seek(block * self.block_size)
107107
root = self._lblock_s(self.fh)
108108

109109
yield from self._walk_large_tree(root, leaf_size, magic)
110110

111111
def walk_small_tree(
112-
self, block: int, agnum: int, leaf_size: int, magic: Optional[list[int]] = None
112+
self, block: int, agnum: int, leaf_size: int, magic: list[int] | None = None
113113
) -> Iterator[bytes]:
114114
block = agnum * self.sb.sb_agblocks + block
115115
self.fh.seek(block * self.block_size)
@@ -122,7 +122,7 @@ def _walk_small_tree(
122122
node: c_xfs.xfs_btree_sblock | c_xfs.xfs_btree_sblock_crc,
123123
leaf_size: int,
124124
agnum: int,
125-
magic: Optional[list[int]] = None,
125+
magic: list[int] | None = None,
126126
) -> Iterator[bytes]:
127127
fh = self.fh
128128
if magic and node.bb_magic not in magic:
@@ -148,7 +148,7 @@ def _walk_large_tree(
148148
self,
149149
node: c_xfs.xfs_btree_lblock | c_xfs.xfs_btree_lblock_crc,
150150
leaf_size: int,
151-
magic: Optional[list[int]] = None,
151+
magic: list[int] | None = None,
152152
) -> Iterator[bytes]:
153153
fh = self.fh
154154
if magic and node.bb_magic not in magic:
@@ -210,9 +210,9 @@ def __init__(self, xfs: XFS, fh: BinaryIO, num: int):
210210
def get_inode(
211211
self,
212212
inum: int,
213-
filename: Optional[str] = None,
214-
filetype: Optional[int] = None,
215-
parent: Optional[INode] = None,
213+
filename: str | None = None,
214+
filetype: int | None = None,
215+
parent: INode | None = None,
216216
lazy: bool = False,
217217
) -> INode:
218218
inode = INode(self, inum, filename, filetype, parent=parent)
@@ -230,7 +230,7 @@ def walk_extents(self, fsb: int) -> Iterator[tuple[int, int, int, int]]:
230230
def walk_agi(self) -> Iterator[c_xfs.xfs_inobt_rec]:
231231
yield from self.xfs.walk_agi(self.agi.agi_root, self.num)
232232

233-
def walk_tree(self, fsb: int, magic: Optional[list[int]] = None, small: bool = False):
233+
def walk_tree(self, fsb: int, magic: list[int] | None = None, small: bool = False) -> Iterator[bytes]:
234234
agnum, blknum = fsb_to_bb(fsb, self.sb.sb_agblklog)
235235
block = agnum * self.xfs.sb.sb_agblocks + blknum
236236

@@ -245,10 +245,10 @@ def __init__(
245245
self,
246246
ag: AllocationGroup,
247247
inum: int,
248-
filename: Optional[str] = None,
249-
filetype: Optional[int] = None,
250-
parent: Optional[INode] = None,
251-
):
248+
filename: str | None = None,
249+
filetype: int | None = None,
250+
parent: INode | None = None,
251+
) -> None:
252252
self.ag = ag
253253
self.xfs = ag.xfs
254254
self.inum = inum + (ag.num << ag._inum_bits)
@@ -325,7 +325,17 @@ def link(self) -> str:
325325

326326
if not self._link:
327327
if self.inode.di_format != c_xfs.xfs_dinode_fmt.XFS_DINODE_FMT_LOCAL and self.xfs.version == 5:
328-
fh = self.open()
328+
# Almost always, symlinks (max size of 1024) fit within a block. If the block size if 512, we might
329+
# need three blocks. These three blocks could theoretially be distributed over multiple extents.
330+
# Linux kernel handles this by using sl_offset to piece the symlink back together.
331+
# As this edge case of an edge case is very unlikely, it is unsupported until we observe it.
332+
# Ticket: https://github.com/fox-it/dissect.xfs/issues/36
333+
if len(self.dataruns()) > 1:
334+
raise NotImplementedError(f"{self!r} has a symlink distributed over multiple extents")
335+
336+
# We do not use open because for non-resident symlinks self.size does not include the symlink header
337+
symlink_size = len(c_xfs.xfs_dsymlink_hdr) + self.size
338+
fh = RunlistStream(self.xfs.fh, self.dataruns(), symlink_size, self.xfs.block_size)
329339

330340
header = c_xfs.xfs_dsymlink_hdr(fh)
331341
if header.sl_magic != c_xfs.XFS_SYMLINK_MAGIC:
@@ -514,7 +524,7 @@ def attrfork(self) -> BinaryIO:
514524

515525
return RangeStream(self._buf, offset, size)
516526

517-
def dataruns(self) -> list[tuple[Optional[int], int]]:
527+
def dataruns(self) -> list[tuple[int | None, int]]:
518528
if not self._runlist:
519529
runs = []
520530
run_offset = 0

tests/data/xfs_symlink_long.bin.gz

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:ff94bd598afb51a98eb02295d584cfc34a59dd5fde6f7161bf32c5ef1ac20b3e
3+
size 50458

tests/test_xfs.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def test_xfs_bigtime(xfs_bigtime_bin):
7373
("tests/data/xfs_symlink_test1.bin.gz"),
7474
("tests/data/xfs_symlink_test2.bin.gz"),
7575
("tests/data/xfs_symlink_test3.bin.gz"),
76+
("tests/data/xfs_symlink_long.bin.gz"),
7677
],
7778
)
7879
def test_symlinks(image_file):
@@ -85,4 +86,6 @@ def resolve(node):
8586
return node
8687

8788
with gzip.open(image_file, "rb") as disk:
88-
assert resolve(XFS(disk).get(path)).open().read() == expect
89+
link_inode = resolve(XFS(disk).get(path))
90+
assert link_inode.nblocks == 1
91+
assert link_inode.open().read() == expect

0 commit comments

Comments
 (0)