Files
Ernesto A. Fernández 9a13a8ffbe apfsck: update boundary checks for free_next array
Back when I was only working with relatively small disks I was under the
impression that ip_bm_free_next was just a fixed-length 256-bit field.
In reality it's an array of 16-bit fields, with 16 times as many entries
as the number of bitmap blocks needed to map the internal pool. Fix the
boundary checks in spaceman_256_from_off() and rename it accordingly.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2024-11-05 16:26:24 -03:00

1092 lines
35 KiB
C

/*
* Copyright (C) 2019 Ernesto A. Fernández <ernesto.mnd.fernandez@gmail.com>
*/
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <apfs/parameters.h>
#include <apfs/raw.h>
#include "apfsck.h"
#include "btree.h"
#include "key.h"
#include "object.h"
#include "spaceman.h"
#include "super.h"
/**
* block_in_ip - Does this block belong to the internal pool?
* @bno: block number to check
*/
static inline bool block_in_ip(u64 bno)
{
struct spaceman *sm = &sb->s_spaceman;
u64 start = sm->sm_ip_base;
u64 end = start + sm->sm_ip_block_count;
return bno >= start && bno < end;
}
/**
* range_in_ip - Is this range included in the internal pool?
* @paddr: first block of the range
* @length: length of the range
*/
static bool range_in_ip(u64 paddr, u64 length)
{
u64 last = paddr + length - 1;
bool first_in_ip = block_in_ip(paddr);
bool last_in_ip = block_in_ip(last);
if ((first_in_ip && !last_in_ip) || (!first_in_ip && last_in_ip))
report("Free queue record", "internal pool is overrun.");
return first_in_ip;
}
/**
* bmap_mark_as_used - Set a range to ones in a bitmap
* @bitmap: the bitmap
* @paddr: first block number
* @length: block count
*
* Checks that an address range is still zeroed in the given bitmap, and then
* switches those bits.
*/
static void bmap_mark_as_used(u64 *bitmap, u64 paddr, u64 length)
{
u64 *byte;
u64 flag;
u64 i;
for (i = paddr; i < paddr + length; ++i) {
byte = bitmap + i / 64;
flag = 1ULL << i % 64;
if (*byte & flag)
report(NULL /* context */, "A block is used twice.");
*byte |= flag;
}
}
/**
* ip_bmap_mark_as_used - Mark a range as used in the ip allocation bitmap
* @paddr: first block number
* @length: block count
*
* Checks that the given address range is still marked as free in the internal
* pool's allocation bitmap, and then switches those bits.
*/
void ip_bmap_mark_as_used(u64 paddr, u64 length)
{
if (!range_in_ip(paddr, length))
report(NULL /* context */, "Out-of-range ip block number.");
paddr -= sb->s_spaceman.sm_ip_base;
bmap_mark_as_used(sb->s_ip_bitmap, paddr, length);
}
/**
* container_bmap_mark_as_used - Mark a range as used in the allocation bitmap
* @paddr: first block number
* @length: block count
*
* Checks that the given address range is still marked as free in the
* container's allocation bitmap, and then switches those bits.
*/
void container_bmap_mark_as_used(u64 paddr, u64 length)
{
u64 tier2_blkno, max_dev_blkcnt;
bool tier2;
void *bitmap = NULL;
tier2_blkno = APFS_FUSION_TIER2_DEVICE_BYTE_ADDR >> sb->s_blocksize_bits;
tier2 = paddr >= tier2_blkno;
if (tier2)
paddr -= tier2_blkno;
max_dev_blkcnt = tier2 ? sb->s_max_tier2_blkcnt : sb->s_max_main_blkcnt;
bitmap = tier2 ? sb->s_tier2_bitmap : sb->s_main_bitmap;
/* Avoid out-of-bounds writes to the allocation bitmap */
if (paddr + length > max_dev_blkcnt || paddr + length < paddr)
report(NULL /* context */, "Out-of-range block number.");
bmap_mark_as_used(bitmap, paddr, length);
}
/**
* parse_spaceman_chunk_counts - Parse spaceman fields for chunk-related counts
* @raw: pointer to the raw spaceman structure
*
* Checks the counts of blocks per chunk, chunks per cib, and cibs per cab, and
* reads them into the in-memory container superblock.
*/
static void parse_spaceman_chunk_counts(struct apfs_spaceman_phys *raw)
{
struct spaceman *sm = &sb->s_spaceman;
int chunk_info_size = sizeof(struct apfs_chunk_info);
int cib_size = sizeof(struct apfs_chunk_info_block);
int cab_size = sizeof(struct apfs_cib_addr_block);
sm->sm_blocks_per_chunk = le32_to_cpu(raw->sm_blocks_per_chunk);
if (sm->sm_blocks_per_chunk != 8 * sb->s_blocksize)
/* One bitmap block for each chunk */
report("Space manager", "wrong count of blocks per chunk.");
sm->sm_chunks_per_cib = (sb->s_blocksize - cib_size) / chunk_info_size;
if (le32_to_cpu(raw->sm_chunks_per_cib) != sm->sm_chunks_per_cib)
report("Space manager", "wrong count of chunks per cib.");
sm->sm_cibs_per_cab = (sb->s_blocksize - cab_size) / sizeof(__le64);
if (le32_to_cpu(raw->sm_cibs_per_cab) != sm->sm_cibs_per_cab)
report("Space manager", "wrong count of cibs per cab.");
}
/**
* read_chunk_bitmap - Read a chunk's bitmap into memory
* @addr: first block number for the chunk
* @bmap: block number for the chunk's bitmap, or zero if the chunk is all free
* @dev: device that owns this chunk
*
* Returns a pointer to the chunk's bitmap, read into its proper position
* within the in-memory bitmap for the container.
*/
static void *read_chunk_bitmap(u64 addr, u64 bmap, struct spaceman_device *dev)
{
struct spaceman *sm = &sb->s_spaceman;
ssize_t read_bytes;
void *buf, *ret;
size_t count;
off_t offset;
u32 chunk_number;
assert(dev->sm_bitmap);
/* Prevent out-of-bounds writes to sm->sm_bitmap */
if (addr & (sm->sm_blocks_per_chunk - 1))
report("Chunk-info", "chunk address isn't multiple of size.");
chunk_number = addr / sm->sm_blocks_per_chunk;
if (addr >= sb->s_block_count)
report("Chunk-info", "chunk address is out of bounds.");
ret = buf = dev->sm_bitmap + chunk_number * sb->s_blocksize;
if (!bmap) /* The whole chunk is free, so leave this block as zero */
return ret;
count = sb->s_blocksize;
offset = bmap * sb->s_blocksize;
do {
read_bytes = apfs_pread(buf, count, offset);
if (read_bytes < 0)
system_error();
buf += read_bytes;
count -= read_bytes;
offset += read_bytes;
} while (read_bytes > 0);
/* Mark the bitmap block as used in the actual allocation bitmap */
ip_bmap_mark_as_used(bmap, 1 /* length */);
return ret;
}
/**
* count_chunk_free - Count the free blocks in a chunk
* @bmap: pointer to the chunk's bitmap
* @blks: number of blocks in the chunk
*/
static int count_chunk_free(void *bmap, u32 blks)
{
unsigned long long *curr, *end;
int free = blks;
end = bmap + sb->s_blocksize;
for (curr = bmap; curr < end; ++curr)
free -= __builtin_popcountll(*curr);
return free;
}
/**
* parse_chunk_info - Parse and check a chunk info structure
* @chunk: pointer to the raw chunk info structure
* @is_last: is this the last chunk of the device?
* @start: expected first block number for the chunk
* @xid: on return, the transaction id of the chunk
* @dev: device that owns this chunk
*
* Returns the first block number for the next chunk.
*/
static u64 parse_chunk_info(struct apfs_chunk_info *chunk, bool is_last, u64 start, u64 *xid, struct spaceman_device *dev)
{
struct spaceman *sm = &sb->s_spaceman;
u32 block_count;
void *bitmap;
u32 free_count;
block_count = le32_to_cpu(chunk->ci_block_count);
if (!block_count)
report("Chunk-info", "has no blocks.");
if (block_count > sm->sm_blocks_per_chunk)
report("Chunk-info", "too many blocks.");
if (!is_last && block_count != sm->sm_blocks_per_chunk)
report("Chunk-info", "too few blocks.");
dev->sm_blocks += block_count;
if (le64_to_cpu(chunk->ci_addr) != start)
report("Chunk-info block", "chunks are not consecutive.");
bitmap = read_chunk_bitmap(start, le64_to_cpu(chunk->ci_bitmap_addr), dev);
free_count = le32_to_cpu(chunk->ci_free_count);
if (free_count != count_chunk_free(bitmap, block_count))
report("Chunk-info", "wrong count of free blocks.");
dev->sm_free += free_count;
*xid = le64_to_cpu(chunk->ci_xid);
if (!*xid)
report("Chunk-info", "bad transaction id.");
return start + block_count;
}
/**
* parse_chunk_info_block - Parse and check a chunk-info block
* @bno: block number of the chunk-info block
* @index: index of the chunk-info block
* @start: expected first block number for the first chunk
* @xid_p: on return, the transaction id of the cib (ignored if NULL)
* @dev: device that owns this cib
*
* Returns the first block number for the first chunk of the next cib.
*/
static u64 parse_chunk_info_block(u64 bno, u32 index, u64 start, u64 *xid_p, struct spaceman_device *dev)
{
struct spaceman *sm = &sb->s_spaceman;
struct object obj;
struct apfs_chunk_info_block *cib;
u32 chunk_count;
bool last_cib = index == dev->sm_cib_count - 1;
u64 max_chunk_xid = 0;
int i;
cib = read_object(bno, NULL, &obj);
if (obj.type != APFS_OBJECT_TYPE_SPACEMAN_CIB)
report("Chunk-info block", "wrong object type.");
if (obj.subtype != APFS_OBJECT_TYPE_INVALID)
report("Chunk-info block", "wrong object subtype.");
if (obj.xid > sm->sm_xid) /* Cib address is stored in the spaceman */
report("Chunk-info block", "xid is more recent than spaceman.");
if (le32_to_cpu(cib->cib_index) != index)
report("Chunk-info block", "wrong index.");
chunk_count = le32_to_cpu(cib->cib_chunk_info_count);
if (!chunk_count)
report("Chunk-info block", "has no chunks.");
if (chunk_count > sm->sm_chunks_per_cib)
report("Chunk-info block", "too many chunks.");
if (!last_cib && chunk_count != sm->sm_chunks_per_cib)
report("Chunk-info block", "too few chunks.");
dev->sm_chunks += chunk_count;
for (i = 0; i < chunk_count; ++i) {
bool last_block = false;
u64 chunk_xid;
if (last_cib && i == chunk_count - 1)
last_block = true;
start = parse_chunk_info(&cib->cib_chunk_info[i], last_block, start, &chunk_xid, dev);
if (chunk_xid > obj.xid)
report("Chunk-info", "xid is too recent.");
if (chunk_xid > max_chunk_xid)
max_chunk_xid = chunk_xid;
}
if (obj.xid != max_chunk_xid) /* Cib only changes if a chunk changes */
report("Chunk-info block", "xid is too recent.");
if (xid_p)
*xid_p = obj.xid;
munmap(cib, obj.size);
return start;
}
/**
* parse_cib_addr_block - Parse and check a chunk-info address block
* @bno: block number of the chunk-info address block
* @index: index of the chunk-info address block
* @start: expected first block number for the first chunk
* @dev: device that owns this cab
*
* Returns the first block number for the first chunk of the next cab.
*/
static u64 parse_cib_addr_block(u64 bno, u32 index, u64 start, struct spaceman_device *dev)
{
struct spaceman *sm = &sb->s_spaceman;
struct object obj;
struct apfs_cib_addr_block *cab = NULL;
u32 cib_count;
bool last_cab = index == dev->sm_cab_count - 1;
u64 max_cib_xid = 0;
int i;
cab = read_object(bno, NULL, &obj);
if (obj.type != APFS_OBJECT_TYPE_SPACEMAN_CAB)
report("Cib address block", "wrong object type.");
if (obj.subtype != APFS_OBJECT_TYPE_INVALID)
report("Cib address block", "wrong object subtype.");
if (obj.xid > sm->sm_xid) /* Cab address is stored in the spaceman */
report("Cib address block", "xid is more recent than spaceman.");
if (le32_to_cpu(cab->cab_index) != index)
report("Cib address block", "wrong index.");
cib_count = le32_to_cpu(cab->cab_cib_count);
if (!cib_count)
report("Cib address block", "has no cibs.");
if (cib_count > sm->sm_cibs_per_cab)
report("Cib address block", "too many cibs.");
if (!last_cab && cib_count != sm->sm_cibs_per_cab)
report("Cib address block", "too few cibs.");
dev->sm_cibs += cib_count;
for (i = 0; i < cib_count; ++i) {
u64 cib_xid;
start = parse_chunk_info_block(le64_to_cpu(cab->cab_cib_addr[i]), sm->sm_cibs_per_cab * index + i, start, &cib_xid, dev);
if (cib_xid > obj.xid)
report("Chunk-info block", "xid is too recent.");
if (cib_xid > max_cib_xid)
max_cib_xid = cib_xid;
}
if (obj.xid != max_cib_xid) /* Cab only changes if a cib changes */
report("Cib address block", "xid is too recent.");
munmap(cab, obj.size);
return start;
}
/**
* spaceman_val_from_off - Get the value stored on a given spaceman offset
* @raw: pointer to the raw space manager
* @offset: offset of the value in @raw
*
* This is not in the official documentation and I didn't figure it out myself.
* Credit should go to Joachim Metz: <https://github.com/libyal/libfsapfs>.
*
* TODO: check that no values found by this function overlap with each other.
*/
static u64 spaceman_val_from_off(struct apfs_spaceman_phys *raw, u32 offset)
{
struct spaceman *sm = &sb->s_spaceman;
char *value_p = (char *)raw + offset;
assert(sm->sm_struct_size);
if (offset & 0x7)
report("Spaceman", "offset is not aligned to 8 bytes.");
if (offset < sm->sm_struct_size)
report("Spaceman", "offset overlaps with structure.");
if (offset >= sm->sm_obj_size || offset + sizeof(u64) > sm->sm_obj_size)
report("Spaceman", "offset is out of bounds.");
return *((u64 *)value_p);
}
/**
* spaceman_16_from_off - Get the 16 bits stored on a given spaceman offset
* @raw: pointer to the raw space manager
* @offset: offset of the value in @raw
*
* TODO: check that no values found by this function overlap with each other.
*/
static u16 spaceman_16_from_off(struct apfs_spaceman_phys *raw, u32 offset)
{
struct spaceman *sm = &sb->s_spaceman;
char *value_p = (char *)raw + offset;
assert(sm->sm_struct_size);
if (offset & 0x1)
report("Spaceman", "offset is not aligned to 2 bytes.");
if (offset < sm->sm_struct_size)
report("Spaceman", "offset overlaps with structure.");
if (offset >= sm->sm_obj_size || offset + sizeof(u16) > sm->sm_obj_size)
report("Spaceman", "offset is out of bounds.");
return *((u16 *)value_p);
}
/**
* spaceman_16_array_from_off - Get a pointer to the array on a spaceman offset
* @raw: pointer to the raw space manager
* @offset: offset of the array of 16-bit entries in @raw
* @len: number of entries in the array
*
* TODO: check that no values found by this function overlap with each other,
* and also with spaceman_val_from_off()/spaceman_16_from_off().
*/
static __le16 *spaceman_16_array_from_off(struct apfs_spaceman_phys *raw, u32 offset, u32 len)
{
struct spaceman *sm = &sb->s_spaceman;
__le16 *array_p = (void *)raw + offset;
assert(sm->sm_struct_size);
if (offset & 0x1)
report("Spaceman", "offset is not aligned to 2 bytes.");
if (offset < sm->sm_struct_size)
report("Spaceman", "offset overlaps with structure.");
if (len > UINT32_MAX / 10 || offset > UINT32_MAX / 10)
report("Spaceman", "length or offset of array are not sane.");
if (offset >= sm->sm_obj_size || offset + sizeof(__le16) * len > sm->sm_obj_size)
report("Spaceman", "offset is out of bounds.");
return array_p;
}
/**
* parse_spaceman_device - Parse and check a spaceman's device struct
* @raw: pointer to the raw space manager
* @which: device to check
*/
static void parse_spaceman_device(struct apfs_spaceman_phys *raw, enum smdev which)
{
struct spaceman *sm = &sb->s_spaceman;
struct spaceman_device *dev = &sm->sm_dev[which];
struct apfs_spaceman_device *rawdev = &raw->sm_dev[which];
u64 max_blkcnt;
u32 cab_count;
u32 addr_off;
u64 start = 0;
int i;
dev->sm_cab_count = le32_to_cpu(rawdev->sm_cab_count);
dev->sm_cib_count = le32_to_cpu(rawdev->sm_cib_count);
dev->sm_chunk_count = le64_to_cpu(rawdev->sm_chunk_count);
dev->sm_block_count = le64_to_cpu(rawdev->sm_block_count);
dev->sm_free_count = le64_to_cpu(rawdev->sm_free_count);
sm->sm_total_chunk_count += dev->sm_chunk_count;
sm->sm_total_cib_count += dev->sm_cib_count;
sm->sm_total_cab_count += dev->sm_cab_count;
sm->sm_total_free_count += dev->sm_free_count;
max_blkcnt = which == APFS_SD_MAIN ? sb->s_max_main_blkcnt : sb->s_max_tier2_blkcnt;
if (dev->sm_block_count > max_blkcnt)
report("Spaceman device", "block count too big for device.");
if (dev->sm_chunk_count != DIV_ROUND_UP(dev->sm_block_count, sm->sm_blocks_per_chunk))
report("Spaceman device", "wrong count of chunks.");
if (dev->sm_cib_count != DIV_ROUND_UP(dev->sm_chunk_count, sm->sm_chunks_per_cib))
report("Spaceman device", "wrong count of chunk-info blocks.");
cab_count = DIV_ROUND_UP(dev->sm_cib_count, sm->sm_cibs_per_cab);
/* CABs are not used unless at least one can be filled */
if (cab_count == 1)
cab_count = 0;
if (dev->sm_cab_count != cab_count)
report("Spaceman device", "wrong count of chunk-info address blocks.");
/* All bitmaps will need to be read into memory */
dev->sm_bitmap = calloc(dev->sm_chunk_count, sb->s_blocksize);
if (!dev->sm_bitmap)
system_error();
/*
* TODO: is it mandatory that sm_addr_off for the tier2 device comes
* right after the end of the same array for the main device?
*/
addr_off = le32_to_cpu(rawdev->sm_addr_offset);
if (!dev->sm_cab_count) {
/* If CABs are not used, the spaceman just lists the CIBs */
for (i = 0; i < dev->sm_cib_count; ++i) {
u64 bno = spaceman_val_from_off(raw, addr_off + i * sizeof(u64));
start = parse_chunk_info_block(bno, i, start, NULL /* xid_p */, dev);
}
} else {
for (i = 0; i < dev->sm_cab_count; ++i) {
u64 bno = spaceman_val_from_off(raw, addr_off + i * sizeof(u64));
start = parse_cib_addr_block(bno, i, start, dev);
}
if (dev->sm_cib_count != dev->sm_cibs)
report("Spaceman device", "bad total number of cibs.");
}
if (dev->sm_chunk_count != dev->sm_chunks)
report("Spaceman device", "bad total number of chunks.");
if (dev->sm_block_count != dev->sm_blocks)
report("Spaceman device", "bad total number of blocks.");
if (dev->sm_free_count != dev->sm_free)
report("Spaceman device", "bad total number of free blocks.");
if (rawdev->sm_reserved || rawdev->sm_reserved2)
report("Spaceman device", "non-zero padding.");
}
struct alloc_zone {
struct alloc_zone *next; /* Next entry in linked list */
u16 id; /* Zone id */
u64 start; /* Start of zone */
u64 end; /* End of zone */
};
static struct alloc_zone *alloc_zone_list = NULL;
static void check_alloc_zone_sanity(u64 start, u64 end)
{
if (start & (sb->s_blocksize - 1))
report("Allocation zone", "start isn't multiple of block size.");
if (end & (sb->s_blocksize - 1))
report("Allocation zone", "end isn't multiple of block size.");
if (start >= end)
report("Allocation zone", "invalid range.");
}
/* Puts alloc zones in a list to check for overlap */
static void check_new_alloc_zone(u16 id, u64 start, u64 end)
{
struct alloc_zone **zone_p = NULL;
struct alloc_zone *zone = NULL;
struct alloc_zone *new = NULL;
check_alloc_zone_sanity(start, end);
zone_p = &alloc_zone_list;
zone = *zone_p;
while (zone) {
if (zone->id == id)
report("Allocation zones", "repeated id.");
if (start < zone->end && end > zone->start)
report("Allocations zones", "overlapping ranges.");
zone_p = &zone->next;
zone = *zone_p;
}
new = calloc(1, sizeof(*new));
if (!new)
system_error();
new->id = id;
new->start = start;
new->end = end;
*zone_p = new;
}
static void free_checked_alloc_zones(void)
{
struct alloc_zone *curr = alloc_zone_list;
alloc_zone_list = NULL;
while (curr) {
struct alloc_zone *next = NULL;
next = curr->next;
curr->next = NULL;
free(curr);
curr = next;
}
}
/* If old zones are reported, just check that the index is valid */
static void check_prev_alloc_zones(struct apfs_spaceman_allocation_zone_info_phys *az)
{
struct apfs_spaceman_allocation_zone_boundaries *azb = NULL;
u16 prev_index;
int j;
prev_index = le16_to_cpu(az->saz_previous_boundary_index);
if (prev_index > APFS_SM_ALLOCZONE_NUM_PREVIOUS_BOUNDARIES)
report("Allocation zones", "out-of-range previous index.");
for (j = 0; j < APFS_SM_ALLOCZONE_NUM_PREVIOUS_BOUNDARIES; ++j) {
azb = &az->saz_previous_boundaries[j];
if (prev_index == 0) {
/* No previous zones should be reported */
if (azb->saz_zone_start || azb->saz_zone_end)
report("Previous allocation zones", "missing index.");
continue;
}
if (!azb->saz_zone_start && !azb->saz_zone_end) {
/* No zone reported in this slot */
if (j == prev_index - 1 && !azb->saz_zone_start)
report("Allocation zones", "latest is missing.");
continue;
}
check_alloc_zone_sanity(le64_to_cpu(azb->saz_zone_start), le64_to_cpu(azb->saz_zone_end));
}
}
/**
* check_spaceman_datazone - Check the spaceman allocation zones
* @dz: pointer to the raw datazone structure
*
* Allocation zones are undocumented, so we can't do much more than report them
* as unsupported if they are in use.
*/
static void check_spaceman_datazone(struct apfs_spaceman_datazone_info_phys *dz)
{
int i, dev;
for (dev = 0; dev < APFS_SD_COUNT; ++dev) {
for (i = 0; i < APFS_SM_DATAZONE_ALLOCZONE_COUNT; ++i) {
struct apfs_spaceman_allocation_zone_info_phys *az = NULL;
struct apfs_spaceman_allocation_zone_boundaries *azb = NULL;
az = &dz->sdz_allocation_zones[dev][i];
azb = &az->saz_current_boundaries;
if (az->saz_zone_id) {
if (dev != APFS_SD_MAIN)
report_unknown("Fusion drive");
check_new_alloc_zone(le16_to_cpu(az->saz_zone_id), le64_to_cpu(azb->saz_zone_start), le64_to_cpu(azb->saz_zone_end));
} else if (azb->saz_zone_start || azb->saz_zone_end) {
report("Allocation zone", "has no id.");
}
if (az->saz_reserved)
report("Datazone", "reserved field in use.");
check_prev_alloc_zones(az);
}
free_checked_alloc_zones();
}
}
/**
* check_spaceman_tier2_fq_is_null - Check that a tier 2 free queue is unset
* @sfq_raw: raw free queue to check
*/
static void check_spaceman_free_queue_is_null(struct apfs_spaceman_free_queue *sfq_raw)
{
if (sfq_raw->sfq_count || sfq_raw->sfq_tree_oid || sfq_raw->sfq_oldest_xid || sfq_raw->sfq_tree_node_limit)
report("Tier 2 free queue", "should not be set.");
}
/**
* check_spaceman_free_queue_dev - Check the free queue for a given device
* @sfq_raw: raw free queue to check
* @which: device for the queue
*/
static void check_spaceman_free_queue_dev(struct apfs_spaceman_free_queue *sfq_raw, enum smdev which)
{
struct spaceman *sm = &sb->s_spaceman;
struct spaceman_device *dev = NULL;
struct free_queue *fq = NULL;
int idx;
if (which == APFS_SD_TIER2 && !apfs_is_fusion_drive())
return check_spaceman_free_queue_is_null(sfq_raw);
dev = &sm->sm_dev[which];
idx = which == APFS_SD_MAIN ? APFS_SFQ_MAIN : APFS_SFQ_TIER2;
dev->sm_dev_fq = parse_free_queue_btree(le64_to_cpu(sfq_raw->sfq_tree_oid), idx);
fq = dev->sm_dev_fq;
if (le64_to_cpu(sfq_raw->sfq_count) != fq->sfq_count)
report("Spaceman free queue", "wrong block count.");
if (le64_to_cpu(sfq_raw->sfq_oldest_xid) != fq->sfq_oldest_xid)
report("Spaceman free queue", "oldest xid is wrong.");
if (le16_to_cpu(sfq_raw->sfq_tree_node_limit) < fq->sfq_btree.node_count)
report("Spaceman free queue", "node count above limit.");
if (le16_to_cpu(sfq_raw->sfq_tree_node_limit) != main_fq_node_limit(dev->sm_blocks))
report("Spaceman free queue", "wrong node limit.");
}
/**
* check_spaceman_free_queues - Check the spaceman free queues
* @sfq: pointer to the raw free queue array
*/
static void check_spaceman_free_queues(struct apfs_spaceman_free_queue *sfq)
{
struct spaceman *sm = &sb->s_spaceman;
int i;
for (i = 0; i < APFS_SFQ_COUNT; ++i) {
if (sfq[i].sfq_pad16 || sfq[i].sfq_pad32)
report("Spaceman free queue", "non-zero padding.");
if (sfq[i].sfq_reserved)
report("Spaceman free queue", "reserved field in use.");
}
sm->sm_ip_fq = parse_free_queue_btree(
le64_to_cpu(sfq[APFS_SFQ_IP].sfq_tree_oid), APFS_SFQ_IP);
if (le64_to_cpu(sfq[APFS_SFQ_IP].sfq_count) != sm->sm_ip_fq->sfq_count)
report("Spaceman free queue", "wrong block count.");
if (le64_to_cpu(sfq[APFS_SFQ_IP].sfq_oldest_xid) !=
sm->sm_ip_fq->sfq_oldest_xid)
report("Spaceman free queue", "oldest xid is wrong.");
if (le16_to_cpu(sfq[APFS_SFQ_IP].sfq_tree_node_limit) <
sm->sm_ip_fq->sfq_btree.node_count)
report("Spaceman free queue", "node count above limit.");
if (le16_to_cpu(sfq[APFS_SFQ_IP].sfq_tree_node_limit) != ip_fq_node_limit(sm->sm_total_chunk_count))
report("Spaceman free queue", "wrong node limit.");
check_spaceman_free_queue_dev(&sfq[APFS_SFQ_MAIN], APFS_SD_MAIN);
check_spaceman_free_queue_dev(&sfq[APFS_SFQ_TIER2], APFS_SD_TIER2);
}
/**
* compare_container_bitmaps_dev - Verify a device's allocation bitmap
* @which: device to check
*/
static void compare_container_bitmaps_dev(enum smdev which)
{
struct spaceman *sm = &sb->s_spaceman;
struct spaceman_device *dev = NULL;
u64 *reported_bmap = NULL, *real_bmap = NULL;
unsigned long long bmap_size;
dev = &sm->sm_dev[which];
if (dev->sm_chunk_count == 0) /* No tier 2 device */
return;
reported_bmap = dev->sm_bitmap;
real_bmap = which == APFS_SD_MAIN ? sb->s_main_bitmap : sb->s_tier2_bitmap;
bmap_size = sb->s_blocksize * dev->sm_chunk_count;
if (memcmp(reported_bmap, real_bmap, bmap_size) != 0)
report("Space manager", "bad allocation bitmap.");
}
/**
* check_ip_free_next - Check the free_next field for the internal pool
* @free_next: free_next list to check
* @free_head: first block in the linked list of free blocks
* @free_tail: last block in the linked list of free blocks
* @bmap_count: total number of ip bitmaps
*
* Returns the number of used entries.
*/
static int check_ip_free_next(__le16 *free_next, u16 free_head, u16 free_tail, u32 bmap_count)
{
u16 curr, next;
int free_count = 0;
/*
* Entries for free ip bitmap blocks are a linked list, where each one
* gives the index of the next one. The rest of the blocks are used;
* they aren't part of the list, so their entries are set to the
* invalid index 0xFFFF.
*/
curr = free_tail;
next = free_head;
do {
if (curr >= bmap_count || next >= bmap_count)
report("Internal pool", "free bitmaps are out-of-bounds.");
if (++free_count >= bmap_count)
report("Internal pool", "free bitmap list loops.");
curr = next;
next = le16_to_cpu(free_next[curr]);
} while (curr != free_tail);
/*
* The tail itself is also set to the invalid index because the linked
* list ends there. Its block is free, though.
*/
if (le16_to_cpu(free_next[free_tail]) != APFS_SPACEMAN_IP_BM_INDEX_INVALID)
report("Free ip bitmaps list", "free tail is not used.");
return bmap_count - free_count;
}
/**
* read_ip_bitmap_block - Read a single internal pool bitmap block into memory
* @bmap_base: first block of the bitmap ring
* @bmap_len: length of the bitmap ring
* @bmap_off: offset of the block to read in the ring
* @bmap: on return, the whole ip bitmap
*/
static void read_ip_bitmap_block(u64 bmap_base, u32 bmap_len, u16 bmap_off, char *bmap)
{
char *curr_blk = NULL;
u64 bno = bmap_base + bmap_off % bmap_len;
curr_blk = apfs_mmap(NULL, sb->s_blocksize, PROT_READ, MAP_PRIVATE, bno * sb->s_blocksize);
if (curr_blk == MAP_FAILED)
system_error();
memcpy(bmap, curr_blk, sb->s_blocksize);
munmap(curr_blk, sb->s_blocksize);
curr_blk = NULL;
}
/**
* parse_ip_bitmap_list - Check consistency of the internal pool bitmap list
* @raw: pointer to the raw space manager
* @bmap: on return, set the whole ip bitmap here
*/
static void parse_ip_bitmap_list(struct apfs_spaceman_phys *raw, char *bmap)
{
u64 bmap_base = le64_to_cpu(raw->sm_ip_bm_base);
u16 bmap_off;
u32 bmap_length = le32_to_cpu(raw->sm_ip_bm_block_count);
u32 bm_size_in_blocks = le32_to_cpu(raw->sm_ip_bm_size_in_blocks);
u16 free_head, free_tail, used_count;
__le16 *free_next = NULL;
u16 *used_bitmaps = NULL;
u32 i, j;
/*
* The bitmap area is a ring structure that keeps both the currently
* valid ip bitmaps and some older versions. I don't know the reason
* for this.
*/
if (bmap_length > APFS_SPACEMAN_IP_BM_BLOCK_COUNT_MAX)
report("Internal pool", "bitmap list is too long.");
if (bmap_length != 16 * bm_size_in_blocks)
report("Space manager", "ip doesn't have 16 bitmap copies.");
free_head = le16_to_cpu(raw->sm_ip_bm_free_head);
free_tail = le16_to_cpu(raw->sm_ip_bm_free_tail);
free_next = spaceman_16_array_from_off(raw, le32_to_cpu(raw->sm_ip_bm_free_next_offset), bmap_length);
used_count = check_ip_free_next((__le16 *)free_next, free_head, free_tail, bmap_length);
if (used_count != bm_size_in_blocks)
report("Internal pool", "incorrect count of used blocks.");
container_bmap_mark_as_used(bmap_base, bmap_length);
used_bitmaps = calloc(used_count + 1, sizeof(*used_bitmaps));
if (!used_bitmaps)
system_error();
used_bitmaps[0] = free_tail;
for (i = 0; i < bm_size_in_blocks; ++i) {
bmap_off = spaceman_16_from_off(raw, le32_to_cpu(raw->sm_ip_bitmap_offset) + i * sizeof(bmap_off));
if (bmap_off >= bmap_length)
report("Internal pool", "bitmap block is out-of-bounds.");
if (le16_to_cpu(free_next[bmap_off]) != APFS_SPACEMAN_IP_BM_INDEX_INVALID)
report("Internal pool", "used bitmap marked as free.");
read_ip_bitmap_block(bmap_base, bmap_length, bmap_off, bmap + i * sb->s_blocksize);
/*
* Make sure that the same bitmap blocks aren't being reused
* for different ip chunks. Remember that the first used bitmap
* is the one for free_tail, set outside this loop.
*/
for (j = 0; j < i + 1; ++j) {
if (used_bitmaps[j] == bmap_off)
report("Internal pool", "same bitmap used twice.");
}
used_bitmaps[i + 1] = bmap_off;
}
free(used_bitmaps);
used_bitmaps = NULL;
}
/**
* check_ip_bitmap_blocks - Check that the bitmap blocks are properly zeroed
* @raw: pointer to the raw space manager
*
* For most internal pool bitmap blocks this is the only check needed; the
* current one also needs to be compared against the actual allocation bitmap.
*/
static void check_ip_bitmap_blocks(struct apfs_spaceman_phys *raw)
{
u64 bmap_base = le64_to_cpu(raw->sm_ip_bm_base);
u32 bmap_length = le32_to_cpu(raw->sm_ip_bm_block_count);
u64 pool_blocks = le64_to_cpu(raw->sm_ip_block_count);
int i;
/*
* These zeroed-tail checks don't really make sense with multiblock ip
* bitmaps, because we can't know for sure which ones were tails and
* which ones were used in full.
*/
if (le32_to_cpu(raw->sm_ip_bm_size_in_blocks) != 1)
return;
for (i = 0; i < bmap_length; ++i) {
char *bmap;
int edge, j;
bmap = apfs_mmap(NULL, sb->s_blocksize, PROT_READ, MAP_PRIVATE, (bmap_base + i) * sb->s_blocksize);
if (bmap == MAP_FAILED)
system_error();
/*
* The edge is the last byte inside the allocation bitmap;
* everything that comes afterwards must be zeroed.
*/
edge = pool_blocks / 8;
for (j = pool_blocks % 8; j < 8; ++j) {
u8 flag = 1 << j;
if (bmap[edge] & flag)
report("Internal pool", "non-zeroed bitmap.");
}
for (j = edge + 1; j < sb->s_blocksize; ++j) {
if (bmap[j])
report("Internal pool", "non-zeroed bitmap.");
}
munmap(bmap, sb->s_blocksize);
}
}
/**
* check_internal_pool - Check the internal pool of blocks
* @raw: pointer to the raw space manager
*/
static void check_internal_pool(struct apfs_spaceman_phys *raw)
{
struct spaceman *sm = &sb->s_spaceman;
char *pool_bmap;
u64 pool_base = le64_to_cpu(raw->sm_ip_base);
u64 pool_blocks = le64_to_cpu(raw->sm_ip_block_count);
u64 ip_chunk_count = le32_to_cpu(raw->sm_ip_bm_size_in_blocks);
u64 i;
if ((sm->sm_total_chunk_count + sm->sm_total_cib_count + sm->sm_total_cab_count) * 3 != pool_blocks)
report("Space manager", "wrong size of internal pool.");
pool_bmap = calloc(ip_chunk_count, sb->s_blocksize);
if (!pool_bmap)
system_error();
parse_ip_bitmap_list(raw, pool_bmap);
if (memcmp(pool_bmap, sb->s_ip_bitmap, ip_chunk_count * sb->s_blocksize))
report("Space manager", "bad ip allocation bitmap.");
container_bmap_mark_as_used(pool_base, pool_blocks);
free(pool_bmap);
pool_bmap = NULL;
if (le32_to_cpu(raw->sm_ip_bm_tx_multiplier) !=
APFS_SPACEMAN_IP_BM_TX_MULTIPLIER)
report("Space manager", "bad tx multiplier for internal pool.");
for (i = 0; i < ip_chunk_count; ++i) {
u64 xid;
xid = spaceman_val_from_off(raw, le32_to_cpu(raw->sm_ip_bm_xid_offset) + i * sizeof(xid));
if (xid > sb->s_xid)
report("Internal pool", "bad transaction id.");
}
check_ip_bitmap_blocks(raw);
}
/**
* check_spaceman - Check the space manager structures for a container
* @oid: ephemeral object id for the spaceman structure
*/
void check_spaceman(u64 oid)
{
struct spaceman *sm = &sb->s_spaceman;
struct object obj;
struct apfs_spaceman_phys *raw;
u64 ip_chunk_count;
u32 flags;
raw = read_ephemeral_object(oid, &obj);
if (obj.type != APFS_OBJECT_TYPE_SPACEMAN)
report("Space manager", "wrong object type.");
if (obj.subtype != APFS_OBJECT_TYPE_INVALID)
report("Space manager", "wrong object subtype.");
sm->sm_xid = obj.xid;
sm->sm_obj_size = obj.size;
sm->sm_ip_base = le64_to_cpu(raw->sm_ip_base);
sm->sm_ip_block_count = le64_to_cpu(raw->sm_ip_block_count);
ip_chunk_count = DIV_ROUND_UP(sm->sm_ip_block_count, 8 * sb->s_blocksize);
if (ip_chunk_count != le32_to_cpu(raw->sm_ip_bm_size_in_blocks))
report("Space manager", "bad ip bm size.");
sb->s_ip_bitmap = calloc(ip_chunk_count, sb->s_blocksize);
if (!sb->s_ip_bitmap)
system_error();
flags = le32_to_cpu(raw->sm_flags);
if ((flags & APFS_SM_FLAGS_VALID_MASK) != flags)
report("Space manager", "invalid flag in use.");
if (flags & APFS_SM_FLAG_VERSIONED) {
sm->sm_struct_size = le32_to_cpu(raw->sm_struct_size);
if (sm->sm_struct_size != sizeof(*raw))
report("Space manager", "wrong reported struct size.");
check_spaceman_datazone(&raw->sm_datazone);
} else {
/* Some fields are missing in the non-versioned structure */
sm->sm_struct_size = sizeof(*raw) - sizeof(raw->sm_datazone) -
sizeof(raw->sm_struct_size) -
sizeof(raw->sm_version);
}
if (le32_to_cpu(raw->sm_block_size) != sb->s_blocksize)
report("Space manager", "wrong block size.");
parse_spaceman_chunk_counts(raw);
parse_spaceman_device(raw, APFS_SD_MAIN);
parse_spaceman_device(raw, APFS_SD_TIER2);
if (sb->s_block_count != sm->sm_dev[APFS_SD_MAIN].sm_block_count + sm->sm_dev[APFS_SD_TIER2].sm_block_count)
report("Spaceman devices", "wrong block count.");
check_spaceman_free_queues(raw->sm_fq);
check_internal_pool(raw);
free(sb->s_ip_bitmap);
if (le64_to_cpu(raw->sm_fs_reserve_block_count) != sm->sm_reserve_block_num)
report("Space manager", "wrong block reservation total.");
if (le64_to_cpu(raw->sm_fs_reserve_alloc_count) != sm->sm_reserve_alloc_num)
report("Space manager", "wrong reserve block allocation total.");
if (sm->sm_reserve_block_num - sm->sm_reserve_alloc_num > sm->sm_total_free_count)
report("Space manager", "block reservation not respected.");
compare_container_bitmaps_dev(APFS_SD_MAIN);
compare_container_bitmaps_dev(APFS_SD_TIER2);
free(raw);
}
/**
* parse_free_queue_record - Parse a free queue record and check for corruption
* @key: pointer to the raw key
* @val: pointer to the raw value
* @len: length of the raw value
* @btree: the free queue btree structure
*
* Internal consistency of @key must be checked before calling this function.
*/
void parse_free_queue_record(struct apfs_spaceman_free_queue_key *key,
void *val, int len, struct btree *btree)
{
struct free_queue *sfq = (struct free_queue *)btree;
u64 paddr, length, xid;
bool inside_ip;
if (!len) {
length = 1; /* Ghost records are for one block long extents */
} else if (len == 8) {
__le64 *val64 = (__le64 *)val;
length = le64_to_cpu(*val64);
if (!length)
report("Free queue record", "length is zero.");
if (length == 1)
report("Free queue record", "value is unnecessary.");
} else {
report("Free queue record", "wrong size of value.");
}
sfq->sfq_count += length;
paddr = le64_to_cpu(key->sfqk_paddr);
inside_ip = range_in_ip(paddr, length);
if (sfq->sfq_index == APFS_SFQ_IP && !inside_ip)
report("Free queue record", "range should be inside the IP.");
if (sfq->sfq_index != APFS_SFQ_IP && inside_ip)
report("Free queue record", "range should be outside the IP.");
xid = le64_to_cpu(key->sfqk_xid);
if (xid > sb->s_xid)
report("Free queue record", "bad transaction id.");
if (!sfq->sfq_oldest_xid || xid < sfq->sfq_oldest_xid)
sfq->sfq_oldest_xid = xid;
/*
* These blocks are free, but still not marked as such. The point
* seems to be the preservation of recent checkpoints.
*/
if (inside_ip)
ip_bmap_mark_as_used(paddr, length);
else
container_bmap_mark_as_used(paddr, length);
}