# ext4 Driver — Status & Future Tasks (2026-05-08)

Status snapshot after the WinFsp-adapter file-copy debug-and-fix session.
Companion to `IMPROVEMENT-PLAN.md` (general hardening) and
`ext4-full-write-support.md` (write-feature matrix).

## What triggered this session

Mounting an ext4 image on Windows via `ext4-win-driver` worked for
`mkdir` and tiny text files. Copying an 8 MiB or 64 MiB file failed with
**"Invalid MSDOS function"** in Explorer. That's `ERROR_INVALID_FUNCTION` /
`STATUS_INVALID_DEVICE_REQUEST` — the wildcard branch of the adapter's
errno→NTSTATUS map firing on an EIO returned from `apply_replace_file_content`'s
allocator after fragmentation buildup. Root cause: the WinFsp `write`
callback was faking positional pwrite by reading the entire file, splicing
the chunk in, and replacing the whole file — O(filesize²) per copy with
heavy bitmap fragmentation.

## Landed (this session)

### Streaming write primitive
- **`Filesystem::apply_pwrite(path, offset, data)`** — `src/fs.rs:2603`.
  Walks affected logical blocks; for each contiguous unmapped run,
  allocates one or more physical runs (halving the request on
  "no contiguous free run") and inserts each as its own extent.
  Existing mapped blocks are read-modify-written; freshly-allocated
  blocks get a zero-init buffer + splice (no stale-disk reads). Single
  atomic `commit_block_buffer`.
- **`fs_ext4_pwrite(fs, path, data, len, offset)`** — `src/capi.rs:1679`.
  C ABI surface. 1 GiB hard cap on `len`. Declared in
  `include/fs_ext4.h`.
- **WinFsp adapter rewired** — `ext4-win-driver/src/mount.rs:793`. The
  ~40-line merge-and-replace hack is gone; `write()` now dispatches
  directly to `fs_ext4_pwrite`. Cost per chunk dropped from
  O(filesize) to O(chunk).
- Tests: `tests/capi_pwrite.rs` — 7 cases including 1 MiB streamed in
  64 KiB chunks with remount-and-verify.

### Extent tree depth promotion + multi-run allocation
- `apply_pwrite` now falls back from `plan_insert_extent` to
  `plan_insert_extent_deep` on either `LEAF_FULL_NEEDS_PROMOTION` (inline
  4-extent root overflow) or `multi-level tree mutation not yet supported`
  (post-promotion descent). Tree-meta blocks are checksum-patched, eagerly
  written to disk so subsequent inserts in the same call can descend
  through them, and staged in `buf` for the atomic commit.
- New `FsBlockReader` adapter implementing `DeepReader` —
  `src/fs.rs:80`.
- Allocation halves on fragment failure and inserts each sub-run as a
  separate extent (auto-merge keeps sequential writes at one extent).
  True ENOSPC surfaces as `Error::NoSpaceLeftOnDevice`.
- Test: `many_disjoint_writes_promote_extent_tree_depth` —
  6 disjoint inserts force depth promotion + post-promotion descent.

### Rename overwrite (atomic)
- **`apply_rename(src, dst, replace_if_exists)`** — `src/fs.rs:3683`.
  When dst exists and `replace_if_exists=true`: hardlink-noop (same
  inode), type-compat (`IsADirectory`/`NotADirectory`), non-empty-dir
  check, then a single-buffer atomic overwrite that removes old dst
  entry, adds new dst entry pointing at src, removes src entry, fixes
  `..` / parent nlinks for cross-parent dir moves, and reaps the old
  dst inode (extents freed via `plan_truncate_shrink`/`extent::collect_all`,
  bitmap slot cleared) when its link count hits zero.
- **`fs_ext4_rename2(fs, src, dst, flags)`** — new C ABI with
  `FS_EXT4_RENAME_REPLACE = 0x01`. Existing `fs_ext4_rename` keeps its
  signature (calls `replace=false`). Declared in `include/fs_ext4.h`.
- WinFsp adapter `rename` callback now passes WinFsp's
  `replace_if_exists` through. Explorer's "Save As" / drag-onto-existing
  unblocked.
- Tests: `tests/capi_rename_overwrite.rs` — 8 cases including
  hardlinked dst (link count drops, blocks not freed) and remount.

### Long symlinks
- `apply_symlink` extended from 60-byte fast-only to PATH_MAX (4096)
  via the slow-symlink data-block path — `src/fs.rs:1984`. Boundary
  aligned with `fs_ext4_readlink` (slow when `target.len() >= 60`,
  matching Linux).
- FFI cap raised in `fs_ext4_symlink` to surface `ENAMETOOLONG`
  cleanly instead of `EINVAL`.
- Tests: `tests/capi_long_symlink.rs` — 8 cases including
  csum-validated remount with 1000-byte target.

### READONLY attribute round-trip
- WinFsp `set_basic_info` now maps `FILE_ATTRIBUTE_READONLY` ↔ POSIX
  write bits via `fs_ext4_chmod` — `ext4-win-driver/src/mount.rs:952`.
  Symmetric (clears `0o222` to make read-only, sets it to clear).
  Only chmods when the bit *changes*; plain mtime updates don't churn
  ctime. Skips when `file_attributes == 0` or `INVALID_FILE_ATTRIBUTES`
  (WinFsp "leave unchanged" sentinels).

### errno → NTSTATUS map
- `ext4-win-driver/src/mount.rs` — `errno_to_status` now covers
  EIO, EINVAL, ENOTSUP, ENAMETOOLONG, ENOMEM, EFBIG, ENOSYS in addition
  to the original 8 codes. Fixed latent macOS-vs-Linux mismatch
  (ENOTEMPTY 66 not 39 — the fs library returns macOS values).
