Files
appsandbox/tools/iso-patch/engine/ext4.c
T
James Stringer 2135f5cbeb Direct Ubuntu ISO->VHDX build (no subiquity, no cidata)
iso-patch.exe gains a new --ubuntu-to-vhdx mode that produces a fully
installed, bootable Ubuntu VHDX in one shot:
  - vendored xz-embedded + custom ext4 writer + squashfs reader
  - mounts the ISO, partitions GPT (ESP + Linux root), streams
    casper/minimal.squashfs into a freshly-built ext4 rootfs through
    a producer/N-decompress-worker/single-writer pipeline
  - installs signed shim + grub from the ISO's pool .debs onto ESP,
    writes a redirect grub.cfg pointing at the rootfs by UUID
  - plants a defensive one-shot first-boot systemd unit that runs
    grub-install + update-grub + AppSandbox guest setup (agent ELFs,
    kernel modules, systemd units, wsl-mesa, wsl-deps, ldconfig,
    no-sleep dconf, etc.) and reboots

linux_create_thread now spawns iso-patch with --ubuntu-to-vhdx
and a --stage manifest covering the full extras tree (reuses
stage_linux_agent_and_extras to populate a staging dir then walks
it). image_path is cleared post-build so the ISO isn't re-attached
to the runtime VM. ComPorts/com1 named pipe and the do_connect_rdp
window path are dropped (IDD is the only live display path now).

VideoMonitor stays unconditional (vmwp.exe crashes 0xC0000005
without it) — but with no ConnectionOptions/named pipe, matching
master.

JS frontend: "Installing Linux" branch added so the spinner label
no longer says "Installing Windows" for Linux VMs. Progress %
now tracks ingest fraction (n_processed / total_inodes) mapped
into 10..75%, with post-ingest phases at 82..99%.
2026-05-25 22:34:57 -04:00

1481 lines
54 KiB
C

