Files
appsandbox/tools/iso-patch/engine/squashfs.c
T

1161 lines
41 KiB
C

/* squashfs.c - squashfs reader: superblock + metadata block decompressor. */
#include "squashfs.h"
#include "log.h"
#include "xz/xz.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Forward decls (definition order is reader-friendly, not call-graph order).
* sqfs_stream_t is defined further below alongside struct sqfs_ctx. */
struct sqfs_stream;
typedef struct sqfs_stream sqfs_stream_t;
int sqfs_read_metadata_block(sqfs_ctx_t *ctx, uint64_t *off,
void *out, size_t *out_len);
static int load_id_table(sqfs_ctx_t *ctx);
static int load_fragment_table(sqfs_ctx_t *ctx);
static int stream_load_range(sqfs_ctx_t *ctx, sqfs_stream_t *s,
uint64_t start, uint64_t end);
/* A decompressed "stream" of concatenated metadata blocks. We remember the
* compressed start offset of each block (RELATIVE to table start, since
* inode references and dir_entry.start_block are encoded that way) so we
* can map an inode/dir reference to its decompressed position. */
struct sqfs_stream {
uint8_t *buf; /* concatenated decompressed bytes */
size_t len;
size_t cap;
/* per-block mapping: c_off[i] = compressed offset relative to base,
* d_off[i] = decompressed offset where block i starts */
uint64_t *c_off;
size_t *d_off;
size_t n_blocks;
size_t cap_blocks;
uint64_t base; /* absolute file offset of block 0 */
};
struct sqfs_ctx {
HANDLE file;
sqfs_superblock_t sb;
sqfs_stream_t inode_stream;
sqfs_stream_t dir_stream;
uint32_t *id_table; /* uid/gid lookup */
size_t id_count;
uint64_t *fragment_index; /* fragment_entry[] flattened */
size_t fragment_count;
};
const char *sqfs_compressor_name(uint16_t id)
{
switch (id) {
case SQFS_COMP_GZIP: return "GZIP";
case SQFS_COMP_LZMA: return "LZMA";
case SQFS_COMP_LZO: return "LZO";
case SQFS_COMP_XZ: return "XZ";
case SQFS_COMP_LZ4: return "LZ4";
case SQFS_COMP_ZSTD: return "ZSTD";
default: return "UNKNOWN";
}
}
sqfs_ctx_t *sqfs_open(const wchar_t *path)
{
LOG_I("sqfs_open: %ls", path);
/* Build the xz CRC32 table (idempotent - first caller wins).
* Without this, xz_crc32 returns garbage and stream-flags CRC check
* inside dec_stream_header fails with XZ_DATA_ERROR at in_pos=12. */
static int crc_inited = 0;
if (!crc_inited) { xz_crc32_init(); crc_inited = 1; }
HANDLE h = CreateFileW(path, GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if (h == INVALID_HANDLE_VALUE) {
LOG_WIN32("CreateFileW(squashfs)", GetLastError());
return NULL;
}
LARGE_INTEGER sz;
if (!GetFileSizeEx(h, &sz)) {
LOG_WIN32("GetFileSizeEx", GetLastError());
CloseHandle(h);
return NULL;
}
LOG_I(" file size: %lld bytes (%.2f MiB)",
sz.QuadPart, (double)sz.QuadPart / (1024.0 * 1024.0));
sqfs_ctx_t *ctx = (sqfs_ctx_t *)calloc(1, sizeof(*ctx));
if (!ctx) { CloseHandle(h); return NULL; }
ctx->file = h;
/* Read + parse superblock. */
DWORD br = 0;
if (!ReadFile(h, &ctx->sb, sizeof(ctx->sb), &br, NULL) ||
br != sizeof(ctx->sb)) {
LOG_WIN32("ReadFile(superblock)", GetLastError());
sqfs_close(ctx);
return NULL;
}
if (ctx->sb.magic != SQFS_MAGIC) {
LOG_E(" bad magic 0x%08x (expected 0x%08x)",
ctx->sb.magic, SQFS_MAGIC);
sqfs_close(ctx);
return NULL;
}
LOG_I(" superblock:");
LOG_I(" magic 0x%08x (OK)", ctx->sb.magic);
LOG_I(" inode_count %u", ctx->sb.inode_count);
LOG_I(" block_size %u (%u KiB)", ctx->sb.block_size, ctx->sb.block_size / 1024);
LOG_I(" block_log %u (1 << %u = %u)",
ctx->sb.block_log, ctx->sb.block_log, 1u << ctx->sb.block_log);
LOG_I(" fragments %u", ctx->sb.fragment_entry_count);
LOG_I(" compressor %u (%s)",
ctx->sb.compression_id, sqfs_compressor_name(ctx->sb.compression_id));
LOG_I(" flags 0x%04x", ctx->sb.flags);
LOG_I(" version %u.%u",
ctx->sb.version_major, ctx->sb.version_minor);
LOG_I(" root inode ref 0x%016llx (block=%llu off=%u)",
(unsigned long long)ctx->sb.root_inode_ref,
(unsigned long long)(ctx->sb.root_inode_ref >> 16),
(unsigned)(ctx->sb.root_inode_ref & 0xffff));
LOG_I(" bytes_used %llu", (unsigned long long)ctx->sb.bytes_used);
LOG_I(" inode_table 0x%llx", (unsigned long long)ctx->sb.inode_table_start);
LOG_I(" directory_table 0x%llx", (unsigned long long)ctx->sb.directory_table_start);
LOG_I(" fragment_table 0x%llx", (unsigned long long)ctx->sb.fragment_table_start);
LOG_I(" lookup_table 0x%llx", (unsigned long long)ctx->sb.lookup_table_start);
LOG_I(" id_table 0x%llx", (unsigned long long)ctx->sb.id_table_start);
LOG_I(" xattr_id_table 0x%llx", (unsigned long long)ctx->sb.xattr_id_table_start);
if (ctx->sb.version_major != 4) {
LOG_E(" unsupported squashfs version %u.%u (need 4.x)",
ctx->sb.version_major, ctx->sb.version_minor);
sqfs_close(ctx);
return NULL;
}
if (ctx->sb.compression_id != SQFS_COMP_XZ &&
ctx->sb.compression_id != SQFS_COMP_GZIP) {
LOG_E(" unsupported compressor %u (%s) - only XZ and GZIP impl'd",
ctx->sb.compression_id, sqfs_compressor_name(ctx->sb.compression_id));
sqfs_close(ctx);
return NULL;
}
/* Validate block_size before any code divides/modulos by it (see
* sqfs_read_file). It is image-controlled: must be non-zero, a power of
* two, equal to (1 << block_log), and within the spec's 1 MiB ceiling. */
if (ctx->sb.block_size == 0 ||
(ctx->sb.block_size & (ctx->sb.block_size - 1)) != 0 ||
ctx->sb.block_log >= 32 ||
ctx->sb.block_size != (1u << ctx->sb.block_log) ||
ctx->sb.block_size > (1u << 20)) {
LOG_E(" invalid block_size %u (block_log %u)",
ctx->sb.block_size, ctx->sb.block_log);
sqfs_close(ctx);
return NULL;
}
/* Pre-load id table, fragment table, inode table, dir table.
* Each is small (a few MB max) and random access is O(1). */
if (load_id_table(ctx) != 0) {
LOG_E(" load_id_table failed"); sqfs_close(ctx); return NULL;
}
if (load_fragment_table(ctx) != 0) {
LOG_E(" load_fragment_table failed"); sqfs_close(ctx); return NULL;
}
LOG_I(" loading inode table:");
if (stream_load_range(ctx, &ctx->inode_stream,
ctx->sb.inode_table_start,
ctx->sb.directory_table_start) != 0) {
LOG_E(" inode table load failed"); sqfs_close(ctx); return NULL;
}
LOG_I(" loading directory table:");
if (stream_load_range(ctx, &ctx->dir_stream,
ctx->sb.directory_table_start,
ctx->sb.fragment_table_start) != 0) {
LOG_E(" dir table load failed"); sqfs_close(ctx); return NULL;
}
return ctx;
}
static void stream_free(sqfs_stream_t *s)
{
free(s->buf); s->buf = NULL;
free(s->c_off); s->c_off = NULL;
free(s->d_off); s->d_off = NULL;
}
void sqfs_close(sqfs_ctx_t *ctx)
{
if (!ctx) return;
if (ctx->file != INVALID_HANDLE_VALUE) CloseHandle(ctx->file);
stream_free(&ctx->inode_stream);
stream_free(&ctx->dir_stream);
free(ctx->id_table);
free(ctx->fragment_index);
free(ctx);
}
const sqfs_superblock_t *sqfs_sb(const sqfs_ctx_t *ctx) { return &ctx->sb; }
int sqfs_read_raw(sqfs_ctx_t *ctx, uint64_t off, void *buf, size_t len)
{
/* Use OVERLAPPED with Offset to skip SetFilePointerEx. This makes the
* call THREAD-SAFE: the file pointer isn't shared/mutated; the offset
* is per-call. Even on a non-FILE_FLAG_OVERLAPPED handle this works -
* the call is synchronous, just doesn't advance the file pointer.
* Allows N decompress threads to read the same squashfs concurrently. */
OVERLAPPED ov = { 0 };
ov.Offset = (DWORD)(off & 0xffffffffu);
ov.OffsetHigh = (DWORD)(off >> 32);
DWORD br = 0;
if (!ReadFile(ctx->file, buf, (DWORD)len, &br, &ov) || br != (DWORD)len) {
DWORD err = GetLastError();
LOG_WIN32("ReadFile(raw)", err);
return -1;
}
return 0;
}
/* Append one decompressed block to the stream + record offsets. */
static int stream_append(sqfs_ctx_t *ctx, sqfs_stream_t *s, uint64_t c_off)
{
if (s->cap < s->len + SQFS_METADATA_SIZE) {
size_t newcap = (s->cap == 0) ? (1u << 20) : s->cap * 2;
while (newcap < s->len + SQFS_METADATA_SIZE) newcap *= 2;
uint8_t *nb = (uint8_t *)realloc(s->buf, newcap);
if (!nb) return -1;
s->buf = nb;
s->cap = newcap;
}
if (s->n_blocks == s->cap_blocks) {
size_t newcap = (s->cap_blocks == 0) ? 256 : s->cap_blocks * 2;
uint64_t *nc = (uint64_t *)realloc(s->c_off, newcap * sizeof(uint64_t));
size_t *nd = (size_t *)realloc(s->d_off, newcap * sizeof(size_t));
if (!nc || !nd) { free(nc); free(nd); return -1; }
s->c_off = nc; s->d_off = nd; s->cap_blocks = newcap;
}
uint64_t off = c_off;
size_t block_len = 0;
if (sqfs_read_metadata_block(ctx, &off, s->buf + s->len, &block_len) != 0) {
return -1;
}
/* Store offset RELATIVE to stream base (= inode/dir table start). */
s->c_off[s->n_blocks] = c_off - s->base;
s->d_off[s->n_blocks] = s->len;
s->n_blocks++;
s->len += block_len;
return (int)(off - c_off); /* on-disk consumed */
}
/* Load a contiguous range of metadata blocks into a stream. */
static int stream_load_range(sqfs_ctx_t *ctx, sqfs_stream_t *s,
uint64_t start, uint64_t end)
{
s->base = start;
uint64_t cur = start;
LOG_I(" loading metadata stream %llx..%llx (%llu bytes on disk)",
(unsigned long long)start, (unsigned long long)end,
(unsigned long long)(end - start));
while (cur < end) {
int adv = stream_append(ctx, s, cur);
if (adv <= 0) {
LOG_E(" stream_append at 0x%llx failed", (unsigned long long)cur);
return -1;
}
cur += adv;
}
LOG_I(" -> %zu blocks, %zu decompressed bytes",
s->n_blocks, s->len);
return 0;
}
/* Convert a (compressed block offset, byte offset) reference into a flat
* decompressed-stream offset. Returns -1 on lookup failure. */
static int64_t stream_resolve(const sqfs_stream_t *s,
uint64_t c_block_off, uint16_t byte_off)
{
/* Linear scan - block lists are short (~hundreds). Could binary-search. */
for (size_t i = 0; i < s->n_blocks; i++) {
if (s->c_off[i] == c_block_off) {
return (int64_t)(s->d_off[i] + byte_off);
}
}
return -1;
}
/* The id_table is an indirection: at sb.id_table_start there's an array
* of uint64_t pointing to metadata blocks; each block decompresses to a
* uint32_t[] of UIDs/GIDs. */
static int load_id_table(sqfs_ctx_t *ctx)
{
size_t n = ctx->sb.id_count;
size_t bytes = n * sizeof(uint32_t);
size_t n_blocks = (bytes + SQFS_METADATA_SIZE - 1) / SQFS_METADATA_SIZE;
LOG_I(" id table: %zu entries -> %zu metadata block(s)", n, n_blocks);
if (n == 0) { ctx->id_count = 0; return 0; }
uint64_t *block_offs = (uint64_t *)malloc(n_blocks * sizeof(uint64_t));
if (!block_offs) return -1;
if (sqfs_read_raw(ctx, ctx->sb.id_table_start,
block_offs, n_blocks * sizeof(uint64_t)) != 0) {
free(block_offs); return -1;
}
ctx->id_table = (uint32_t *)malloc(n_blocks * SQFS_METADATA_SIZE);
if (!ctx->id_table) { free(block_offs); return -1; }
size_t pos = 0;
for (size_t i = 0; i < n_blocks; i++) {
uint64_t off = block_offs[i];
uint8_t tmp[SQFS_METADATA_SIZE];
size_t len = 0;
if (sqfs_read_metadata_block(ctx, &off, tmp, &len) != 0) {
free(block_offs); free(ctx->id_table); ctx->id_table = NULL;
return -1;
}
memcpy((uint8_t *)ctx->id_table + pos, tmp, len);
pos += len;
}
ctx->id_count = pos / sizeof(uint32_t);
free(block_offs);
return 0;
}
/* Lookup uid/gid via id_index. */
static uint32_t id_lookup(const sqfs_ctx_t *ctx, uint16_t idx)
{
if (idx >= ctx->id_count) return 0;
return ctx->id_table[idx];
}
/* Fragment table: parallel structure to id_table. Each fragment entry
* is 16 bytes: u64 start, u32 size, u32 unused. */
typedef struct {
uint64_t start;
uint32_t size;
uint32_t unused;
} sqfs_fragment_entry_t;
static int load_fragment_table(sqfs_ctx_t *ctx)
{
size_t n = ctx->sb.fragment_entry_count;
size_t bytes = n * sizeof(sqfs_fragment_entry_t);
size_t n_blocks = (bytes + SQFS_METADATA_SIZE - 1) / SQFS_METADATA_SIZE;
LOG_I(" fragment table: %zu entries -> %zu metadata block(s)", n, n_blocks);
if (n == 0) { ctx->fragment_count = 0; return 0; }
uint64_t *block_offs = (uint64_t *)malloc(n_blocks * sizeof(uint64_t));
if (!block_offs) return -1;
if (sqfs_read_raw(ctx, ctx->sb.fragment_table_start,
block_offs, n_blocks * sizeof(uint64_t)) != 0) {
free(block_offs); return -1;
}
ctx->fragment_index = (uint64_t *)malloc(n_blocks * SQFS_METADATA_SIZE);
if (!ctx->fragment_index) { free(block_offs); return -1; }
size_t pos = 0;
for (size_t i = 0; i < n_blocks; i++) {
uint64_t off = block_offs[i];
uint8_t tmp[SQFS_METADATA_SIZE];
size_t len = 0;
if (sqfs_read_metadata_block(ctx, &off, tmp, &len) != 0) {
free(block_offs); return -1;
}
memcpy((uint8_t *)ctx->fragment_index + pos, tmp, len);
pos += len;
}
ctx->fragment_count = pos / sizeof(sqfs_fragment_entry_t);
free(block_offs);
return 0;
}
int sqfs_read_metadata_block(sqfs_ctx_t *ctx, uint64_t *off,
void *out, size_t *out_len)
{
/* 2-byte header: top bit = uncompressed flag, low 15 bits = on-disk size. */
uint16_t hdr;
if (sqfs_read_raw(ctx, *off, &hdr, 2) != 0) return -1;
int uncompressed = (hdr & 0x8000) ? 1 : 0;
uint16_t disk_len = hdr & 0x7fff;
if (disk_len == 0 || disk_len > SQFS_METADATA_SIZE) {
LOG_E("metadata block at 0x%llx: bad disk_len=%u",
(unsigned long long)*off, disk_len);
return -1;
}
uint8_t inbuf[SQFS_METADATA_SIZE];
if (sqfs_read_raw(ctx, *off + 2, inbuf, disk_len) != 0) return -1;
if (uncompressed) {
memcpy(out, inbuf, disk_len);
*out_len = disk_len;
*off += 2 + disk_len;
return 0;
}
/* Compressed: dispatch on compressor. Only XZ + GZIP impl'd for now. */
if (ctx->sb.compression_id == SQFS_COMP_XZ) {
/* XZ_DYNALLOC: multi-call mode. We give all input at once + a
* large enough output buffer, then loop until XZ_STREAM_END or
* XZ_OK with no progress. Linux's squashfs driver uses PREALLOC;
* DYNALLOC suits us since we only need a decoder per block. */
struct xz_dec *dec = xz_dec_init(XZ_DYNALLOC, 1u << 16);
if (!dec) { LOG_E("xz_dec_init OOM"); return -1; }
struct xz_buf b = { 0 };
b.in = inbuf;
b.in_pos = 0;
b.in_size = disk_len;
b.out = (uint8_t *)out;
b.out_pos = 0;
b.out_size = SQFS_METADATA_SIZE;
enum xz_ret r;
for (;;) {
size_t prev_in = b.in_pos, prev_out = b.out_pos;
r = xz_dec_run(dec, &b);
if (r == XZ_STREAM_END) break;
if (r != XZ_OK) {
LOG_E("xz_dec_run at 0x%llx: rc=%d (in=%zu/%zu out=%zu/%zu)",
(unsigned long long)*off, (int)r,
b.in_pos, b.in_size, b.out_pos, b.out_size);
xz_dec_end(dec);
return -1;
}
if (b.in_pos == prev_in && b.out_pos == prev_out) {
LOG_E("xz_dec_run stalled: rc=%d in=%zu/%zu out=%zu/%zu",
(int)r, b.in_pos, b.in_size, b.out_pos, b.out_size);
xz_dec_end(dec);
return -1;
}
}
xz_dec_end(dec);
*out_len = b.out_pos;
*off += 2 + disk_len;
return 0;
}
LOG_E("compressor %u not implemented", ctx->sb.compression_id);
return -1;
}
typedef struct {
uint64_t next_block;
uint64_t end;
size_t pos;
size_t len;
uint8_t data[SQFS_METADATA_SIZE];
} sqfs_xattr_cursor_t;
static int xattr_next_block(sqfs_ctx_t *ctx, sqfs_xattr_cursor_t *c)
{
uint16_t hdr;
if (c->next_block >= c->end || c->end - c->next_block < 2 ||
sqfs_read_raw(ctx, c->next_block, &hdr, sizeof(hdr)) != 0 ||
(hdr & 0x7fff) > c->end - c->next_block - 2)
return -1;
if (sqfs_read_metadata_block(ctx, &c->next_block, c->data, &c->len) != 0 ||
c->len == 0 || (c->next_block != c->end && c->len != SQFS_METADATA_SIZE))
return -1;
c->pos = 0;
return 0;
}
static int xattr_cursor_start(sqfs_ctx_t *ctx, sqfs_xattr_cursor_t *c,
uint64_t start, uint64_t end, size_t offset)
{
if (end > ctx->sb.bytes_used || start >= end || offset >= SQFS_METADATA_SIZE)
return -1;
c->next_block = start;
c->end = end;
if (xattr_next_block(ctx, c) != 0 || offset >= c->len) return -1;
c->pos = offset;
return 0;
}
static int xattr_read(sqfs_ctx_t *ctx, sqfs_xattr_cursor_t *c,
void *out, size_t size)
{
uint8_t *p = (uint8_t *)out;
while (size) {
if (c->pos == c->len && xattr_next_block(ctx, c) != 0) return -1;
size_t n = c->len - c->pos;
if (n > size) n = size;
if (p) { memcpy(p, c->data + c->pos, n); p += n; }
c->pos += n;
size -= n;
}
return 0;
}
int sqfs_read_capability(sqfs_ctx_t *ctx, const sqfs_entry_t *entry,
void **out_data, size_t *out_size)
{
*out_data = NULL;
*out_size = 0;
if (entry->type != SQFS_EREG_TYPE || entry->_xattr_idx == UINT32_MAX)
return 0;
struct {
uint64_t start;
uint32_t count;
uint32_t unused;
} table;
struct {
uint64_t ref;
uint32_t count;
uint32_t size;
} id;
uint64_t table_off = ctx->sb.xattr_id_table_start;
if (table_off > ctx->sb.bytes_used ||
sizeof(table) > ctx->sb.bytes_used - table_off ||
sqfs_read_raw(ctx, table_off, &table, sizeof(table)) != 0 ||
entry->_xattr_idx >= table.count)
return -1;
uint64_t index_bytes = (((uint64_t)table.count * sizeof(id) +
SQFS_METADATA_SIZE - 1) / SQFS_METADATA_SIZE) * 8;
table_off += sizeof(table);
if (index_bytes != ctx->sb.bytes_used - table_off) return -1;
uint64_t xattr_end, id_block;
uint64_t id_offset = (uint64_t)entry->_xattr_idx * sizeof(id);
if (sqfs_read_raw(ctx, table_off, &xattr_end, sizeof(xattr_end)) != 0 ||
sqfs_read_raw(ctx, table_off + (id_offset / SQFS_METADATA_SIZE) * 8,
&id_block, sizeof(id_block)) != 0 ||
table.start < sizeof(sqfs_superblock_t) || table.start >= xattr_end ||
xattr_end > id_block || id_block >= ctx->sb.xattr_id_table_start)
return -1;
sqfs_xattr_cursor_t c;
if (xattr_cursor_start(ctx, &c, id_block, ctx->sb.xattr_id_table_start,
(size_t)(id_offset % SQFS_METADATA_SIZE)) != 0 ||
xattr_read(ctx, &c, &id, sizeof(id)) != 0 ||
id.count == 0 ||
(id.ref >> 16) >= xattr_end - table.start ||
xattr_cursor_start(ctx, &c, table.start + (id.ref >> 16), xattr_end,
(size_t)(id.ref & 0xffff)) != 0)
return -1;
for (uint32_t i = 0; i < id.count; i++) {
uint16_t attr[2];
char name[sizeof("capability") - 1];
uint32_t value_size;
if (xattr_read(ctx, &c, attr, sizeof(attr)) != 0 ||
attr[1] == 0 || (attr[0] & ~0x1ffu) != 0)
return -1;
int match = (attr[0] & 0xff) == 2 && attr[1] == sizeof(name);
if (xattr_read(ctx, &c, match ? name : NULL, attr[1]) != 0 ||
xattr_read(ctx, &c, &value_size, sizeof(value_size)) != 0 ||
((attr[0] & 0x100) && value_size != sizeof(uint64_t)))
return -1;
if (!match || memcmp(name, "capability", sizeof(name)) != 0) {
if (xattr_read(ctx, &c, NULL, value_size) != 0) return -1;
continue;
}
if (attr[0] & 0x100) {
uint64_t ref;
if (xattr_read(ctx, &c, &ref, sizeof(ref)) != 0 ||
(ref >> 16) >= xattr_end - table.start ||
xattr_cursor_start(ctx, &c, table.start + (ref >> 16), xattr_end,
(size_t)(ref & 0xffff)) != 0 ||
xattr_read(ctx, &c, &value_size, sizeof(value_size)) != 0)
return -1;
}
if (value_size != 12 && value_size != 20 && value_size != 24) return -1;
void *value = malloc(value_size);
if (!value) return -1;
if (xattr_read(ctx, &c, value, value_size) != 0) {
free(value);
return -1;
}
*out_data = value;
*out_size = value_size;
return 0;
}
return 0;
}
/* ---- Inode + directory parsing ----
*
* All structs are stored little-endian. We use memcpy into local typed
* variables instead of casting (avoids alignment issues on platforms
* that care, plus MSVC warning-clean).
*/
#pragma pack(push, 1)
typedef struct {
uint16_t type;
uint16_t mode;
uint16_t uid_idx;
uint16_t gid_idx;
uint32_t mtime;
uint32_t inode_number;
} sqfs_inode_common_t;
typedef struct {
uint32_t block_idx;
uint32_t nlink;
uint16_t file_size;
uint16_t block_offset;
uint32_t parent_inode;
} sqfs_basic_dir_t;
typedef struct {
uint32_t nlink;
uint32_t file_size;
uint32_t block_idx;
uint32_t parent_inode;
uint16_t index_count;
uint16_t block_offset;
uint32_t xattr_idx;
} sqfs_ext_dir_t;
typedef struct {
uint32_t blocks_start;
uint32_t frag_idx;
uint32_t block_offset;
uint32_t file_size;
/* uint32_t block_sizes[] follows */
} sqfs_basic_file_t;
typedef struct {
uint64_t blocks_start;
uint64_t file_size;
uint64_t sparse;
uint32_t nlink;
uint32_t frag_idx;
uint32_t block_offset;
uint32_t xattr_idx;
/* uint32_t block_sizes[] follows */
} sqfs_ext_file_t;
typedef struct {
uint32_t nlink;
uint32_t target_size;
/* char target[target_size] follows */
} sqfs_basic_symlink_t;
typedef struct {
uint32_t nlink;
uint32_t rdev;
} sqfs_basic_dev_t;
typedef struct {
uint32_t count;
uint32_t start_block;
uint32_t inode_number;
} sqfs_dir_header_t;
typedef struct {
uint16_t offset;
int16_t inode_offset;
uint16_t type;
uint16_t name_size;
/* char name[name_size+1] follows */
} sqfs_dir_entry_t;
#pragma pack(pop)
/* Track ancestors for cycle detection (squashfs shouldn't have them, but
* defensive). Static stack OK - max depth ~30 in real filesystems. */
#define WALK_MAX_DEPTH 64
typedef struct {
sqfs_ctx_t *ctx;
sqfs_walk_cb_t cb;
void *user;
char path_buf[4096];
size_t path_len;
int depth;
int cb_rc; /* sticky non-zero stops further callbacks */
/* stats */
size_t n_files;
size_t n_dirs;
size_t n_syms;
size_t n_special;
} walk_state_t;
/* Push a name onto path_buf, return previous length to pop later. */
static size_t path_push(walk_state_t *w, const char *name, size_t namelen)
{
size_t prev = w->path_len;
if (prev + 1 + namelen + 1 > sizeof(w->path_buf)) {
return prev; /* truncate silently rather than overflow */
}
if (prev > 1 || (prev == 1 && w->path_buf[0] != '/')) {
w->path_buf[w->path_len++] = '/';
} else if (prev == 0) {
w->path_buf[w->path_len++] = '/';
}
memcpy(w->path_buf + w->path_len, name, namelen);
w->path_len += namelen;
w->path_buf[w->path_len] = 0;
return prev;
}
static void path_pop(walk_state_t *w, size_t prev) {
w->path_len = prev;
w->path_buf[w->path_len] = 0;
}
/* Forward decls. */
static int walk_inode(walk_state_t *w, uint64_t inode_ref);
/* Resolve inode_ref (high48 = compressed block_off, low16 = byte_off)
* to a pointer inside the decompressed inode stream + the inode common header. */
static int read_inode_common(sqfs_ctx_t *ctx, uint64_t ref,
const uint8_t **out_p, sqfs_inode_common_t *out_hdr)
{
uint64_t block_off = ref >> 16;
uint16_t byte_off = (uint16_t)(ref & 0xffff);
int64_t d = stream_resolve(&ctx->inode_stream, block_off, byte_off);
if (d < 0 || (size_t)d + sizeof(sqfs_inode_common_t) > ctx->inode_stream.len) {
LOG_E("inode ref 0x%llx resolves out of range (d=%lld)",
(unsigned long long)ref, (long long)d);
return -1;
}
*out_p = ctx->inode_stream.buf + d;
memcpy(out_hdr, *out_p, sizeof(*out_hdr));
return 0;
}
/* Walk a directory metadata block starting at (dir_block_off, byte_off)
* with logical size `dir_size` (includes the 3 bytes of `. ..` accounting).
* For each entry, recurse into the child inode. */
static int walk_directory(walk_state_t *w, uint32_t dir_block_off,
uint16_t byte_off, uint32_t dir_size)
{
sqfs_ctx_t *ctx = w->ctx;
int64_t d = stream_resolve(&ctx->dir_stream, dir_block_off, byte_off);
if (d < 0 || (size_t)d > ctx->dir_stream.len) {
LOG_E("dir block 0x%x byte_off=%u not in dir stream",
dir_block_off, byte_off);
return -1;
}
const uint8_t *base = ctx->dir_stream.buf + d;
/* Per spec: file_size is 3 bytes more than the actual directory bytes,
* accounting for `.` and `..` which aren't stored. So actual bytes = size - 3. */
if (dir_size < 3) return 0;
size_t bytes_left = dir_size - 3;
/* dir_size is image-controlled; clamp the scan window to what actually
* remains in the decompressed dir stream so `end` cannot run past buf. */
{
size_t avail = ctx->dir_stream.len - (size_t)d;
if (bytes_left > avail) bytes_left = avail;
}
const uint8_t *p = base;
const uint8_t *end = base + bytes_left;
while (p < end) {
if (p + sizeof(sqfs_dir_header_t) > end) {
LOG_W("dir at block=0x%x: truncated header", dir_block_off);
break;
}
sqfs_dir_header_t dh;
memcpy(&dh, p, sizeof(dh));
p += sizeof(dh);
uint32_t count = dh.count + 1; /* stored as count-1 */
for (uint32_t i = 0; i < count; i++) {
if (p + sizeof(sqfs_dir_entry_t) > end) {
LOG_W("dir at block=0x%x: truncated entry", dir_block_off);
return -1;
}
sqfs_dir_entry_t de;
memcpy(&de, p, sizeof(de));
p += sizeof(de);
size_t name_len = (size_t)de.name_size + 1;
if (p + name_len > end) {
LOG_W("dir at block=0x%x: truncated name", dir_block_off);
return -1;
}
char name[256];
size_t name_copy = name_len < sizeof(name) - 1 ? name_len : sizeof(name) - 1;
memcpy(name, p, name_copy);
name[name_copy] = 0;
p += name_len;
/* Build inode ref for child. */
uint64_t child_ref = ((uint64_t)dh.start_block << 16) | de.offset;
size_t prev = path_push(w, name, name_copy);
int rc = walk_inode(w, child_ref);
path_pop(w, prev);
if (rc) return rc;
if (w->cb_rc) return w->cb_rc;
}
}
return 0;
}
static int walk_inode(walk_state_t *w, uint64_t inode_ref)
{
sqfs_ctx_t *ctx = w->ctx;
if (w->depth >= WALK_MAX_DEPTH) {
LOG_W("walk depth limit hit at %s", w->path_buf);
return 0;
}
const uint8_t *p = NULL;
sqfs_inode_common_t hdr;
if (read_inode_common(ctx, inode_ref, &p, &hdr) != 0) return -1;
const uint8_t *body = p + sizeof(hdr);
size_t body_avail = ctx->inode_stream.len -
(size_t)(body - ctx->inode_stream.buf);
sqfs_entry_t e = { 0 };
e.path = w->path_buf;
e.type = hdr.type;
e.mode = hdr.mode;
e.uid = id_lookup(ctx, hdr.uid_idx);
e.gid = id_lookup(ctx, hdr.gid_idx);
e.mtime = hdr.mtime;
e._file_inode_off = (size_t)(p - ctx->inode_stream.buf);
e._xattr_idx = UINT32_MAX;
switch (hdr.type) {
case SQFS_DIR_TYPE: {
if (body_avail < sizeof(sqfs_basic_dir_t)) return -1;
sqfs_basic_dir_t d;
memcpy(&d, body, sizeof(d));
e.size = 0;
if (w->cb_rc == 0) w->cb_rc = w->cb(&e, w->user);
if (w->cb_rc) return w->cb_rc;
w->n_dirs++;
w->depth++;
int rc = walk_directory(w, d.block_idx, d.block_offset, d.file_size);
w->depth--;
return rc;
}
case SQFS_EDIR_TYPE: {
if (body_avail < sizeof(sqfs_ext_dir_t)) return -1;
sqfs_ext_dir_t d;
memcpy(&d, body, sizeof(d));
e.size = 0;
if (w->cb_rc == 0) w->cb_rc = w->cb(&e, w->user);
if (w->cb_rc) return w->cb_rc;
w->n_dirs++;
w->depth++;
int rc = walk_directory(w, d.block_idx, d.block_offset, d.file_size);
w->depth--;
return rc;
}
case SQFS_REG_TYPE: {
if (body_avail < sizeof(sqfs_basic_file_t)) return -1;
sqfs_basic_file_t f;
memcpy(&f, body, sizeof(f));
e.size = f.file_size;
if (w->cb_rc == 0) w->cb_rc = w->cb(&e, w->user);
w->n_files++;
return w->cb_rc;
}
case SQFS_EREG_TYPE: {
if (body_avail < sizeof(sqfs_ext_file_t)) return -1;
sqfs_ext_file_t f;
memcpy(&f, body, sizeof(f));
e.size = f.file_size;
e._xattr_idx = f.xattr_idx;
if (w->cb_rc == 0) w->cb_rc = w->cb(&e, w->user);
w->n_files++;
return w->cb_rc;
}
case SQFS_SYM_TYPE:
case SQFS_ESYM_TYPE: {
if (body_avail < sizeof(sqfs_basic_symlink_t)) return -1;
sqfs_basic_symlink_t s;
memcpy(&s, body, sizeof(s));
if (body_avail < sizeof(s) + s.target_size) return -1;
e.size = s.target_size;
e.symlink_target = (const char *)(body + sizeof(s));
e.symlink_target_size = s.target_size;
if (w->cb_rc == 0) w->cb_rc = w->cb(&e, w->user);
w->n_syms++;
return w->cb_rc;
}
case SQFS_BLK_TYPE: case SQFS_CHR_TYPE:
case SQFS_EBLK_TYPE: case SQFS_ECHR_TYPE: {
if (body_avail < sizeof(sqfs_basic_dev_t)) return -1;
sqfs_basic_dev_t dv;
memcpy(&dv, body, sizeof(dv));
e.rdev = dv.rdev;
if (w->cb_rc == 0) w->cb_rc = w->cb(&e, w->user);
w->n_special++;
return w->cb_rc;
}
case SQFS_FIFO_TYPE: case SQFS_SOCK_TYPE:
case SQFS_EFIFO_TYPE: case SQFS_ESOCK_TYPE: {
if (w->cb_rc == 0) w->cb_rc = w->cb(&e, w->user);
w->n_special++;
return w->cb_rc;
}
default:
LOG_W("inode at %s: unknown type %u", w->path_buf, hdr.type);
return 0;
}
}
int sqfs_walk(sqfs_ctx_t *ctx, sqfs_walk_cb_t cb, void *user)
{
walk_state_t w = { 0 };
w.ctx = ctx;
w.cb = cb;
w.user = user;
w.path_buf[0] = 0;
LOG_I("sqfs_walk: starting at root_inode_ref=0x%llx",
(unsigned long long)ctx->sb.root_inode_ref);
int rc = walk_inode(&w, ctx->sb.root_inode_ref);
LOG_I(" walk done: rc=%d cb_rc=%d files=%zu dirs=%zu syms=%zu special=%zu",
rc, w.cb_rc, w.n_files, w.n_dirs, w.n_syms, w.n_special);
return rc ? rc : w.cb_rc;
}
/* Reusable XZ decoder for hot-path data block decompression. xz_dec_init
* allocates ~100 KB of LZMA2 state; doing it per data block (50k+ for a
* desktop rootfs) burns 5+ GB of allocator churn. xz_dec_reset is cheap.
*
* TLS: with the multi-threaded decompress pipeline each worker gets its
* own instance (xz_dec is not thread-safe; one shared instance would
* corrupt across concurrent decompresses). */
static __declspec(thread) struct xz_dec *g_data_dec = NULL;
static int data_decompress(const uint8_t *cbuf, size_t in_size,
uint8_t *out, size_t out_cap, size_t *out_len)
{
if (!g_data_dec) {
g_data_dec = xz_dec_init(XZ_DYNALLOC, 1u << 20);
if (!g_data_dec) { LOG_E("xz_dec_init OOM"); return -1; }
} else {
xz_dec_reset(g_data_dec);
}
struct xz_buf b = { 0 };
b.in = cbuf; b.in_size = in_size;
b.out = out; b.out_size = out_cap;
enum xz_ret r;
for (;;) {
size_t pi = b.in_pos, po = b.out_pos;
r = xz_dec_run(g_data_dec, &b);
if (r == XZ_STREAM_END) break;
if (r != XZ_OK || (b.in_pos == pi && b.out_pos == po)) {
LOG_E("data_decompress: rc=%d in=%zu/%zu out=%zu/%zu",
(int)r, b.in_pos, b.in_size, b.out_pos, b.out_size);
return -1;
}
}
*out_len = b.out_pos;
return 0;
}
/* ---- File content read ----
*
* Reads all data blocks for a regular file, concatenating into out_data.
* Fragment tail (if any) appended last.
*/
int sqfs_read_file(sqfs_ctx_t *ctx, const sqfs_entry_t *entry,
void **out_data, size_t *out_size)
{
const uint8_t *p = ctx->inode_stream.buf + entry->_file_inode_off;
sqfs_inode_common_t hdr;
size_t inode_off;
size_t body_avail;
memcpy(&hdr, p, sizeof(hdr));
const uint8_t *body = p + sizeof(hdr);
uint64_t blocks_start;
uint64_t file_size;
uint32_t frag_idx;
uint32_t frag_off;
const uint32_t *block_sizes;
size_t block_list_avail; /* # of uint32 block_sizes[] entries in body */
/* body must lie within the decompressed inode stream; bytes after the
* fixed struct hold the variable-length block_sizes[] array. */
inode_off = (size_t)(body - ctx->inode_stream.buf);
if (body < ctx->inode_stream.buf || inode_off > ctx->inode_stream.len) {
LOG_E("sqfs_read_file: %s: inode body out of range", entry->path);
return -1;
}
body_avail = ctx->inode_stream.len - inode_off;
if (hdr.type == SQFS_REG_TYPE) {
sqfs_basic_file_t f;
if (body_avail < sizeof(f)) {
LOG_E("sqfs_read_file: %s: inode body too short", entry->path);
return -1;
}
memcpy(&f, body, sizeof(f));
blocks_start = f.blocks_start;
file_size = f.file_size;
frag_idx = f.frag_idx;
frag_off = f.block_offset;
block_sizes = (const uint32_t *)(body + sizeof(f));
block_list_avail = (body_avail - sizeof(f)) / sizeof(uint32_t);
} else if (hdr.type == SQFS_EREG_TYPE) {
sqfs_ext_file_t f;
if (body_avail < sizeof(f)) {
LOG_E("sqfs_read_file: %s: inode body too short", entry->path);
return -1;
}
memcpy(&f, body, sizeof(f));
blocks_start = f.blocks_start;
file_size = f.file_size;
frag_idx = f.frag_idx;
frag_off = f.block_offset;
block_sizes = (const uint32_t *)(body + sizeof(f));
block_list_avail = (body_avail - sizeof(f)) / sizeof(uint32_t);
} else {
LOG_E("sqfs_read_file: %s is not a regular file (type=%u)",
entry->path, hdr.type);
return -1;
}
size_t block_size = ctx->sb.block_size;
if (block_size == 0) {
LOG_E("sqfs_read_file: %s: zero block_size", entry->path);
return -1;
}
size_t n_full = (size_t)(file_size / block_size);
int has_frag = (frag_idx != 0xffffffffu);
size_t tail = (size_t)(file_size % block_size);
uint8_t *out = (uint8_t *)malloc(file_size ? file_size : 1);
if (!out) return -1;
uint64_t disk = blocks_start;
size_t written = 0;
/* Full blocks (and the last block if no fragment). */
size_t block_list_count = has_frag ? n_full : (n_full + (tail ? 1 : 0));
/* The block_sizes[] array is image-controlled in length only via the
* inode body; reject if the inode does not actually carry that many
* uint32 entries (else block_sizes[i] reads past inode_stream). */
if (block_list_count > block_list_avail) {
LOG_E("sqfs_read_file: %s: block_list_count %zu exceeds inode body (%zu)",
entry->path, block_list_count, block_list_avail);
free(out); return -1;
}
for (size_t i = 0; i < block_list_count; i++) {
uint32_t bs = block_sizes[i];
int uncompressed = (bs & 0x01000000) ? 1 : 0;
size_t on_disk = bs & 0x00ffffffu;
size_t this_block_logical = (i < n_full) ? block_size : tail;
/* Bytes still free in `out`. Every write below must stay within it;
* on valid input each block writes exactly this_block_logical and
* the sum equals file_size, so the normal path is unchanged. */
size_t remaining = (size_t)file_size - written;
if (this_block_logical > remaining) {
LOG_E("sqfs_read_file: %s: block %zu overruns output", entry->path, i);
free(out); return -1;
}
if (on_disk == 0) {
/* sparse - zero fill */
memset(out + written, 0, this_block_logical);
written += this_block_logical;
continue;
}
if (uncompressed) {
/* An uncompressed block holds its bytes verbatim; it must equal
* the logical block length and fit in the remaining output. */
if (on_disk != this_block_logical) {
LOG_E("sqfs_read_file: %s: uncompressed block %zu size %zu != %zu",
entry->path, i, on_disk, this_block_logical);
free(out); return -1;
}
if (sqfs_read_raw(ctx, disk, out + written, on_disk) != 0) {
free(out); return -1;
}
written += on_disk;
} else {
/* Compressed data block. Reuse the cached XZ decoder via
* data_decompress (xz_dec_reset is cheap, init is not). */
uint8_t *cbuf = (uint8_t *)malloc(on_disk);
if (!cbuf) { free(out); return -1; }
if (sqfs_read_raw(ctx, disk, cbuf, on_disk) != 0) {
free(cbuf); free(out); return -1;
}
size_t got = 0;
/* Output cap is the true remaining logical block length, not the
* full block_size: the tail block has only `tail` bytes left. */
if (data_decompress(cbuf, on_disk, out + written, this_block_logical, &got) != 0) {
LOG_E("data block decompress at 0x%llx", (unsigned long long)disk);
free(cbuf); free(out); return -1;
}
if (got != this_block_logical) {
LOG_E("sqfs_read_file: %s: block %zu decompressed %zu != %zu",
entry->path, i, got, this_block_logical);
free(cbuf); free(out); return -1;
}
free(cbuf);
written += got;
}
disk += on_disk;
}
/* Fragment tail. */
if (has_frag && tail > 0) {
if ((size_t)frag_idx >= ctx->fragment_count) {
LOG_E("%s: fragment_idx %u out of range", entry->path, frag_idx);
free(out); return -1;
}
sqfs_fragment_entry_t fe;
memcpy(&fe,
(const uint8_t *)ctx->fragment_index + frag_idx * sizeof(fe),
sizeof(fe));
int uncompressed = (fe.size & 0x01000000) ? 1 : 0;
size_t on_disk = fe.size & 0x00ffffffu;
uint8_t *fragbuf = (uint8_t *)malloc(on_disk);
if (!fragbuf) { free(out); return -1; }
if (sqfs_read_raw(ctx, fe.start, fragbuf, on_disk) != 0) {
free(fragbuf); free(out); return -1;
}
uint8_t *fragdec = fragbuf;
size_t fragdec_size = on_disk;
uint8_t *fragdec_alloc = NULL;
if (!uncompressed) {
fragdec_alloc = (uint8_t *)malloc(block_size);
if (!fragdec_alloc) { free(fragbuf); free(out); return -1; }
size_t got = 0;
if (data_decompress(fragbuf, on_disk, fragdec_alloc, block_size, &got) != 0) {
free(fragdec_alloc); free(fragbuf); free(out); return -1;
}
fragdec = fragdec_alloc;
fragdec_size = got;
}
if (frag_off + tail > fragdec_size) {
LOG_E("%s: fragment slice out of range", entry->path);
free(fragdec_alloc); free(fragbuf); free(out); return -1;
}
memcpy(out + written, fragdec + frag_off, tail);
written += tail;
free(fragdec_alloc); free(fragbuf);
}
if (written != file_size) {
LOG_E("%s: written=%zu != file_size=%llu",
entry->path, written, (unsigned long long)file_size);
free(out); return -1;
}
*out_data = out;
*out_size = (size_t)file_size;
return 0;
}