Skip to content

Commit 0a1a529

Browse files
authored
Improve checkpoint superblock selection (#3)
1 parent ecf5c41 commit 0a1a529

File tree

5 files changed

+89
-7
lines changed

5 files changed

+89
-7
lines changed

dissect/apfs/apfs.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import logging
4+
import os
35
from typing import TYPE_CHECKING, BinaryIO
46

57
from dissect.apfs.c_apfs import c_apfs
@@ -11,6 +13,9 @@
1113
from dissect.apfs.objects.fs import FS
1214
from dissect.apfs.objects.keybag import ContainerKeybag
1315

16+
log = logging.getLogger(__name__)
17+
log.setLevel(os.getenv("DISSECT_LOG_APFS", "CRITICAL"))
18+
1419

1520
class APFS:
1621
"""Container class for APFS operations.
@@ -24,8 +29,29 @@ def __init__(self, fh: BinaryIO):
2429
self.fh.seek(0)
2530

2631
self.sb = NxSuperblock.from_block(self, 0, self.fh.read(c_apfs.NX_DEFAULT_BLOCK_SIZE))
27-
self.sbs = [self.sb] + [obj for obj in self.sb.checkpoint_objects if isinstance(obj, NxSuperblock)]
28-
self.sb = sorted(self.sbs, key=lambda obj: obj.xid)[-1]
32+
self.sb.check()
33+
34+
self.sbs = sorted(
35+
[obj for obj in self.sb.checkpoint_objects if isinstance(obj, NxSuperblock)],
36+
key=lambda obj: obj.xid,
37+
reverse=True,
38+
)
39+
40+
# TODO: Do more accurate checkpoint traversal
41+
for sb in self.sbs:
42+
try:
43+
sb.check()
44+
sb.compare(self.sb)
45+
except Exception as e:
46+
log.debug("Skipping superblock xid=%d: %s", sb.xid, e)
47+
continue
48+
49+
if not sb.omap.is_valid():
50+
log.debug("Skipping superblock xid=%d: invalid OMAP", sb.xid)
51+
continue
52+
53+
self.sb = sb
54+
break
2955

3056
@property
3157
def block_size(self) -> int:

dissect/apfs/objects/nx_superblock.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,41 @@ class NxSuperblock(Object):
2929
__struct__ = c_apfs.nx_superblock
3030
object: c_apfs.nx_superblock
3131

32-
def __init__(self, *args, **kwargs):
33-
super().__init__(*args, **kwargs)
32+
def check(self) -> None:
33+
"""Check the validity of the superblock."""
34+
if not self.is_valid():
35+
raise Error("Invalid nx_superblock checksum")
36+
37+
if self.type != c_apfs.OBJECT_TYPE.NX_SUPERBLOCK:
38+
raise Error("Invalid nx_superblock type")
39+
40+
if not self.is_ephemeral:
41+
raise Error("Invalid nx_superblock storage type")
3442

3543
if self.object.nx_magic.to_bytes(4, "big") != c_apfs.NX_MAGIC:
3644
raise Error(
3745
"Invalid nx_superblock magic "
3846
f"(expected {c_apfs.NX_MAGIC!r}, got {self.object.nx_magic.to_bytes(4, 'big')!r})"
3947
)
4048

49+
def compare(self, other: NxSuperblock) -> None:
50+
"""Compare this superblock to another superblock."""
51+
if self.header.o_xid < other.header.o_xid:
52+
raise Error("Lower xid than other superblock")
53+
54+
for attr in (
55+
"nx_uuid",
56+
"nx_fusion_uuid",
57+
"nx_block_size",
58+
"nx_block_count",
59+
"nx_xp_desc_blocks",
60+
"nx_xp_data_blocks",
61+
"nx_xp_desc_base",
62+
"nx_xp_data_base",
63+
):
64+
if getattr(self.object, attr) != getattr(other.object, attr):
65+
raise Error(f"Mismatch on {attr}")
66+
4167
@cached_property
4268
def block_size(self) -> int:
4369
"""The block size of the container."""
@@ -66,14 +92,20 @@ def uuid(self) -> UUID:
6692
@cached_property
6793
def checkpoint_objects(self) -> list[CheckpointMap | NxSuperblock]:
6894
"""All checkpoint objects in the container."""
69-
return list(_read_checkpoint_objects(self.container, self.object.nx_xp_desc_base, self.object.nx_xp_desc_len))
95+
# TODO: Rework this a bit to be more accurate
96+
return list(
97+
_read_checkpoint_objects(self.container, self.object.nx_xp_desc_base, self.object.nx_xp_desc_blocks)
98+
)
7099

71100
@cached_property
72101
def ephemeral_objects(self) -> dict[int, Object]:
73102
"""All ephemeral objects in the container."""
103+
# TODO: I don't think this is correct
74104
return {
75105
obj.oid: obj
76-
for obj in _read_checkpoint_objects(self.container, self.object.nx_xp_data_base, self.object.nx_xp_data_len)
106+
for obj in _read_checkpoint_objects(
107+
self.container, self.object.nx_xp_data_base, self.object.nx_xp_data_blocks
108+
)
77109
}
78110

79111
@cached_property

dissect/apfs/objects/omap.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ def __init__(self, *args, **kwargs):
2020

2121
self.lookup = lru_cache(128)(self.lookup)
2222

23+
def is_valid(self) -> bool:
24+
return (
25+
super().is_valid()
26+
and self.type == c_apfs.OBJECT_TYPE.OMAP
27+
and self.subtype == 0
28+
and self.is_physical
29+
and self.oid == self.address
30+
)
31+
2332
@cached_property
2433
def btree(self) -> BTree:
2534
"""The B-tree of the object map."""

tests/_data/corrupt.bin.gz

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:672c556cb370c4ae547e69b822d1896a2327fcb392eeba19f3d73414382cc80f
3+
size 595750

tests/test_apfs.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88

9-
from dissect.apfs.apfs import APFS
9+
from dissect.apfs.apfs import APFS, log
1010
from dissect.apfs.c_apfs import c_apfs
1111
from tests.conftest import absolute_path
1212

@@ -357,3 +357,15 @@ def test_snapshots() -> None:
357357
for i, snapshot in enumerate(volume.snapshots):
358358
assert snapshot.name == f"Snapshot {i}"
359359
assert snapshot.open().get("file").open().read() == f"Snapshot {i}\n".encode()
360+
361+
362+
def test_corrupt_checkpoints(caplog: pytest.LogCaptureFixture) -> None:
363+
"""Test APFS volumes with corrupt checkpoints."""
364+
with gzip.open(absolute_path("_data/corrupt.bin.gz"), "rb") as fh, caplog.at_level("DEBUG", log.name):
365+
container = APFS(fh)
366+
367+
assert container.sb.xid == 302
368+
assert len(container.volumes) == 1
369+
370+
assert caplog.messages[0] == "Skipping superblock xid=304: invalid OMAP"
371+
assert caplog.messages[1] == "Skipping superblock xid=303: Invalid nx_superblock checksum"

0 commit comments

Comments
 (0)