/* ext4.c - minimal ext4 image writer. See ext4.h for design notes.
*
* On-disk structures from the ext4 disk-format docs:
* https://www.kernel.org/doc/html/latest/filesystems/ext4/index.html
*
* Convention: every field on disk is little-endian. Windows is LE so we
* write naturally; if this ever runs on BE we'd need bswap wrappers.
*/
#include "ext4.h"
#include "log.h"
#include <windows.h>
#include <winioctl.h>
#include <objbase.h> /* CoCreateGuid */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define BLOCK_SIZE 4096u
#define INODE_SIZE 256u
#define BLOCKS_PER_GROUP 32768u
#define INODES_PER_GROUP 1024u
#define EXT4_SUPER_MAGIC 0xEF53u
/* Reserved inodes (must match kernel's expectations). */
#define EXT4_ROOT_INO 2u
#define EXT4_LOST_FOUND_INO 11u
#define EXT4_FIRST_INO 11u /* s_first_ino: first non-reserved */
/* Feature flags - kept very small. */
#define EXT4_FEATURE_COMPAT 0
#define EXT4_FEATURE_INCOMPAT (0x02u | 0x40u) /* FILETYPE | EXTENTS */
#define EXT4_FEATURE_RO_COMPAT (0x01u | 0x02u | 0x08u | 0x20u | 0x40u)
/* SPARSE_SUPER | LARGE_FILE | HUGE_FILE
* | DIR_NLINK | EXTRA_ISIZE */
/* File-type bits in dir entry (s_feature_incompat & FILETYPE). */
#define EXT4_FT_UNKNOWN 0
#define EXT4_FT_REG_FILE 1
#define EXT4_FT_DIR 2
#define EXT4_FT_CHRDEV 3
#define EXT4_FT_BLKDEV 4
#define EXT4_FT_FIFO 5
#define EXT4_FT_SOCK 6
#define EXT4_FT_SYMLINK 7
/* Inode mode type bits (S_IF*). */
#define S_IFMT 0xF000u
#define S_IFSOCK 0xC000u
#define S_IFLNK 0xA000u
#define S_IFREG 0x8000u
#define S_IFBLK 0x6000u
#define S_IFDIR 0x4000u
#define S_IFCHR 0x2000u
#define S_IFIFO 0x1000u
/* Inode flags. */
#define EXT4_EXTENTS_FL 0x00080000u
/* Extent constants. */
#define EXT4_EXT_MAGIC 0xF30Au
/* ---- On-disk structs ---- */
#pragma pack(push, 1)
typedef struct {
uint32_t s_inodes_count;
uint32_t s_blocks_count_lo;
uint32_t s_r_blocks_count_lo;
uint32_t s_free_blocks_count_lo;
uint32_t s_free_inodes_count;
uint32_t s_first_data_block;
uint32_t s_log_block_size;
uint32_t s_log_cluster_size;
uint32_t s_blocks_per_group;
uint32_t s_clusters_per_group;
uint32_t s_inodes_per_group;
uint32_t s_mtime;
uint32_t s_wtime;
uint16_t s_mnt_count;
uint16_t s_max_mnt_count;
uint16_t s_magic;
uint16_t s_state;
uint16_t s_errors;
uint16_t s_minor_rev_level;
uint32_t s_lastcheck;
uint32_t s_checkinterval;
uint32_t s_creator_os;
uint32_t s_rev_level;
uint16_t s_def_resuid;
uint16_t s_def_resgid;
uint32_t s_first_ino;
uint16_t s_inode_size;
uint16_t s_block_group_nr;
uint32_t s_feature_compat;
uint32_t s_feature_incompat;
uint32_t s_feature_ro_compat;
uint8_t s_uuid[16];
char s_volume_name[16];
char s_last_mounted[64];
uint32_t s_algorithm_usage_bitmap;
uint8_t s_prealloc_blocks;
uint8_t s_prealloc_dir_blocks;
uint16_t s_reserved_gdt_blocks;
uint8_t s_journal_uuid[16];
uint32_t s_journal_inum;
uint32_t s_journal_dev;
uint32_t s_last_orphan;
uint32_t s_hash_seed[4];
uint8_t s_def_hash_version;
uint8_t s_jnl_backup_type;
uint16_t s_desc_size;
uint32_t s_default_mount_opts;
uint32_t s_first_meta_bg;
uint32_t s_mkfs_time;
uint32_t s_jnl_blocks[17];
uint32_t s_blocks_count_hi;
uint32_t s_r_blocks_count_hi;
uint32_t s_free_blocks_count_hi;
uint16_t s_min_extra_isize;
uint16_t s_want_extra_isize;
uint32_t s_flags;
uint16_t s_raid_stride;
uint16_t s_mmp_update_interval;
uint64_t s_mmp_block;
uint32_t s_raid_stripe_width;
uint8_t s_log_groups_per_flex;
uint8_t s_checksum_type;
uint8_t s_encryption_level;
uint8_t s_reserved_pad;
uint64_t s_kbytes_written;
uint32_t s_snapshot_inum;
uint32_t s_snapshot_id;
uint64_t s_snapshot_r_blocks_count;
uint32_t s_snapshot_list;
uint32_t s_error_count;
uint32_t s_first_error_time;
uint32_t s_first_error_ino;
uint64_t s_first_error_block;
uint8_t s_first_error_func[32];
uint32_t s_first_error_line;
uint32_t s_last_error_time;
uint32_t s_last_error_ino;
uint32_t s_last_error_line;
uint64_t s_last_error_block;
uint8_t s_last_error_func[32];
uint8_t s_mount_opts[64];
uint32_t s_usr_quota_inum;
uint32_t s_grp_quota_inum;
uint32_t s_overhead_clusters;
uint32_t s_backup_bgs[2];
uint8_t s_encrypt_algos[4];
uint8_t s_encrypt_pw_salt[16];
uint32_t s_lpf_ino;
uint32_t s_prj_quota_inum;
uint32_t s_checksum_seed;
uint8_t s_wtime_hi;
uint8_t s_mtime_hi;
uint8_t s_mkfs_time_hi;
uint8_t s_lastcheck_hi;
uint8_t s_first_error_time_hi;
uint8_t s_last_error_time_hi;
uint8_t s_first_error_errcode;
uint8_t s_last_error_errcode;
uint16_t s_encoding;
uint16_t s_encoding_flags;
uint32_t s_orphan_file_inum;
uint32_t s_reserved[94];
uint32_t s_checksum;
} ext4_superblock_t;
/* sizeof(ext4_superblock_t) must equal 1024 bytes. Compile-time check below. */
typedef struct {
uint32_t bg_block_bitmap_lo;
uint32_t bg_inode_bitmap_lo;
uint32_t bg_inode_table_lo;
uint16_t bg_free_blocks_count_lo;
uint16_t bg_free_inodes_count_lo;
uint16_t bg_used_dirs_count_lo;
uint16_t bg_flags;
uint32_t bg_exclude_bitmap_lo;
uint16_t bg_block_bitmap_csum_lo;
uint16_t bg_inode_bitmap_csum_lo;
uint16_t bg_itable_unused_lo;
uint16_t bg_checksum;
} ext4_group_desc_t;
/* sizeof = 32. */
typedef struct {
uint16_t i_mode;
uint16_t i_uid;
uint32_t i_size_lo;
uint32_t i_atime;
uint32_t i_ctime;
uint32_t i_mtime;
uint32_t i_dtime;
uint16_t i_gid;
uint16_t i_links_count;
uint32_t i_blocks_lo;
uint32_t i_flags;
uint32_t i_osd1;
uint8_t i_block[60];
uint32_t i_generation;
uint32_t i_file_acl_lo;
uint32_t i_size_hi;
uint32_t i_obso_faddr;
uint16_t i_osd2_blocks_hi;
uint16_t i_osd2_file_acl_hi;
uint16_t i_osd2_uid_hi;
uint16_t i_osd2_gid_hi;
uint16_t i_osd2_checksum_lo;
uint16_t i_osd2_reserved;
uint16_t i_extra_isize;
uint16_t i_checksum_hi;
uint32_t i_ctime_extra;
uint32_t i_mtime_extra;
uint32_t i_atime_extra;
uint32_t i_crtime;
uint32_t i_crtime_extra;
uint32_t i_version_hi;
uint32_t i_projid;
} ext4_inode_t;
/* sizeof = 0xa0 = 160. Inode-size 256 leaves 96 trailing bytes (xattrs). */
typedef struct {
uint16_t eh_magic;
uint16_t eh_entries;
uint16_t eh_max;
uint16_t eh_depth;
uint32_t eh_generation;
} ext4_extent_header_t;
/* sizeof = 12. */
typedef struct {
uint32_t ee_block;
uint16_t ee_len;
uint16_t ee_start_hi;
uint32_t ee_start_lo;
} ext4_extent_t;
/* sizeof = 12. */
typedef struct {
uint32_t ei_block; /* first logical block this index covers */
uint32_t ei_leaf_lo; /* physical block holding the child node */
uint16_t ei_leaf_hi;
uint16_t ei_unused;
} ext4_extent_idx_t;
/* sizeof = 12. */
typedef struct {
uint32_t inode;
uint16_t rec_len;
uint8_t name_len;
uint8_t file_type;
/* char name[]; */
} ext4_dir_entry_t;
#pragma pack(pop)
/* ---- In-memory state ---- */
typedef struct dir_entry_rec {
char name[256]; /* null-terminated */
uint8_t name_len;
uint8_t file_type;
uint32_t inode;
struct dir_entry_rec *next;
} dir_entry_rec_t;
typedef struct dir_node {
uint32_t inode;
struct dir_node *parent;
dir_entry_rec_t *entries;
dir_entry_rec_t *entries_tail;
/* children dir_nodes - for lookup by name; linked sibling chain */
struct dir_node *first_child;
struct dir_node *next_sibling;
char name[256]; /* leaf name; "" for root */
} dir_node_t;
typedef struct {
uint32_t inode;
/* On-disk representation buffered until close. We write the inode body
* directly to its slot when we know enough; for dirs the size + extent
* are known only after we've serialized entries. So buffer everything. */
ext4_inode_t body;
/* If this inode owns data blocks, they're a single contiguous extent. */
uint32_t data_first_block;
uint32_t data_block_count;
/* For dirs only - serialized entry blob (filled at close). */
uint8_t *dir_blob;
uint32_t dir_blob_len;
} inode_rec_t;
struct ext4_writer {
HANDLE phys;
uint64_t part_lba_start;
uint64_t part_lba_count;
uint64_t part_bytes;
/* Layout. */
uint32_t total_blocks;
uint32_t blocks_per_group;
uint32_t inodes_per_group;
uint32_t num_groups;
uint32_t inode_table_blocks_per_group;
uint32_t total_inodes;
/* Per-group metadata layout (relative to group start in blocks):
* non-backup: bbitmap=0, ibitmap=1, itable=2..2+T-1, data=2+T..
* backup : sb=0, gdt=1, bbitmap=2, ibitmap=3, itable=4..4+T-1, data=4+T..
* (We use the same layout in group 0 as backup groups since the SB+GDT
* live there.) */
uint32_t gdt_blocks;
uint8_t *sb_buf; /* canonical SB (1024 bytes, allocated 4096) */
uint8_t *gdt_buf; /* GDT, gdt_blocks * 4096 */
/* Block allocator: per-group "first free data block" cursor. */
uint32_t *next_free_block; /* [num_groups], in group-relative blocks */
uint32_t *blocks_used_in_group; /* [num_groups], count of data blocks allocated */
/* Inode allocator: bump pointer through global inode space. */
uint32_t next_free_inode;
/* Inode records (1..total_inodes). Index 0 unused. Allocated lazily. */
inode_rec_t **inodes; /* [total_inodes + 1] */
uint32_t n_used_inodes;
/* Directory tree. */
dir_node_t *root;
/* UUID + label. */
uint8_t uuid[16];
char label[17];
/* Write coalescer. Synchronous WriteFile to \\.\PhysicalDriveN is
* absurdly slow for many small writes (~30 MB/s no matter what the
* SSD can do) because each syscall pays full I/O-manager + vhdmp +
* NTFS round-trip latency. Coalescing contiguous adjacent writes
* into 16 MiB chunks lets the path see a few large requests instead
* of hundreds of thousands of small ones - 10-20x faster in practice. */
uint8_t *coal_buf;
size_t coal_buf_cap;
uint64_t coal_off; /* absolute partition offset of buf[0] */
size_t coal_len; /* bytes currently buffered */
int coal_active; /* 0 if no active batch */
/* I/O stats - cumulative across the writer's lifetime. */
uint64_t stat_bytes_in; /* bytes passed to part_write */
uint64_t stat_bytes_flushed; /* bytes that hit WriteFile */
uint64_t stat_flush_ticks; /* QPC ticks spent in WriteFile */
uint64_t stat_flush_calls; /* number of WriteFile syscalls */
};
/* ---- Helpers ---- */
#define MIN_OF(a,b) ((a) < (b) ? (a) : (b))
/* Mark a backup-superblock group per sparse_super: group 0, 1, and groups
* with index == 3^n, 5^n, 7^n. */
static int is_backup_group(uint32_t g)
{
if (g == 0 || g == 1) return 1;
for (uint64_t v = 3; v <= g; v *= 3) if (v == g) return 1;
for (uint64_t v = 5; v <= g; v *= 5) if (v == g) return 1;
for (uint64_t v = 7; v <= g; v *= 7) if (v == g) return 1;
return 0;
}
/* Compute the first data block within a group (group-relative). */
static uint32_t group_first_data_block(const ext4_writer_t *w, uint32_t g)
{
uint32_t off = is_backup_group(g) ? (1 + w->gdt_blocks) : 0;
off += 2 /* bitmaps */ + w->inode_table_blocks_per_group;
return off;
}
/* Direct (unbuffered) write straight to the underlying disk handle. */
static int part_write_direct(ext4_writer_t *w, uint64_t off,
const void *buf, size_t len)
{
LARGE_INTEGER li;
li.QuadPart = (LONGLONG)(w->part_lba_start * 512ULL + off);
if (!SetFilePointerEx(w->phys, li, NULL, FILE_BEGIN)) {
LOG_WIN32("SetFilePointerEx(part_write_direct)", GetLastError());
return -1;
}
LARGE_INTEGER t0, t1;
QueryPerformanceCounter(&t0);
DWORD wr = 0;
BOOL ok = WriteFile(w->phys, buf, (DWORD)len, &wr, NULL);
QueryPerformanceCounter(&t1);
w->stat_flush_ticks += (uint64_t)(t1.QuadPart - t0.QuadPart);
w->stat_flush_calls++;
w->stat_bytes_flushed += len;
if (!ok || wr != (DWORD)len) {
LOG_WIN32("WriteFile(part_write_direct)", GetLastError());
LOG_E(" off=%lld len=%zu wr=%lu", (long long)li.QuadPart, len, wr);
return -1;
}
return 0;
}
/* Flush whatever is currently in the coalescing buffer to disk. */
static int part_flush(ext4_writer_t *w)
{
if (!w->coal_active || w->coal_len == 0) return 0;
int rc = part_write_direct(w, w->coal_off, w->coal_buf, w->coal_len);
w->coal_active = 0;
w->coal_len = 0;
return rc;
}
/* Coalesced write: route every write through the aligned coalesce buffer.
* Contiguous adjacent writes append to the active batch (no flush, no
* syscall). On non-contiguous or full, flush + restart. Writes larger
* than the buffer get chunked through it.
*
* Everything must go through this path because the disk handle uses
* FILE_FLAG_NO_BUFFERING and requires sector-aligned BUFFERS, not just
* offsets/lengths - we can't direct-write a malloc'd file-data pointer. */
static int part_write(ext4_writer_t *w, uint64_t off, const void *buf, size_t len)
{
w->stat_bytes_in += len;
const uint8_t *src = (const uint8_t *)buf;
while (len > 0) {
/* If there's an active batch and we're contiguous, try to append. */
if (w->coal_active && off == w->coal_off + (uint64_t)w->coal_len) {
size_t room = w->coal_buf_cap - w->coal_len;
size_t chunk = (len < room) ? len : room;
memcpy(w->coal_buf + w->coal_len, src, chunk);
w->coal_len += chunk;
src += chunk;
off += chunk;
len -= chunk;
if (w->coal_len == w->coal_buf_cap) {
if (part_flush(w) != 0) return -1;
}
continue;
}
/* Not contiguous (or no active batch). Flush + start new. */
if (part_flush(w) != 0) return -1;
w->coal_off = off;
w->coal_active = 1;
}
return 0;
}
/* Write at a logical block number. */
static int part_write_block(ext4_writer_t *w, uint32_t block_no, const void *buf, size_t blocks)
{
return part_write(w, (uint64_t)block_no * BLOCK_SIZE, buf, blocks * BLOCK_SIZE);
}
/* Zero a block range. Caller passes a 4 KB zero buffer. */
static int part_zero_blocks(ext4_writer_t *w, uint32_t block_no,
uint32_t n_blocks, const uint8_t *zero_block)
{
for (uint32_t i = 0; i < n_blocks; i++) {
if (part_write_block(w, block_no + i, zero_block, 1) != 0) return -1;
}
return 0;
}
/* ---- Path navigation ---- */
/* Find or fail-to-find a path's parent dir + leaf name.
* On entry path is absolute; on success *out_parent is the dir_node
* containing the parent, *out_leaf is the leaf component (heap-copied). */
static int split_path(ext4_writer_t *w, const char *path,
dir_node_t **out_parent, char *out_leaf, size_t leaf_cap)
{
if (path[0] != '/') return -1;
if (path[1] == 0) return -1; /* "/" itself */
/* Walk components. */
dir_node_t *cur = w->root;
const char *p = path + 1;
char comp[256];
while (1) {
const char *slash = strchr(p, '/');
size_t complen;
if (slash) {
complen = (size_t)(slash - p);
} else {
complen = strlen(p);
}
if (complen == 0 || complen >= sizeof(comp)) return -1;
memcpy(comp, p, complen);
comp[complen] = 0;
if (!slash) {
/* leaf */
if (complen >= leaf_cap) return -1;
memcpy(out_leaf, comp, complen + 1);
*out_parent = cur;
return 0;
}
/* descend into existing child dir */
dir_node_t *child = cur->first_child;
while (child && strcmp(child->name, comp) != 0) child = child->next_sibling;
if (!child) {
LOG_E("split_path: parent dir %.*s does not exist for %s",
(int)complen, comp, path);
return -1;
}
cur = child;
p = slash + 1;
}
}
/* ---- Inode allocation ---- */
static inode_rec_t *alloc_inode_rec(ext4_writer_t *w, uint32_t inode)
{
if (inode >= w->total_inodes + 1) {
LOG_E("alloc_inode_rec: out of inode space (req=%u max=%u)",
inode, w->total_inodes);
return NULL;
}
inode_rec_t *r = (inode_rec_t *)calloc(1, sizeof(*r));
if (!r) return NULL;
r->inode = inode;
w->inodes[inode] = r;
w->n_used_inodes++;
return r;
}
static uint32_t alloc_next_inode(ext4_writer_t *w)
{
uint32_t n = w->next_free_inode++;
if (n > w->total_inodes) { LOG_E("inode space exhausted"); return 0; }
return n;
}
/* ---- Block allocation ----
*
* We allocate contiguous runs of data blocks per file. Strategy:
* 1. Try to satisfy from the current "favourite" group's free space
* 2. If not enough, move to next group with free space
* 3. Return the absolute block number of the first block in the run
*
* Returns 0 on success. *out_first set to absolute block number. */
static int alloc_data_blocks(ext4_writer_t *w, uint32_t want,
uint32_t *out_first)
{
if (want == 0) { *out_first = 0; return 0; }
for (uint32_t g = 0; g < w->num_groups; g++) {
uint32_t group_blocks = (g == w->num_groups - 1) ?
(w->total_blocks - g * w->blocks_per_group) : w->blocks_per_group;
uint32_t group_data_end = group_blocks; /* group-relative */
uint32_t free_now = group_data_end - w->next_free_block[g];
if (free_now >= want) {
uint32_t first_rel = w->next_free_block[g];
w->next_free_block[g] += want;
w->blocks_used_in_group[g] += want;
*out_first = g * w->blocks_per_group + first_rel;
return 0;
}
}
LOG_D("alloc_data_blocks: cannot satisfy %u blocks (caller will retry smaller)", want);
return -1;
}
/* ---- Inode body construction ---- */
/* Fill in the standard fields. Caller fills type-specific fields after. */
static void inode_init(ext4_inode_t *inode, uint16_t mode_full,
uint32_t uid, uint32_t gid, uint32_t mtime)
{
memset(inode, 0, sizeof(*inode));
inode->i_mode = mode_full;
inode->i_uid = (uint16_t)(uid & 0xffff);
inode->i_osd2_uid_hi = (uint16_t)(uid >> 16);
inode->i_gid = (uint16_t)(gid & 0xffff);
inode->i_osd2_gid_hi = (uint16_t)(gid >> 16);
inode->i_atime = inode->i_mtime = inode->i_ctime = mtime;
inode->i_links_count = 1;
inode->i_extra_isize = (uint16_t)(INODE_SIZE - 128); /* 256 - 128 = 128 */
}
/* Set an extent tree in i_block for a single contiguous run of blocks. */
static void inode_set_single_extent(ext4_inode_t *inode,
uint32_t first_block, uint32_t n_blocks)
{
ext4_extent_header_t h = { 0 };
h.eh_magic = EXT4_EXT_MAGIC;
h.eh_entries = (n_blocks > 0) ? 1 : 0;
h.eh_max = 4; /* fits in 60 bytes: header(12) + 4*ext(12) = 60 */
h.eh_depth = 0;
h.eh_generation = 0;
memcpy(inode->i_block, &h, sizeof(h));
if (n_blocks > 0) {
ext4_extent_t e = { 0 };
e.ee_block = 0;
e.ee_len = (uint16_t)n_blocks;
e.ee_start_lo = first_block;
e.ee_start_hi = 0;
memcpy(inode->i_block + sizeof(h), &e, sizeof(e));
inode->i_flags |= EXT4_EXTENTS_FL;
/* i_blocks counts in 512-byte units (logical "blocks count"). */
inode->i_blocks_lo = n_blocks * (BLOCK_SIZE / 512u);
} else {
/* Empty extents allowed; just header. */
inode->i_flags |= EXT4_EXTENTS_FL;
}
}
/* Forward decls for the writer functions used by inode_set_extents below. */
typedef struct ext4_writer ext4_writer_t_fwd;
static int alloc_data_blocks(struct ext4_writer *w, uint32_t want, uint32_t *out_first);
static int part_write_block(struct ext4_writer *w, uint32_t block_no,
const void *buf, size_t blocks);
/* Extents that fit in the inode's 60-byte i_block field: header (12) +
* up to 4 leaf entries (12 each). For more extents, build a depth-1 tree:
* one index entry in the inode pointing at a leaf block holding all the
* actual leaves (up to ~340 in a 4 KiB block). */
#define INODE_DIRECT_EXTENTS 4u
#define LEAFS_PER_BLOCK ((BLOCK_SIZE - sizeof(ext4_extent_header_t)) / sizeof(ext4_extent_t))
typedef struct { uint32_t first; uint16_t len; uint32_t logical; } ext_run_t;
static void inode_set_extents_direct(ext4_inode_t *inode, const ext_run_t *exts,
int n, uint64_t total_size_512_units)
{
ext4_extent_header_t h = { 0 };
h.eh_magic = EXT4_EXT_MAGIC;
h.eh_entries = (uint16_t)n;
h.eh_max = INODE_DIRECT_EXTENTS;
h.eh_depth = 0;
memcpy(inode->i_block, &h, sizeof(h));
for (int i = 0; i < n; i++) {
ext4_extent_t e = { 0 };
e.ee_block = exts[i].logical;
e.ee_len = exts[i].len;
e.ee_start_lo = exts[i].first;
memcpy(inode->i_block + sizeof(h) + i * sizeof(e), &e, sizeof(e));
}
inode->i_flags |= EXT4_EXTENTS_FL;
inode->i_blocks_lo = (uint32_t)total_size_512_units;
}
/* Build a depth-1 extent tree: allocate one "leaf block" on disk, write
* all N extents into it, set the inode to a single index entry pointing
* at that leaf block. Adds 1 block to i_blocks_lo for the leaf itself. */
static int inode_set_extents_indirect(struct ext4_writer *w, ext4_inode_t *inode,
const ext_run_t *exts, int n,
uint64_t total_size_512_units)
{
if ((size_t)n > LEAFS_PER_BLOCK) {
LOG_E("inode_set_extents_indirect: n=%d exceeds %zu (would need depth=2)",
n, LEAFS_PER_BLOCK);
return -1;
}
uint32_t leaf_block = 0;
if (alloc_data_blocks(w, 1, &leaf_block) != 0) return -1;
/* Build the leaf block: header + N extents, padded with zeros. */
uint8_t leafbuf[BLOCK_SIZE] = { 0 };
ext4_extent_header_t lh = { 0 };
lh.eh_magic = EXT4_EXT_MAGIC;
lh.eh_entries = (uint16_t)n;
lh.eh_max = (uint16_t)LEAFS_PER_BLOCK;
lh.eh_depth = 0;
memcpy(leafbuf, &lh, sizeof(lh));
for (int i = 0; i < n; i++) {
ext4_extent_t e = { 0 };
e.ee_block = exts[i].logical;
e.ee_len = exts[i].len;
e.ee_start_lo = exts[i].first;
memcpy(leafbuf + sizeof(lh) + i * sizeof(e), &e, sizeof(e));
}
if (part_write_block(w, leaf_block, leafbuf, 1) != 0) return -1;
/* Inode header: 1 index entry, depth=1. */
ext4_extent_header_t ih = { 0 };
ih.eh_magic = EXT4_EXT_MAGIC;
ih.eh_entries = 1;
ih.eh_max = INODE_DIRECT_EXTENTS;
ih.eh_depth = 1;
memcpy(inode->i_block, &ih, sizeof(ih));
ext4_extent_idx_t idx = { 0 };
idx.ei_block = 0;
idx.ei_leaf_lo = leaf_block;
memcpy(inode->i_block + sizeof(ih), &idx, sizeof(idx));
inode->i_flags |= EXT4_EXTENTS_FL;
/* +1 for the leaf block itself (in 512-byte units). */
inode->i_blocks_lo = (uint32_t)(total_size_512_units + (BLOCK_SIZE / 512u));
return 0;
}
/* ---- Layout planner + open ---- */
ext4_writer_t *ext4_writer_open(HANDLE phys,
uint64_t partition_lba_start,
uint64_t partition_lba_count,
uint32_t est_inodes)
{
ext4_writer_t *w = (ext4_writer_t *)calloc(1, sizeof(*w));
if (!w) return NULL;
w->phys = phys;
w->part_lba_start = partition_lba_start;
w->part_lba_count = partition_lba_count;
w->part_bytes = partition_lba_count * 512ULL;
w->total_blocks = (uint32_t)(w->part_bytes / BLOCK_SIZE);
w->blocks_per_group = BLOCKS_PER_GROUP;
w->inodes_per_group = INODES_PER_GROUP;
w->num_groups = (w->total_blocks + w->blocks_per_group - 1) / w->blocks_per_group;
/* Inodes per group: scale UP if est_inodes exceeds current default,
* never DOWN. Always a multiple of 8 (kernel requirement). */
if (est_inodes > w->num_groups * w->inodes_per_group) {
uint32_t per = (est_inodes + w->num_groups - 1) / w->num_groups;
per = (per + 7) & ~7u;
w->inodes_per_group = per;
}
w->total_inodes = w->inodes_per_group * w->num_groups;
w->inode_table_blocks_per_group =
(w->inodes_per_group * INODE_SIZE + BLOCK_SIZE - 1) / BLOCK_SIZE;
w->gdt_blocks = (w->num_groups * (uint32_t)sizeof(ext4_group_desc_t)
+ BLOCK_SIZE - 1) / BLOCK_SIZE;
LOG_I("ext4 layout:");
LOG_I(" partition bytes : %llu (%.2f GiB)",
(unsigned long long)w->part_bytes,
(double)w->part_bytes / (1024.0 * 1024.0 * 1024.0));
LOG_I(" block size : %u", BLOCK_SIZE);
LOG_I(" total blocks : %u", w->total_blocks);
LOG_I(" blocks per group : %u", w->blocks_per_group);
LOG_I(" num groups : %u", w->num_groups);
LOG_I(" inode size : %u", INODE_SIZE);
LOG_I(" inodes per group : %u", w->inodes_per_group);
LOG_I(" total inodes : %u", w->total_inodes);
LOG_I(" inode table blocks/g : %u", w->inode_table_blocks_per_group);
LOG_I(" GDT blocks : %u", w->gdt_blocks);
w->next_free_block = (uint32_t *)calloc(w->num_groups, sizeof(uint32_t));
w->blocks_used_in_group = (uint32_t *)calloc(w->num_groups, sizeof(uint32_t));
w->inodes = (inode_rec_t **)calloc((size_t)w->total_inodes + 1, sizeof(inode_rec_t *));
if (!w->next_free_block || !w->blocks_used_in_group || !w->inodes) {
free(w->next_free_block); free(w->blocks_used_in_group); free(w->inodes);
free(w); return NULL;
}
for (uint32_t g = 0; g < w->num_groups; g++) {
w->next_free_block[g] = group_first_data_block(w, g);
}
/* Generate a fresh UUID and zero the label. */
GUID g;
if (CoCreateGuid(&g) != S_OK) {
memset(w->uuid, 0, 16);
} else {
/* GUID layout matches ext4's UUID layout (16 bytes raw). */
memcpy(w->uuid, &g, 16);
}
memset(w->label, 0, sizeof(w->label));
/* Allocate SB + GDT scratch buffers via VirtualAlloc (page-aligned).
* The disk handle uses FILE_FLAG_NO_BUFFERING which requires
* sector-aligned buffer pointers - malloc has no such guarantee. */
w->sb_buf = (uint8_t *)VirtualAlloc(NULL, BLOCK_SIZE,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
w->gdt_buf = (uint8_t *)VirtualAlloc(NULL, (SIZE_T)w->gdt_blocks * BLOCK_SIZE,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
w->coal_buf_cap = 16ULL * 1024ULL * 1024ULL; /* 16 MiB */
w->coal_buf = (uint8_t *)VirtualAlloc(NULL, w->coal_buf_cap,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!w->sb_buf || !w->gdt_buf || !w->coal_buf) {
if (w->sb_buf) VirtualFree(w->sb_buf, 0, MEM_RELEASE);
if (w->gdt_buf) VirtualFree(w->gdt_buf, 0, MEM_RELEASE);
if (w->coal_buf) VirtualFree(w->coal_buf, 0, MEM_RELEASE);
free(w->next_free_block); free(w->blocks_used_in_group);
free(w->inodes); free(w); return NULL;
}
memset(w->sb_buf, 0, BLOCK_SIZE);
memset(w->gdt_buf, 0, (size_t)w->gdt_blocks * BLOCK_SIZE);
/* Inode allocator: 1..10 reserved, lost+found = 11, app inodes start 12. */
w->next_free_inode = EXT4_FIRST_INO + 1;
/* Build root dir node + reserve root inode + lost+found. */
w->root = (dir_node_t *)calloc(1, sizeof(dir_node_t));
w->root->inode = EXT4_ROOT_INO;
inode_rec_t *root_rec = alloc_inode_rec(w, EXT4_ROOT_INO);
inode_init(&root_rec->body, S_IFDIR | 0755, 0, 0, (uint32_t)time(NULL));
root_rec->body.i_links_count = 2; /* . + .. from itself */
/* Create lost+found dir for fsck friendliness. */
dir_node_t *lf = (dir_node_t *)calloc(1, sizeof(dir_node_t));
lf->inode = EXT4_LOST_FOUND_INO;
strcpy(lf->name, "lost+found");
lf->parent = w->root;
lf->next_sibling = w->root->first_child;
w->root->first_child = lf;
inode_rec_t *lf_rec = alloc_inode_rec(w, EXT4_LOST_FOUND_INO);
inode_init(&lf_rec->body, S_IFDIR | 0700, 0, 0, (uint32_t)time(NULL));
lf_rec->body.i_links_count = 2;
/* Add to root's entries list. */
dir_entry_rec_t *de = (dir_entry_rec_t *)calloc(1, sizeof(*de));
strcpy(de->name, "lost+found");
de->name_len = (uint8_t)strlen(de->name);
de->file_type = EXT4_FT_DIR;
de->inode = EXT4_LOST_FOUND_INO;
w->root->entries = de;
w->root->entries_tail = de;
root_rec->body.i_links_count++; /* link from lost+found's .. */
return w;
}
void ext4_writer_set_label(ext4_writer_t *w, const char *label)
{
if (!label) { memset(w->label, 0, sizeof(w->label)); return; }
strncpy(w->label, label, sizeof(w->label) - 1);
w->label[sizeof(w->label) - 1] = 0;
}
void ext4_writer_get_io_stats(const ext4_writer_t *w,
uint64_t *out_bytes_in,
uint64_t *out_bytes_flushed,
uint64_t *out_flush_ticks,
uint64_t *out_flush_calls)
{
if (out_bytes_in) *out_bytes_in = w->stat_bytes_in;
if (out_bytes_flushed) *out_bytes_flushed = w->stat_bytes_flushed;
if (out_flush_ticks) *out_flush_ticks = w->stat_flush_ticks;
if (out_flush_calls) *out_flush_calls = w->stat_flush_calls;
}
void ext4_writer_get_uuid_text(const ext4_writer_t *w, char *out, size_t cap)
{
if (cap < 37) { if (cap) out[0] = 0; return; }
const uint8_t *u = w->uuid;
/* Big-endian text form. ext4 stores raw bytes; canonical text is
* big-endian per RFC 4122. */
snprintf(out, cap,
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
u[0],u[1],u[2],u[3], u[4],u[5], u[6],u[7],
u[8],u[9], u[10],u[11],u[12],u[13],u[14],u[15]);
}
/* ---- add_dir / add_file / add_symlink / add_special ---- */
static int add_dir_entry(dir_node_t *parent, const char *leaf, size_t leaf_len,
uint32_t child_inode, uint8_t file_type)
{
if (leaf_len >= 256) return -1;
dir_entry_rec_t *de = (dir_entry_rec_t *)calloc(1, sizeof(*de));
if (!de) return -1;
memcpy(de->name, leaf, leaf_len);
de->name_len = (uint8_t)leaf_len;
de->file_type = file_type;
de->inode = child_inode;
if (!parent->entries) parent->entries = de;
else parent->entries_tail->next = de;
parent->entries_tail = de;
return 0;
}
int ext4_writer_add_dir(ext4_writer_t *w, const char *path,
uint16_t perms, uint32_t uid, uint32_t gid,
uint32_t mtime)
{
dir_node_t *parent;
char leaf[256];
if (split_path(w, path, &parent, leaf, sizeof(leaf)) != 0) return -1;
/* Idempotent: if a child directory with this name already exists, no-op.
* Callers like the boot-file staging want to "ensure /boot exists" even
* if it was already created during squashfs ingest. */
for (dir_node_t *c = parent->first_child; c; c = c->next_sibling) {
if (strcmp(c->name, leaf) == 0) {
return 0;
}
}
uint32_t ino = alloc_next_inode(w);
if (!ino) return -1;
inode_rec_t *r = alloc_inode_rec(w, ino);
if (!r) return -1;
inode_init(&r->body, S_IFDIR | (perms & 07777), uid, gid, mtime);
r->body.i_links_count = 2; /* . + .. from self */
dir_node_t *child = (dir_node_t *)calloc(1, sizeof(*child));
if (!child) return -1;
strncpy(child->name, leaf, sizeof(child->name) - 1);
child->inode = ino;
child->parent = parent;
child->next_sibling = parent->first_child;
parent->first_child = child;
if (add_dir_entry(parent, leaf, strlen(leaf), ino, EXT4_FT_DIR) != 0) return -1;
/* parent gains a link from this child's ".." */
w->inodes[parent->inode]->body.i_links_count++;
return 0;
}
static int write_file_extents(ext4_writer_t *w, inode_rec_t *r,
const void *data, uint64_t size)
{
if (size == 0) {
inode_set_single_extent(&r->body, 0, 0);
r->body.i_size_lo = 0;
r->body.i_size_hi = 0;
return 0;
}
uint64_t blocks_needed = (size + BLOCK_SIZE - 1) / BLOCK_SIZE;
/* Allocate enough extents. Up to INODE_DIRECT_EXTENTS (4) fit directly
* in the inode; more spill into a depth-1 extent tree (one leaf block
* holding up to LEAFS_PER_BLOCK entries). For files larger than what
* one leaf block can address (~340 extents * 128 MB = ~42 GB), we'd
* need depth-2 - not supported in this POC. */
uint32_t first = 0;
size_t runs_cap = LEAFS_PER_BLOCK;
ext_run_t *runs = (ext_run_t *)calloc(runs_cap, sizeof(ext_run_t));
if (!runs) return -1;
int n_runs = 0;
uint64_t remaining = blocks_needed;
uint32_t logical = 0;
while (remaining > 0) {
if ((size_t)n_runs >= runs_cap) {
LOG_E("ext4_writer: file at inode %u needs > %zu extents (depth-2 not supported)",
r->inode, runs_cap);
free(runs); return -1;
}
uint32_t got_first = 0;
uint32_t try_n = (remaining > 32768u) ? 32768u : (uint32_t)remaining;
int ok = -1;
while (try_n > 0) {
if (alloc_data_blocks(w, try_n, &got_first) == 0) { ok = 0; break; }
try_n /= 2;
}
if (ok != 0 || try_n == 0) {
LOG_E("ext4_writer: out of space allocating %llu blocks (inode %u)",
(unsigned long long)blocks_needed, r->inode);
free(runs); return -1;
}
runs[n_runs].first = got_first;
runs[n_runs].len = (uint16_t)try_n;
runs[n_runs].logical = logical;
n_runs++;
first = (n_runs == 1) ? got_first : first;
logical += try_n;
remaining -= try_n;
}
/* Write data immediately, chunked along the runs. */
const uint8_t *p = (const uint8_t *)data;
uint64_t left = size;
for (int i = 0; i < n_runs; i++) {
uint64_t run_bytes_avail = (uint64_t)runs[i].len * BLOCK_SIZE;
uint64_t this_write = run_bytes_avail < left ? run_bytes_avail : left;
size_t full_blocks = (size_t)(this_write / BLOCK_SIZE);
size_t tail = (size_t)(this_write % BLOCK_SIZE);
if (full_blocks > 0) {
if (part_write_block(w, runs[i].first, p, full_blocks) != 0) {
free(runs); return -1;
}
}
if (tail > 0) {
uint8_t pad[BLOCK_SIZE] = { 0 };
memcpy(pad, p + full_blocks * BLOCK_SIZE, tail);
uint32_t tail_block = runs[i].first + (uint32_t)full_blocks;
if (part_write_block(w, tail_block, pad, 1) != 0) {
free(runs); return -1;
}
}
p += this_write;
left -= this_write;
}
uint64_t blocks_512_units = blocks_needed * (BLOCK_SIZE / 512u);
if (n_runs == 1) {
inode_set_single_extent(&r->body, runs[0].first, runs[0].len);
r->data_first_block = runs[0].first;
r->data_block_count = runs[0].len;
} else if ((uint32_t)n_runs <= INODE_DIRECT_EXTENTS) {
inode_set_extents_direct(&r->body, runs, n_runs, blocks_512_units);
} else {
if (inode_set_extents_indirect(w, &r->body, runs, n_runs, blocks_512_units) != 0) {
free(runs); return -1;
}
}
r->body.i_size_lo = (uint32_t)(size & 0xffffffffu);
r->body.i_size_hi = (uint32_t)(size >> 32);
free(runs);
return 0;
}
int ext4_writer_add_file(ext4_writer_t *w, const char *path,
uint16_t perms, uint32_t uid, uint32_t gid,
uint32_t mtime, const void *data, uint64_t size)
{
dir_node_t *parent;
char leaf[256];
if (split_path(w, path, &parent, leaf, sizeof(leaf)) != 0) return -1;
uint32_t ino = alloc_next_inode(w);
if (!ino) return -1;
inode_rec_t *r = alloc_inode_rec(w, ino);
if (!r) return -1;
inode_init(&r->body, S_IFREG | (perms & 07777), uid, gid, mtime);
if (write_file_extents(w, r, data, size) != 0) return -1;
if (add_dir_entry(parent, leaf, strlen(leaf), ino, EXT4_FT_REG_FILE) != 0) return -1;
return 0;
}
int ext4_writer_add_symlink(ext4_writer_t *w, const char *path,
const char *target, size_t target_len,
uint32_t uid, uint32_t gid, uint32_t mtime)
{
dir_node_t *parent;
char leaf[256];
if (split_path(w, path, &parent, leaf, sizeof(leaf)) != 0) return -1;
uint32_t ino = alloc_next_inode(w);
if (!ino) return -1;
inode_rec_t *r = alloc_inode_rec(w, ino);
if (!r) return -1;
inode_init(&r->body, S_IFLNK | 0777, uid, gid, mtime);
if (target_len < 60) {
/* Fast symlink: target inline in i_block. The cutoff is < 60 (not
* <= 60) because e2fsck requires strnlen(target, 60) != 60 - i.e.
* the inline region must contain at least one trailing NUL byte.
* The kernel doesn't care, but we want a clean fsck. */
memcpy(r->body.i_block, target, target_len);
r->body.i_size_lo = (uint32_t)target_len;
r->body.i_blocks_lo = 0;
/* Important: do NOT set EXTENTS flag for inline symlinks. */
} else {
/* Slow symlink: target in a data block. */
uint8_t buf[BLOCK_SIZE] = { 0 };
if (target_len > BLOCK_SIZE) {
LOG_E("symlink target too long: %zu bytes", target_len);
return -1;
}
memcpy(buf, target, target_len);
uint32_t first;
if (alloc_data_blocks(w, 1, &first) != 0) return -1;
if (part_write_block(w, first, buf, 1) != 0) return -1;
inode_set_single_extent(&r->body, first, 1);
r->body.i_size_lo = (uint32_t)target_len;
}
if (add_dir_entry(parent, leaf, strlen(leaf), ino, EXT4_FT_SYMLINK) != 0) return -1;
return 0;
}
int ext4_writer_add_special(ext4_writer_t *w, const char *path,
uint16_t mode_with_type,
uint32_t uid, uint32_t gid, uint32_t mtime,
uint32_t rdev)
{
dir_node_t *parent;
char leaf[256];
if (split_path(w, path, &parent, leaf, sizeof(leaf)) != 0) return -1;
uint32_t ino = alloc_next_inode(w);
if (!ino) return -1;
inode_rec_t *r = alloc_inode_rec(w, ino);
if (!r) return -1;
inode_init(&r->body, mode_with_type, uid, gid, mtime);
/* For block/char devices, rdev goes into i_block[0] (legacy major/minor
* format) or i_block[1] (new). We use legacy. */
uint16_t typ = mode_with_type & S_IFMT;
uint8_t ft = EXT4_FT_UNKNOWN;
if (typ == S_IFBLK) ft = EXT4_FT_BLKDEV;
else if (typ == S_IFCHR) ft = EXT4_FT_CHRDEV;
else if (typ == S_IFIFO) ft = EXT4_FT_FIFO;
else if (typ == S_IFSOCK) ft = EXT4_FT_SOCK;
if (typ == S_IFBLK || typ == S_IFCHR) {
memcpy(r->body.i_block, &rdev, sizeof(uint32_t));
}
if (add_dir_entry(parent, leaf, strlen(leaf), ino, ft) != 0) return -1;
return 0;
}
/* ---- Close: write everything ---- */
/* State for ext4 dir-entry serialization. */
typedef struct {
uint8_t *buf;
size_t cap;
size_t pos;
size_t cur_block_start;
ext4_dir_entry_t *last_in_block;
} ser_ctx_t;
static int ser_emit(ser_ctx_t *s, uint32_t inode_no, uint8_t file_type,
const char *name, size_t name_len)
{
size_t rec = 8 + ((name_len + 3) & ~(size_t)3);
if (rec > BLOCK_SIZE) return -1;
size_t block_off = s->pos - s->cur_block_start;
/* If this entry would cross the next block boundary, advance to the
* boundary and extend the last entry's rec_len to fill the gap. */
if (block_off + rec > BLOCK_SIZE) {
if (s->last_in_block) {
size_t fill_to = s->cur_block_start + BLOCK_SIZE;
s->last_in_block->rec_len =
(uint16_t)(fill_to - ((uint8_t *)s->last_in_block - s->buf));
}
s->cur_block_start += BLOCK_SIZE;
s->pos = s->cur_block_start;
}
/* Grow buffer if needed. */
if (s->pos + rec > s->cap) {
size_t newcap = s->cap == 0 ? BLOCK_SIZE : s->cap * 2;
while (newcap < s->pos + rec) newcap *= 2;
uint8_t *nb = (uint8_t *)realloc(s->buf, newcap);
if (!nb) return -1;
memset(nb + s->cap, 0, newcap - s->cap);
s->buf = nb;
s->cap = newcap;
}
ext4_dir_entry_t *de = (ext4_dir_entry_t *)(s->buf + s->pos);
de->inode = inode_no;
de->rec_len = (uint16_t)rec;
de->name_len = (uint8_t)name_len;
de->file_type = file_type;
memcpy(s->buf + s->pos + 8, name, name_len);
s->last_in_block = de;
s->pos += rec;
return 0;
}
static int serialize_dir(ext4_writer_t *w, dir_node_t *d, uint32_t parent_ino,
uint8_t **out_buf, uint32_t *out_len)
{
(void)w;
ser_ctx_t s = { 0 };
if (ser_emit(&s, d->inode, EXT4_FT_DIR, ".", 1) != 0) { free(s.buf); return -1; }
if (ser_emit(&s, parent_ino, EXT4_FT_DIR, "..", 2) != 0) { free(s.buf); return -1; }
for (dir_entry_rec_t *e = d->entries; e; e = e->next) {
if (ser_emit(&s, e->inode, e->file_type, e->name, e->name_len) != 0) {
free(s.buf); return -1;
}
}
/* Extend final entry in last block to reach block boundary, and pad
* total length up to a multiple of BLOCK_SIZE. */
if (s.last_in_block) {
size_t fill_to = s.cur_block_start + BLOCK_SIZE;
s.last_in_block->rec_len =
(uint16_t)(fill_to - ((uint8_t *)s.last_in_block - s.buf));
}
size_t padded = (s.pos + BLOCK_SIZE - 1) & ~(size_t)(BLOCK_SIZE - 1);
if (padded == 0) padded = BLOCK_SIZE;
if (padded > s.cap) {
uint8_t *nb = (uint8_t *)realloc(s.buf, padded);
if (!nb) { free(s.buf); return -1; }
s.buf = nb;
}
memset(s.buf + s.pos, 0, padded - s.pos);
*out_buf = s.buf;
*out_len = (uint32_t)padded;
return 0;
}
static int finalize_dirs(ext4_writer_t *w);
static int finalize_dirs_recur(ext4_writer_t *w, dir_node_t *d)
{
/* First serialize my entries, allocate blocks, write inode body. */
uint8_t *blob = NULL;
uint32_t blob_len = 0;
uint32_t parent_ino = d->parent ? d->parent->inode : d->inode; /* root: .. = root */
if (serialize_dir(w, d, parent_ino, &blob, &blob_len) != 0) return -1;
uint32_t n_blocks = blob_len / BLOCK_SIZE;
uint32_t first = 0;
if (alloc_data_blocks(w, n_blocks, &first) != 0) { free(blob); return -1; }
if (part_write_block(w, first, blob, n_blocks) != 0) { free(blob); return -1; }
inode_rec_t *r = w->inodes[d->inode];
r->body.i_size_lo = blob_len;
inode_set_single_extent(&r->body, first, n_blocks);
free(blob);
/* Recurse into children. */
for (dir_node_t *c = d->first_child; c; c = c->next_sibling) {
if (finalize_dirs_recur(w, c) != 0) return -1;
}
return 0;
}
static int finalize_dirs(ext4_writer_t *w)
{
return finalize_dirs_recur(w, w->root);
}
/* Write all inode tables. For each group, build a contiguous buffer of
* inodes_per_group * INODE_SIZE bytes in memory, fill in each used inode's
* slot, and write the whole table in one shot. */
static int write_all_inodes(ext4_writer_t *w)
{
LOG_I("writing %u inode bodies across %u groups", w->n_used_inodes, w->num_groups);
size_t table_bytes = (size_t)w->inodes_per_group * INODE_SIZE;
uint8_t *tbl = (uint8_t *)calloc(1, table_bytes);
if (!tbl) return -1;
for (uint32_t g = 0; g < w->num_groups; g++) {
memset(tbl, 0, table_bytes);
uint32_t any = 0;
for (uint32_t idx = 0; idx < w->inodes_per_group; idx++) {
uint32_t inode_num = g * w->inodes_per_group + idx + 1;
if (inode_num > w->total_inodes) break;
if (!w->inodes[inode_num]) continue;
memcpy(tbl + idx * INODE_SIZE,
&w->inodes[inode_num]->body,
sizeof(ext4_inode_t));
any = 1;
}
if (!any) continue;
uint32_t itable_first_block = g * w->blocks_per_group +
(is_backup_group(g) ? (1 + w->gdt_blocks) : 0) + 2;
if (part_write_block(w, itable_first_block, tbl,
w->inode_table_blocks_per_group) != 0) {
LOG_E("inode table write failed for group %u (block %u)",
g, itable_first_block);
free(tbl);
return -1;
}
}
free(tbl);
return 0;
}
/* Build & write the block bitmap for one group. */
static int write_block_bitmap(ext4_writer_t *w, uint32_t g)
{
uint8_t bm[BLOCK_SIZE];
memset(bm, 0, BLOCK_SIZE);
/* Mark all blocks 0..first_data_block-1 used (the metadata),
* plus all data blocks we allocated (first_data..first_data+blocks_used-1). */
uint32_t group_blocks = (g == w->num_groups - 1) ?
(w->total_blocks - g * w->blocks_per_group) : w->blocks_per_group;
uint32_t fdb = group_first_data_block(w, g);
uint32_t used_up_to = fdb + w->blocks_used_in_group[g];
/* Bits 0..used_up_to-1 set; bits >= group_blocks set (past end). */
for (uint32_t b = 0; b < used_up_to; b++)
bm[b >> 3] |= (uint8_t)(1u << (b & 7));
for (uint32_t b = group_blocks; b < BLOCKS_PER_GROUP; b++)
bm[b >> 3] |= (uint8_t)(1u << (b & 7));
uint32_t bb_block = g * w->blocks_per_group +
(is_backup_group(g) ? (1 + w->gdt_blocks) : 0);
return part_write_block(w, bb_block, bm, 1);
}
/* Build & write the inode bitmap for one group. */
static int write_inode_bitmap(ext4_writer_t *w, uint32_t g)
{
uint8_t bm[BLOCK_SIZE];
memset(bm, 0, BLOCK_SIZE);
uint32_t inode_lo = g * w->inodes_per_group + 1;
uint32_t inode_hi = inode_lo + w->inodes_per_group - 1;
for (uint32_t i = inode_lo; i <= inode_hi; i++) {
if (w->inodes[i]) {
uint32_t b = i - inode_lo;
bm[b >> 3] |= (uint8_t)(1u << (b & 7));
}
}
/* Reserve inodes 1..10 in group 0. */
if (g == 0) {
for (uint32_t i = 1; i <= 10; i++) {
uint32_t b = i - 1;
bm[b >> 3] |= (uint8_t)(1u << (b & 7));
}
}
/* Padding: bits at index >= inodes_per_group must be set (those slots
* don't exist). e2fsck checks this strictly. */
for (uint32_t b = w->inodes_per_group; b < BLOCK_SIZE * 8; b++) {
bm[b >> 3] |= (uint8_t)(1u << (b & 7));
}
uint32_t ib_block = g * w->blocks_per_group +
(is_backup_group(g) ? (1 + w->gdt_blocks) : 0) + 1;
return part_write_block(w, ib_block, bm, 1);
}
/* Build the superblock + GDT in our scratch buffers, write to all backup
* groups + group 0. */
static int write_superblocks_and_gdt(ext4_writer_t *w)
{
/* Compute free counts. Must match the per-group sums in the GDT. */
uint64_t free_blocks = 0;
for (uint32_t g = 0; g < w->num_groups; g++) {
uint32_t group_blocks = (g == w->num_groups - 1) ?
(w->total_blocks - g * w->blocks_per_group) : w->blocks_per_group;
uint32_t used = group_first_data_block(w, g) + w->blocks_used_in_group[g];
free_blocks += (group_blocks - used);
}
/* Reserved inodes 1..10 are always considered used by ext4 conventions;
* root (inode 2) is in that range so we don't double-count. */
uint32_t allocated_above_reserved = 0;
for (uint32_t i = 11; i <= w->total_inodes; i++) {
if (w->inodes[i]) allocated_above_reserved++;
}
uint32_t free_inodes = w->total_inodes - 10 - allocated_above_reserved;
ext4_superblock_t *sb = (ext4_superblock_t *)(w->sb_buf);
memset(sb, 0, sizeof(*sb));
sb->s_inodes_count = w->total_inodes;
sb->s_blocks_count_lo = w->total_blocks;
sb->s_r_blocks_count_lo = 0;
sb->s_free_blocks_count_lo = (uint32_t)free_blocks;
sb->s_free_inodes_count = free_inodes;
sb->s_first_data_block = 0; /* with 4K blocks, SB itself lives in block 0 */
sb->s_log_block_size = 2; /* 1024 << 2 = 4096 */
sb->s_log_cluster_size = 2;
sb->s_blocks_per_group = w->blocks_per_group;
sb->s_clusters_per_group = w->blocks_per_group;
sb->s_inodes_per_group = w->inodes_per_group;
sb->s_mtime = 0;
sb->s_wtime = (uint32_t)time(NULL);
sb->s_mnt_count = 0;
sb->s_max_mnt_count = (uint16_t)-1;
sb->s_magic = EXT4_SUPER_MAGIC;
sb->s_state = 1; /* clean */
sb->s_errors = 1; /* continue */
sb->s_minor_rev_level = 0;
sb->s_lastcheck = (uint32_t)time(NULL);
sb->s_checkinterval = 0;
sb->s_creator_os = 0; /* linux */
sb->s_rev_level = 1; /* dynamic */
sb->s_def_resuid = 0;
sb->s_def_resgid = 0;
sb->s_first_ino = EXT4_FIRST_INO;
sb->s_inode_size = INODE_SIZE;
sb->s_block_group_nr = 0;
sb->s_feature_compat = EXT4_FEATURE_COMPAT;
sb->s_feature_incompat = EXT4_FEATURE_INCOMPAT;
sb->s_feature_ro_compat = EXT4_FEATURE_RO_COMPAT;
memcpy(sb->s_uuid, w->uuid, 16);
strncpy(sb->s_volume_name, w->label, sizeof(sb->s_volume_name));
sb->s_desc_size = 32; /* not 64bit */
sb->s_default_mount_opts = 0;
sb->s_first_meta_bg = 0;
sb->s_mkfs_time = (uint32_t)time(NULL);
sb->s_min_extra_isize = 32;
sb->s_want_extra_isize = 32;
sb->s_flags = 0;
sb->s_log_groups_per_flex = 0;
sb->s_checksum_type = 0; /* no metadata_csum */
/* GDT. */
memset(w->gdt_buf, 0, (size_t)w->gdt_blocks * BLOCK_SIZE);
ext4_group_desc_t *gd = (ext4_group_desc_t *)w->gdt_buf;
for (uint32_t g = 0; g < w->num_groups; g++) {
uint32_t group_blocks = (g == w->num_groups - 1) ?
(w->total_blocks - g * w->blocks_per_group) : w->blocks_per_group;
uint32_t base = g * w->blocks_per_group +
(is_backup_group(g) ? (1 + w->gdt_blocks) : 0);
gd[g].bg_block_bitmap_lo = base;
gd[g].bg_inode_bitmap_lo = base + 1;
gd[g].bg_inode_table_lo = base + 2;
uint32_t used = group_first_data_block(w, g) + w->blocks_used_in_group[g];
gd[g].bg_free_blocks_count_lo = (uint16_t)(group_blocks - used);
/* Used inodes per group. For group 0, the reserved inodes 1..10 are
* always considered used (including root = inode 2, which falls
* within that range). For all groups, count allocated inodes whose
* number isn't already in the reserved range. */
uint32_t inode_lo = g * w->inodes_per_group + 1;
uint32_t used_inodes = 0;
uint32_t dirs = 0;
for (uint32_t i = inode_lo; i < inode_lo + w->inodes_per_group; i++) {
if (i > w->total_inodes) break;
if (g == 0 && i <= 10) continue; /* counted below */
if (!w->inodes[i]) continue;
used_inodes++;
if ((w->inodes[i]->body.i_mode & S_IFMT) == S_IFDIR) dirs++;
}
if (g == 0) {
used_inodes += 10;
/* root inode (2) is a directory, counted in reserved range */
dirs++;
}
gd[g].bg_free_inodes_count_lo = (uint16_t)(w->inodes_per_group - used_inodes);
gd[g].bg_used_dirs_count_lo = (uint16_t)dirs;
/* bg_itable_unused only meaningful with UNINIT_BG/GDT_CSUM features
* (which we don't enable). Setting it nonzero confuses e2fsck. */
gd[g].bg_itable_unused_lo = 0;
gd[g].bg_flags = 0;
}
/* Write to group 0 (block 0 of partition holds SB at offset 1024). */
uint8_t block0[BLOCK_SIZE];
memset(block0, 0, BLOCK_SIZE);
memcpy(block0 + 1024, sb, 1024);
if (part_write_block(w, 0, block0, 1) != 0) return -1;
/* GDT immediately follows in block 1+. */
if (part_write_block(w, 1, w->gdt_buf, w->gdt_blocks) != 0) return -1;
/* Backup SB + GDT in each backup group (1, 3, 5, 7, 9, ...). */
for (uint32_t g = 1; g < w->num_groups; g++) {
if (!is_backup_group(g)) continue;
sb->s_block_group_nr = (uint16_t)g;
memset(block0, 0, BLOCK_SIZE);
/* For backup groups the SB is at block 0 of group, no leading 1024B
* padding. */
memcpy(block0, sb, 1024);
uint32_t gb0 = g * w->blocks_per_group;
if (part_write_block(w, gb0, block0, 1) != 0) return -1;
if (part_write_block(w, gb0 + 1, w->gdt_buf, w->gdt_blocks) != 0) return -1;
}
sb->s_block_group_nr = 0;
uint32_t backups = 0;
for (uint32_t g = 1; g < w->num_groups; g++) if (is_backup_group(g)) backups++;
LOG_I("wrote superblock + GDT to group 0 and %u backup groups", backups);
return 0;
}
int ext4_writer_close(ext4_writer_t *w)
{
LOG_I("ext4_writer_close: finalizing %u inodes", w->n_used_inodes);
/* 1. Serialize all directory data + allocate their blocks + set inode extents. */
if (finalize_dirs(w) != 0) { LOG_E("finalize_dirs failed"); return -1; }
/* 2. Write all inode bodies. */
if (write_all_inodes(w) != 0) return -1;
/* 3. Write block + inode bitmaps for every group. */
for (uint32_t g = 0; g < w->num_groups; g++) {
if (write_block_bitmap(w, g) != 0) {
LOG_E("write_block_bitmap(g=%u) failed", g); return -1;
}
if (write_inode_bitmap(w, g) != 0) {
LOG_E("write_inode_bitmap(g=%u) failed", g); return -1;
}
}
LOG_I("wrote bitmaps for %u groups", w->num_groups);
/* 4. Write superblock + GDT (including backups). */
if (write_superblocks_and_gdt(w) != 0) return -1;
/* 5. Flush any pending coalesced writes before closing. */
if (part_flush(w) != 0) {
LOG_E("final part_flush failed");
return -1;
}
/* 6. Cleanup. */
VirtualFree(w->sb_buf, 0, MEM_RELEASE);
VirtualFree(w->gdt_buf, 0, MEM_RELEASE);
VirtualFree(w->coal_buf, 0, MEM_RELEASE);
free(w->next_free_block); free(w->blocks_used_in_group);
for (uint32_t i = 1; i <= w->total_inodes; i++) {
if (w->inodes[i]) {
free(w->inodes[i]->dir_blob);
free(w->inodes[i]);
}
}
free(w->inodes);
/* Free dir tree. */
/* (Iterative free skipped for brevity - leak on close, process exits anyway.) */
LOG_I("ext4_writer_close OK");
free(w);
return 0;
}