- Every C-ABI failure now logs `errno=N status=0xXXXX: <last_error>` to
  stderr. Future failures surface the actual diagnosis instead of
  "Invalid MSDOS function".

### Already in tree (audit was wrong)
- **External-block xattr write path** — `src/xattr.rs` already has
  `plan_set_in_external_block` + `plan_remove_from_external_block` and
  3 passing tests in `tests/xattr_external_block.rs` covering
  allocation, multi-entry, and free-on-remove. The 2026-05-08 audit
  flagged this as missing; it's not.

## Test gate

`cargo test` from `rust-fs-ext4`: **666 tests across 102 suites,
0 failures.**

## Future tasks (deferred from this session)

### B3 — pwrite over pre-existing uninitialised extents
**What.** When `apply_pwrite` walks a logical block that's covered by
an uninitialised extent (created by `fallocate(FALLOC_FL_KEEP_SIZE)`),
treat it as already-allocated: write the data to the existing physical
block, then mark the extent (or the affected sub-region) initialised.

**Why deferred.** Needs extent-splitting infra: a write that partially
overlaps an uninit extent has to split it into up to three pieces
(`pre-uninit`, `written-init`, `post-uninit`) and re-emit the entries.
`extent_mut.rs` has insert + truncate-shrink; it doesn't yet have
"split-and-flip-flag-on-mid-range".

**Bites who.** Anything that does `posix_fallocate` then writes:
pre-allocated download tools, database journals, sparse VM disk
preparation. Doesn't bite the streaming-copy path because
WinFsp/Explorer don't fallocate before writing.

**Workaround today.** Skip fallocate; let pwrite allocate on demand.
Failure mode if hit: `Error::CorruptExtentTree("extent overlaps existing")`
→ `STATUS_IO_DEVICE_ERROR` with the actual message in stderr.

**Sketch when picking up.** Pre-walk the affected range with
`extent::lookup` (returns the full Extent including `uninitialized`)
to classify each block: hole, mapped+init, mapped+uninit. Mapped+init →
RMW. Mapped+uninit → write to existing phys + record the extent for
flag-flip in Phase 3. Hole → existing alloc-and-insert path. Phase 3
splits each touched uninit extent at write boundaries and re-encodes
both halves.

### A2 (extension) — Full NT security-descriptor mapping
**What.** WinFsp `set_security` currently returns Ok and does nothing.
Apps that explicitly set ACLs (Office, Visual Studio installers) lose
their changes silently. The READONLY round-trip via `set_basic_info`
(landed this session) is a partial workaround for the most common
case but doesn't cover ACE-level permissions.

**Why deferred.** NT SDs (owner SID, group SID, DACL with arbitrary
ACEs, SACL) don't map cleanly to POSIX mode bits. Faithful round-trip
needs an `xattr` slot to store the raw NT SD blob (e.g.
`system.nt_security`) and a SD-aware getter that prefers the xattr
over synthesised mode-bit-derived SDs. WinFsp examples do this; it's
~100-200 lines plus xattr name-prefix handling.

**Bites who.** Apps that gate functionality on file ACLs (rare on a
mounted ext4 volume in practice — Linux source trees don't use ACLs).
Mostly an "Explorer Properties → Security tab shows wrong info"
cosmetic gap.

### B5 — mkfs multi-group support
**What.** `mkfs.rs` currently only writes single-group ext4 images.
Volumes larger than `~blocks_per_group * block_size` (~128 MiB at 4 KiB
blocks / 32768 blocks per group) can't be freshly formatted.

**Why deferred.** Doesn't bite a *mounted* volume — the driver mounts
multi-group ext4 images fine; only the format path is single-group.
Users typically format on Linux and copy.

**Bites who.** Anyone doing the full Windows-only flow: format an
empty disk + immediately mount it. Workaround: format on Linux, then
mount.

### Other gaps from the 2026-05-08 audit (not blockers)

- **1 GiB hard cap** on a single `fs_ext4_write_file` / `fs_ext4_pwrite`
  call. Streaming callers chunk well below this anyway; not a real
  constraint.
- **4096-byte FFI path cap.** Matches Linux PATH_MAX. Windows long
  paths can exceed; rare in practice.
- **No explicit `fs_ext4_fsync` ABI.** Each mutating op flushes its
  own commit through the journal already. A dedicated fsync entry
  would let database-style callers force durability between ops.
- **Journal replay skipped on RO mounts.** `features.rs:74` flag
  parsed but no replay; metadata stays stale until the next RW mount.
- **BIGALLOC / INLINE_DATA** — partial; not a hot path for typical
  Windows workloads.

### Cross-cutting

- **Adapter test harness on Windows.** The fs library has 666 tests
  but the WinFsp adapter has none — every adapter change is verified
  by the user running the driver in a Windows VM. A
  `winfsp-fs-skeleton` extraction (existing project memory:
  `winfsp-fs-skeleton extraction plan`) would unblock adapter-side
  unit tests against a mock WinFsp host.

## Test gate (re-stated for the next session)

When picking up any deferred item:

```
cargo test         # full suite: 666 tests across 102 suites must stay green
cargo test --test capi_pwrite               # streaming writes
cargo test --test capi_rename_overwrite     # rename
cargo test --test capi_long_symlink         # symlinks
cargo test --test xattr_external_block      # xattr overflow
```

Adapter (`ext4-win-driver`) — Windows target only; verified by manual
copy/save/rename/properties-toggle in Explorer against a mounted image.
