📦 ARCHIVE CONTENTS (NTFS_EFI.7z — 122.8K)
No password. Plain 7-Zip archive — extract and copy the files onto a FAT32 ESP or stick.
NTFS_EFI-latest/
│
├── ntfs.efi ⭐ NTFS read+write UEFI driver (EFI_DRIVER_BINDING_PROTOCOL) [91.5K]
├── EC.efi ⭐ EFI Commander — dual-panel pre-boot file manager [160.0K]
├── ntfs_probe.efi 🧪 Self-loading test harness + tree-copy benchmark [48.5K]
├── EC.ini ⚙ Sample configuration for EC.efi
└── README.txt 📄 Deployment guide
ntfs.efi and EC.efi must sit in the same directory — that is how EC.efi finds the driver
(<AppDir>\ntfs.efi, overridable with NtfsDriverPath in EC.ini).
ntfs_probe.efi is stricter on purpose: it loads \ntfs.efi from the root of the volume it was booted from.
🔗 DOWNLOAD
| File | Size | Description |
|---|---|---|
| NTFS_EFI.7z | 122.8K | Driver + file manager + test harness (no password) |
| Source code | — | Full tree: EDK2 header subset, prebuilt libs, PowerShell test scripts |
🚀 QUICK START
UEFI Shell
Shell> load fs0:\ntfs.efi
Shell> map -r
Shell> fs1: # the NTFS volume, now mounted by the driver
fs1:\> cp fs0:\patched.sys fs1:\Windows\System32\drivers\
Interactive rescue stick — no Shell needed
Copy both binaries into \EFI\Boot\ on a FAT32 stick and rename the manager to the fallback path:
\EFI\Boot\BOOTX64.EFI <- copy of EC.efi
\EFI\Boot\ntfs.efi <- same directory, found automatically
\EFI\Boot\EC.ini <- optional
Boot the stick: a dual-panel file manager with NTFS write access, no operating system involved.
EC.efi loads and connects ntfs.efi itself if the firmware has not.
Quit with
F10before pulling the stick. That triggers the preallocation trim, the$Volume
dirty-flag clear andFlushBlocks. Cutting power at the panel view leaves the volume marked dirty.
🔒 WHAT CHANGED IN THIS REFRESH — 08.2026
The Master File Table and its own bitmap now grow together. Allocating a record used to stop the
moment the last existing bit in $MFT:$BITMAP was taken. On any real Windows volume that attribute
is non-resident — a 136 000-record table needs about 17 KB of bits — and the non-resident branch
simply returned EFI_VOLUME_FULL, on a volume with tens of gigabytes free. It now grows in both
forms: resident in place, non-resident inside the clusters it already owns, taking one more run only
when those are used up, with the newly exposed bytes zeroed so no stale bit hands out a record that
is in use. The growth itself takes 256 clusters at a time instead of 16, which is 1024 records for a
single mapping pair in MFT record 0, and the cluster allocator now keeps the longest free run it saw
rather than the last one — asking for a big chunk used to walk past a long run and come back with
whatever few clusters sat at the end of the range.
Measured on a live Windows 11 install with its MFT filled down to 40 free records: 6344 more files
created, the table grown from 136 960 to 143 872 records and its bitmap from 17 120 to 17 984
bytes, chkdsk "found no problems". On a fresh volume, the same test placed 30 000 of 30 000
files, the table growing to 30 976 records in 31 runs. What stops the growth now is the
mapping-pair budget in MFT record 0.
An index block that loses its last key leaves the tree. Deleting entries one by one empties
leaves, and the emptied block used to stay marked in $BITMAP:$I30 while still hanging off an entry
in its parent. Windows treats that as damage: chkdsk reports free space marked as allocated in the
bitmap for index $I30 plus errors on the parent entries, and repairs the directory. Unhooking is
the inverse of a split — the parent's END entry gives up its child pointer and returns to its
16-byte form, while a separator carrying a key leaves the node whole and goes back into the tree
through the ordinary insert, keeping the $FILE_NAME copy Windows checks against the file's own
record. The test battery now leaves the volume chkdsk-clean, which it never did before.
An emptied directory hands its index space back. Collapsing a directory to a resident
$INDEX_ROOT cleared every allocation bit but left $INDEX_ALLOCATION owning all its clusters, with
a mapping pair for every run of them, inside the same 1 KB record that also has to hold the index
root — so emptying a big directory made the next fill harder, not easier. The attribute is now cut
back to one block, the tail clusters return to $Bitmap, the mapping pairs are re-encoded for what
is left and $BITMAP:$I30 goes back to its eight-byte minimum. One block stays mapped on purpose:
that is what lets the next create refill the directory without allocating anything. Measured on a
directory filled with 2000 long-named files and then emptied through the driver: $INDEX_ALLOCATION
down to a single 4 KB block in one run, 88 bytes of attribute, $BITMAP:$I30 from 56 bytes to
40, chkdsk clean, and the same directory refills to 2000 of 2000.
CompactOS files compressed with LZX are readable now. WOF has two codecs. XPRESS was already
handled; a file packed with compact /c /exe:LZX came back EFI_UNSUPPORTED — and those are the
ones worth reading, since a real shell32.dll goes 7.6 MB to 3.3 MB where XPRESS manages far
less. The stream is LZX as WIM uses it, and three things separate that from the LZX a decoder
written off the CAB description produces: there is no E8-translation header, the block size is a
flag rather than a number — one bit meaning "the whole 32 KiB chunk" — and x86 call translation is
undone on the way out unconditionally, with the fixed 12000000 span. Each chunk is its own stream,
so the window, the three repeated offsets and every code length start over, and a match can never
reach behind the chunk it belongs to.
Chunk reads had to change with it. A chunk begins wherever the previous one ended, so its offset
inside the compressed stream is arbitrary, and Hyper-V's DiskIo refuses such a span outright
instead of returning a short read — which looked exactly like a corrupt chunk. Reads are widened to
whole sectors now. XPRESS never hit this because its chunks are small enough that a test file fits
in one.
Verified against four real Windows binaries packed with LZX, from 72 KB to 7.6 MB and up to 243
chunks: every one reads back byte-exact through the driver, checked by full-file checksum
against the same file read by Windows — xcopy.exe 73 728, notepad.exe 360 448, cmd.exe
344 064, shell32.dll 7 947 416 bytes.
Two smaller corrections. The index trim above releases its clusters only once the record that
stopped pointing at them is on disk; releasing first would have left the old mapping addressing
space the volume already considers free. And the $MFT record bitmap is scanned the way the
cluster bitmap already was, stepping over all-ones words instead of walking 17 KB of set bits one
at a time on every create.
All three binaries carry a version resource. A UEFI image is an ordinary PE/COFF file, so it can
hold a .rsrc section like any .sys or .dll — firmware ignores it, Windows reads it, which is
why Explorer shows a Details tab for bootmgfw.efi and now shows one for these three: author,
description, copyright, contact and version. Verified afterwards that the firmware still loads both
an application and a driver built this way.
EFI Commander grew the tools a file manager is actually used for. The panels, the viewer and the
editor were already there; what a rescue session keeps asking for is proof that a copy arrived
intact and a way to find something without knowing where it is. So: SHA-256 and CRC32 of a file,
computed in 64 KB reads with no crypto library linked in; VerifyAfterCopy in EC.ini, which
re-reads both sides after every copy and fails the operation on a mismatch; a recursive tree compare
that hashes same-sized files and can then update one side while keeping entries only the destination
has; = to light up, on both panels at once, exactly what differs by name, size or modification
time; Ctrl+Q Quick View showing the first 32 KB as text, as hex when the content looks binary, or
as a directory summary; Ctrl+F2 for the DOS attribute bits and the modification time; F7 and F3
to find bytes inside the viewer; a read-only view of BootOrder, BootNext and every Boot####
description; and running a selected image with an argument string passed as LoadOptions, the way
the Shell would. Every option in F9 → Settings is written back to EC.ini the moment it changes.
The long operations stopped looking like a hang. Hashing a tree takes minutes, and the compare
used to draw one box before it started and nothing after, with no way out. It now reports the path
it has reached and stops on Esc, redrawing every sixteenth entry so the drawing cannot outweigh
the comparison, and an interrupted update still flushes what it wrote. A copy is also sized up
before it starts — the source measured by listings alone, the destination volume asked what it has
left — and a shortfall asks rather than refuses, because cluster slack is uncounted and an overwrite
gives its bytes back.
Alt+F7 looks inside files, not only at their names. The same dialog takes text the file must
contain, matched case-insensitively as ASCII by the viewer's own engine, with files read in 64 KB
chunks that carry the tail of the previous chunk forward so a match across a boundary is still
found. The mask is what keeps it affordable, so Esc is polled per file rather than per directory.
EC has a scripted self-test. build.ps1 -SelfTest defines EC_SELFTEST, which is the only
thing that compiles SelfTest.c into anything; the released binary carries neither the code nor the
check for it. tests\hyperv-ectest.ps1 builds it, lays a fixture on a scratch NTFS volume, boots it
under Hyper-V and reads the report back with nothing to click: 59 checks, none failing. The
content search is driven against a 70 000-byte file with the needle at offset 65 532, so a chunked
scan that forgets its carry fails the test instead of passing quietly.
Earlier in this line
- A directory emptied from Windows could not be written to at all. Windows hands the index
clusters back and leaves$INDEX_ALLOCATION:$I30present withHighestVCN = -1and no runs — a
valid empty directory that the driver called corrupt and refused every create in, 417 of 417 on a
real copy target. It now allocates the block for VCN 0 instead. - Mapping pairs stopped accumulating. One run per index block, never reused, spends a budget that
lives in the directory's own 1 KB record: a directory filled and emptied repeatedly was measured
holding 106 single-cluster runs — 512 of its 1024 bytes — with exactly one block in use. Blocks the
directory already owns are handed out first and fresh clusters come 8 blocks to a run. Three full
fill/empty passes of\Windows\System32\drivers: 548 of 548 every pass, zero refusals, the index
attribute settling at 4 runs in 104 bytes. - Silent data loss on large real directories. An internal node keeps its rightmost child VCN
inside itsENDentry; one split path read the pointer atHeader + TotalSizeOfEntries, the byte
after it, and stamped neighbouring slack into the new node as a child pointer. Files were created
and written,Create()returned success, and Windows saw none of them.\Windows\System32\CatRoot
went from 964 created with 0 visible to 1854 of 1854 created and visible, 249 index blocks
described in 216 bytes. - One index, two MFT records. Insert, delete and collapse each edit the record their own
attributes live in, and every record is written back under the MFT index the resolver reports,
never the record's ownMFTRecordNumberfield. - Directory index sorting. The resident-root insert path collated with a hand-rolled
a-z
fold while every other path used the volume's$UpCasetable. Beyond ASCII the two disagree, and
the result violated NTFS collation —chkdsk:Index $I30 in file <n> is incorrectly sorted. That
path uses$UpCasenow. - Refusal codes are a build switch. Every limit in the write path names its own status through
NTFS_REFUSE();build.ps1 -Diagreturns it, and the production binary is byte-for-byte identical
with the mechanism in place as without it. - Hardened parser. Every walk over raw disk bytes is bounded once, centrally: USA fixup, volume
label, attribute walk, mapping pairs, index headers, recursive descent capped at 32 levels. Three
of those could write past their buffer. SetInfogrows a still-resident file. The promotion composes the head in memory and writes it
once, cluster-aligned, instead of a second unaligned write that Hyper-V's DiskIo rejected.- EFI Commander — the copy dialog stays inside its frame. The path field was measured from the
box width while the text starts after the inset and theFrom:/To:label, so a glyph could land
13 px past the border, outside the per-frame erase, and hang there over every later file.
✅ WHAT'S IN THIS BUILD — 08.2026
NTFS read + write from UEFI firmware
Driver (ntfs.efi)
EFI_DRIVER_BINDING_PROTOCOL+EFI_SIMPLE_FILE_SYSTEM_PROTOCOL— every existing UEFI application,
shell command and bootloader writes to NTFS with no code change- Create, write (in place / append / past EOF), delete, rename, cross-directory move,
SetInfo
timestamps + DOS attributes, shrink, preallocation trim onClose() - Full B+tree directory engine: leaf and internal splits with separator promotion, root push-down,
rebalance on separator delete, emptied blocks taken back out of the tree, and collapse into a
resident$INDEX_ROOTthat returns the directory's unused index clusters to$Bitmap $MFTgrowth on demand in 256-cluster chunks,$MFT:$BITMAPgrown with it in either form,
$MFTMirrlock-step mirroring of records 0-3$UpCase-based collation (the volume's own 65536-entry table),$ATTRIBUTE_LISTfollowed on read,
LZNT1 decompression, WOF/CompactOS reads in all four algorithms (XPRESS4K/8K/16K and LZX),
$REPARSE_POINTsymlink resolution- Ordered writes + explicit rollback: a crash leaks space, it never leaves a dangling reference
Verified
- ~3.9 MB real Windows 11
System32subset — SHA256 byte-exact per file,chkdsk /fCLEAN, NOT dirty - 1500 files in one directory — forces
INDXleaf splits and separator promotion —chkdskCLEAN \Windows\System32\CatRooton a live Windows 11 volume — 1854 of 1854 long-named files into
one directory, all visible to Windows, 0 refusals, 0 orphans,chkdsk"found no problems"- 7 GB mixed data, FAT source to NTFS target on Hyper-V Gen 2 — ~4 minutes,
bad=0,chkdskCLEAN - MFT exhaustion on a live Windows 11 volume — 6344 files created past the last free record, table
grown to 143 872 records,chkdsk"found no problems"; 30 000 of 30 000 on a fresh volume - Fill and drain — 2000 long-named files created and then deleted in one directory, leaving
$INDEX_ALLOCATIONat a single 4 KB block in one run,chkdsk"found no problems"
EFI Commander (EC.efi)
- Norton Commander / Far Manager layout rendered directly into the GOP framebuffer, embedded 8x16 font
- Recursive copy / move / delete across FAT32 and NTFS behind one VFS layer
- Text viewer with hex mode, text editor, sort by name/extension/size/date, filter masks, tagging,
per-panel path history,Alt+F10directory hotlist,EC.iniconfiguration - Loads
ntfs.efiitself and scopes aDisconnectControlleron exit, so the volume unmounts clean
Known boundaries (deliberate, documented in the project README)
- No DOS 8.3 alias; deletes only at
LinkCount == 1 - The write path never creates
$ATTRIBUTE_LISTextension records itself (it reads and writes indexes
that already live in two records). A directory therefore accepts new entries only while its
$INDEX_ALLOCATIONmapping pairs still fit in its own MFT record. Blocks are allocated 8 to a run
and unused owned blocks are reused, so that budget goes a long way — 1854 long-named entries need
216 of ~1024 bytes — but it is finite; past it inserts are refused withEFI_UNSUPPORTEDand
everything already there stays intact and visible $MFTgrowth ends on the same kind of budget: every chunk that lands away from the previous extent
costs a mapping pair in MFT record 0. On a fragmented volume with no free record left, growth
continued for 6344 files and stopped at 143 872 records with 156 runs- No
$LogFilejournal — integrity rests on ordered writes, rollback and the$Volumedirty flag - No compression on write; 2048 extents per attribute; little-endian x64 only
🔬 VERIFICATION DISCIPLINE
Always attach a result image with Mount-VHD -ReadOnly before checking it:
Mount-VHD -Path .\test.vhdx -ReadOnly -Passthru | Get-Disk | Get-Partition
chkdsk X: /f
fsutil dirty query X:Given write access, Windows silently repairs a volume on first access — a chkdsk run afterwards then
reports a clean volume that the driver did not actually leave clean.
🔨 BUILD FROM SOURCE
No EDK2 tree, no Python, no BaseTools — plain MSVC .vcxproj files linking the prebuilt EDK2 static
libraries shipped under lib/.
git clone https://github.com/wesmar/NTFS_EFI.git
cd NTFS_EFI
.\build.ps1Requires Visual Studio 2022 or 2026 with the Desktop development with C++ workload.
📞 CONTACT
- GitHub: https://github.com/wesmar/NTFS_EFI
- Project page: https://kvc.pl/repositories/ntfs_efi
- Email: marek@wesolowski.eu.org
Release Date: 08.2026
© WESMAR 2026 — MIT licence