From 19b8d6d835ecd0ed146a41cf3264703e106fb249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20A=2E=20Fern=C3=A1ndez?= Date: Thu, 17 Oct 2024 20:24:42 -0300 Subject: [PATCH] apfsck: verify order of checkpoint mappings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I have recently noticed that Apple refuses to acknowledge drives that were formatted by my mkfs, unless my driver wrote to them at least once. The official fsck does not report any corruption, but after some experiments it seems that the problem is that checkpoint mappings are supposed to be in order, and not leave any holes. For containers with a multiblock spaceman, the object itself is supposed to wrap around, again leaving no holes. When the official driver mounts a 7T container that wrapped around precisely when my driver unmounted it, the last transaction gets silently discarded. This should be extremely rare, but I need to fix it. Start by putting the proper checks in place for the mappings. Signed-off-by: Ernesto A. Fernández --- apfsck/object.c | 2 +- apfsck/super.c | 131 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/apfsck/object.c b/apfsck/object.c index d48f892..2c64e93 100644 --- a/apfsck/object.c +++ b/apfsck/object.c @@ -237,7 +237,7 @@ static void free_cpoint_map(struct htable_entry *entry) struct cpoint_map *map = (struct cpoint_map *)entry; u32 blk_count = map->m_size >> sb->s_blocksize_bits; u64 obj_start = map->m_paddr; - u64 obj_end = map->m_paddr + blk_count; /* Objects can't wrap, right? */ + u64 obj_end = map->m_paddr + blk_count; /* Objects can wrap. TODO! */ u64 data_start = sb->s_data_base; u64 data_end = sb->s_data_base + sb->s_data_blocks; u64 valid_start; diff --git a/apfsck/super.c b/apfsck/super.c index 309ea5e..6c3640e 100644 --- a/apfsck/super.c +++ b/apfsck/super.c @@ -1334,11 +1334,28 @@ static void parse_main_super(struct super_block *sb) report("Container superblock", "next transaction id is wrong."); } +struct checkpoint_info { + u64 desc_base; + u64 data_base; + u32 desc_blocks; + u32 data_blocks; + u32 desc_next; + u32 data_next; + u32 desc_index; + u32 data_index; + u32 desc_len; + u32 data_len; +}; + /** * parse_cpoint_map - Parse and verify a checkpoint mapping - * @raw: the raw checkpoint mapping + * @raw: the raw checkpoint mapping + * @cp_info: information about the checkpoint areas + * @idx: expected index for the mapping + * + * Returns the expected index for the next mapping. */ -static void parse_cpoint_map(struct apfs_checkpoint_mapping *raw) +static u32 parse_cpoint_map(struct apfs_checkpoint_mapping *raw, struct checkpoint_info *cp_info, u32 idx) { struct cpoint_map *map; @@ -1348,6 +1365,8 @@ static void parse_cpoint_map(struct apfs_checkpoint_mapping *raw) if (!raw->cpm_paddr) report("Checkpoint map", "invalid physical address."); map->m_paddr = le64_to_cpu(raw->cpm_paddr); + if (map->m_paddr != cp_info->data_base + idx) + report("Chekpoint map", "out of order or with holes."); map->m_type = le32_to_cpu(raw->cpm_type); map->m_subtype = le32_to_cpu(raw->cpm_subtype); @@ -1364,23 +1383,26 @@ static void parse_cpoint_map(struct apfs_checkpoint_mapping *raw) report("Checkpoint map", "non-zero padding."); if (raw->cpm_fs_oid) report_unknown("Ephemeral object belonging to a volume"); + + idx += map->m_size >> sb->s_blocksize_bits; + return idx % cp_info->data_blocks; } /** * parse_cpoint_map_blocks - Parse and verify a checkpoint's mapping blocks - * @desc_base: first block of the checkpoint descriptor area - * @desc_blocks: block count of the checkpoint descriptor area - * @index: index of the first mapping block for the checkpoint + * @cp_info: information about the checkpoint areas + * @index: index of the first mapping block for the checkpoint * * Returns the number of checkpoint-mapping blocks, and sets @index to the * index of their checkpoint superblock. */ -static u32 parse_cpoint_map_blocks(u64 desc_base, u32 desc_blocks, u32 *index) +static u32 parse_cpoint_map_blocks(struct checkpoint_info *cp_info, u32 *index) { struct object obj; struct apfs_checkpoint_map_phys *raw; u32 blk_count = 0; u32 cpm_count; + u32 obj_idx; /* * The current superblock hasn't been parsed yet, so this xid would be @@ -1391,8 +1413,9 @@ static u32 parse_cpoint_map_blocks(u64 desc_base, u32 desc_blocks, u32 *index) assert(!sb->s_cpoint_map_table); sb->s_cpoint_map_table = alloc_htable(); + obj_idx = cp_info->data_index; while (1) { - u64 bno = desc_base + *index; + u64 bno = cp_info->desc_base + *index; u32 flags; int i; @@ -1418,21 +1441,61 @@ static u32 parse_cpoint_map_blocks(u64 desc_base, u32 desc_blocks, u32 *index) sb->s_blocksize) report("Checkpoint maps", "won't fit in block."); for (i = 0; i < cpm_count; ++i) - parse_cpoint_map(&raw->cpm_map[i]); + obj_idx = parse_cpoint_map(&raw->cpm_map[i], cp_info, obj_idx); flags = le32_to_cpu(raw->cpm_flags); munmap(raw, obj.size); blk_count++; - *index = (*index + 1) % desc_blocks; + *index = (*index + 1) % cp_info->desc_blocks; if ((flags & APFS_CHECKPOINT_MAP_LAST) != flags) report("Checkpoint map", "invalid flag in use."); if (flags & APFS_CHECKPOINT_MAP_LAST) - return blk_count; - if (blk_count == desc_blocks) + break; + if (blk_count == cp_info->desc_blocks) report("Checkpoint", "no mapping block marked last."); } + + if (obj_idx != cp_info->data_next) + report("Checkpoint maps", "overlap or have holes."); + return blk_count; +} + +/** + * preread_checkpoint_info - Read the superblock fields that are needed early on + * @nx_sb_copy: superblock backup on block zero + * @info: on return, information about the checkpoint areas + */ +static void preread_checkpoint_info(struct apfs_nx_superblock *nx_sb_copy, struct checkpoint_info *info) +{ + struct apfs_nx_superblock *msb_raw_latest = NULL; + + /* We want to mount the latest valid checkpoint among the descriptors */ + info->desc_base = le64_to_cpu(nx_sb_copy->nx_xp_desc_base); + if (info->desc_base >> 63 != 0) { + /* The highest bit is set when checkpoints are not contiguous */ + report("Block zero", "checkpoint descriptor tree not yet supported."); + } + info->desc_blocks = le32_to_cpu(nx_sb_copy->nx_xp_desc_blocks); + if (info->desc_blocks > 10000) /* Arbitrary loop limit, is it enough? */ + report("Block zero", "too many checkpoint descriptors?"); + + info->data_base = le64_to_cpu(nx_sb_copy->nx_xp_data_base); + info->data_blocks = le32_to_cpu(nx_sb_copy->nx_xp_data_blocks); + + /* Find the valid range, as reported by the latest descriptor */ + msb_raw_latest = read_latest_super(info->desc_base, info->desc_blocks); + info->desc_next = le32_to_cpu(msb_raw_latest->nx_xp_desc_next); + info->desc_index = le32_to_cpu(msb_raw_latest->nx_xp_desc_index); + if (info->desc_next >= info->desc_blocks || info->desc_index >= info->desc_blocks) + report("Checkpoint superblock", "out of range checkpoint descriptors."); + info->data_next = le32_to_cpu(msb_raw_latest->nx_xp_data_next); + info->data_index = le32_to_cpu(msb_raw_latest->nx_xp_data_index); + if (info->data_next >= info->data_blocks || info->data_index >= info->data_blocks) + report("Checkpoint superblock", "out of range checkpoint data."); + munmap(msb_raw_latest, sb->s_blocksize); + msb_raw_latest = NULL; } /** @@ -1440,11 +1503,10 @@ static u32 parse_cpoint_map_blocks(u64 desc_base, u32 desc_blocks, u32 *index) */ void parse_filesystem(void) { - struct apfs_nx_superblock *msb_raw_copy, *msb_raw_latest; - u64 desc_base; - u32 desc_blocks; + struct apfs_nx_superblock *msb_raw_copy; + struct checkpoint_info cp_info = {0}; long long valid_blocks; - u32 desc_next, desc_index, index; + u32 index; sb = calloc(1, sizeof(*sb)); if (!sb) @@ -1453,33 +1515,19 @@ void parse_filesystem(void) /* Read the superblock from the last clean unmount */ msb_raw_copy = read_super_copy(); - /* We want to mount the latest valid checkpoint among the descriptors */ - desc_base = le64_to_cpu(msb_raw_copy->nx_xp_desc_base); - if (desc_base >> 63 != 0) { - /* The highest bit is set when checkpoints are not contiguous */ - report("Block zero", - "checkpoint descriptor tree not yet supported."); - } - desc_blocks = le32_to_cpu(msb_raw_copy->nx_xp_desc_blocks); - if (desc_blocks > 10000) /* Arbitrary loop limit, is it enough? */ - report("Block zero", "too many checkpoint descriptors?"); - - /* Find the valid range, as reported by the latest descriptor */ - msb_raw_latest = read_latest_super(desc_base, desc_blocks); - desc_next = le32_to_cpu(msb_raw_latest->nx_xp_desc_next); - desc_index = le32_to_cpu(msb_raw_latest->nx_xp_desc_index); - if (desc_next >= desc_blocks || desc_index >= desc_blocks) - report("Checkpoint superblock", - "out of range checkpoint descriptors."); - munmap(msb_raw_latest, sb->s_blocksize); - msb_raw_latest = NULL; + /* + * We'll read the superblock in full only after going through the + * checkpoint mapping blocks, so we need to know a few fields related + * to the checkpoints ahead of time. + */ + preread_checkpoint_info(msb_raw_copy, &cp_info); /* * Now go through the valid checkpoints one by one, though it seems * that cleanly unmounted filesystems only preserve the last one. */ - index = desc_index; - valid_blocks = (desc_blocks + desc_next - desc_index) % desc_blocks; + index = cp_info.desc_index; + valid_blocks = (cp_info.desc_blocks + cp_info.desc_next - cp_info.desc_index) % cp_info.desc_blocks; while (valid_blocks > 0) { struct object obj; struct apfs_nx_superblock *raw; @@ -1497,11 +1545,10 @@ void parse_filesystem(void) sb->s_tier2_bitmap = NULL; /* The checkpoint-mapping blocks come before the superblock */ - map_blocks = parse_cpoint_map_blocks(desc_base, desc_blocks, - &index); + map_blocks = parse_cpoint_map_blocks(&cp_info, &index); valid_blocks -= map_blocks; - bno = desc_base + index; + bno = cp_info.desc_base + index; raw = read_object_nocheck(bno, sb->s_blocksize, &obj); if (parse_object_flags(obj.flags, false) != APFS_OBJ_EPHEMERAL) report("Checkpoint superblock", "bad storage type."); @@ -1520,7 +1567,7 @@ void parse_filesystem(void) parse_main_super(sb); /* Do this now, after parse_main_super() allocated the bitmap */ - container_bmap_mark_as_used(desc_base, desc_blocks); + container_bmap_mark_as_used(cp_info.desc_base, cp_info.desc_blocks); container_bmap_mark_as_used(sb->s_data_base, sb->s_data_blocks); check_container(sb); @@ -1529,7 +1576,7 @@ void parse_filesystem(void) sb->s_cpoint_map_table = NULL; /* One more block for the checkpoint superblock itself */ - index = (index + 1) % desc_blocks; + index = (index + 1) % cp_info.desc_blocks; valid_blocks--; }