Files
nt-lite/code/drivers/fs/ext4/ext4_driver.c
T
maksw 73abd6f90d isolate POSIX builds from the host UCRT, wire lwext4 and ntfs-3g as ext4.sys and ntfs.sys, split the FAT driver into directory/file/async/driver/mount units, add posixsrv and dwm plus input/console/desktop syscall clients and a pile of new smoke targets, rename serial.sys to cereal.sys, name HDA/USB/lwIP/AHCI workers, harden ATA probing/reset/removal around actual hardware deadlines, absent-channel detection, diagnostics and explicit worker lifetime, redo ACPI S5 shutdown around GAS/MMIO and HW-reduced firmware, scan SSDTs for _S5 and deal with Intel SMI sleep trapping, make serial and SMP timeouts use real monotonic deadlines, add thread names and fix terminal thread teardown, scheduler and APC prepare/commit races, make advisory file locks queryable and preserve split ranges and add mutable file append state, add shared-memory pipes with kernel-assisted wait/wake, introduce capability-scoped input and display managers, split surface map rights into read/write, add explicit unmap and replace present with commit/generation/damage tracking while keeping physical presentation under the display manager, decouple native exception classes from Win32 exception numbers and translate them in kernel32, add opaque process startup data, bump the resource protocol to v3 with namespace directories, symlinks and enumeration and service control to v2 with generic READY handle exports, add the new POSIX errno/status surface and rebuild fds around posixsrv-owned shared open descriptions, descriptor-local flags, exact inheritance snapshots, dup/fcntl/record locks and shared-ring pipes, add waitpid, POSIX credentials/groups/sessions/passwd/group lookups and move sockets onto stable open descriptions, port the shells and apps to the new console/input services and surface commit API, teach init to load services.ini and start/supervise RSSRV, DWM and generic services under restricted capabilities while publishing their endpoints through RSSRV, split RSSRV into namespace, volume, display, session, input, console and resource components and make /SystemRoot a real symlink to the independently published system volume, shift the syscall table again, expand lock/resource/surface/POSIX/service/thread smoke coverage, make docs scream ASK QUESTIONS and DISCUSS at LLMs, replace MIT No Attribution with WTFPL+.
2026-09-16 10:33:20 +02:00

1180 lines
38 KiB
C

/*
* NTLite ext2/ext3/ext4 read/write filesystem driver backed by lwext4.
*
* Keep NTLite policy and I/O glue here; third_party/lwext4 stays upstream.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include <limits.h>
#include <ntlite/ddi/io_driver.h>
#include <ntlite/ddk/ddk.h>
#include <ntlite/ddk/file.h>
#include <ntlite/ddk/memory.h>
#include <ntlite/ddk/debug.h>
#include <ntlite/ddk/list.h>
#include <ntlite/ddk/mutex.h>
#include <ntlite/ddk/time.h>
#include <ext4.h>
#include <ext4_blockdev.h>
#include <ext4_errno.h>
#include <ext4_oflags.h>
#define NTL_EXT4_SECTOR_SIZE 512u
#define NTL_EXT4_ZERO_CHUNK 4096u
#ifndef INT64_MAX
#define INT64_MAX 9223372036854775807LL
#endif
typedef struct NTL_EXT4_VOLUME {
uint8_t DiskIndex;
uint64_t PartitionLba;
uint64_t PartitionSectors;
struct ext4_blockdev BlockDev;
struct ext4_blockdev_iface BlockIface;
char DeviceName[24];
char MountName[24];
IO_VOLUME* Volume;
LIST_ENTRY MountLink;
bool Mounted;
bool JournalStarted;
bool Dead;
} NTL_EXT4_VOLUME;
typedef struct NTL_EXT4_FILE {
NTL_EXT4_VOLUME* Volume;
ext4_file File;
char* Path;
} NTL_EXT4_FILE;
typedef struct NTL_EXT4_DIR {
NTL_EXT4_VOLUME* Volume;
ext4_dir Dir;
char* Path;
} NTL_EXT4_DIR;
static LIST_ENTRY gExt4Mounts;
static uint32_t gExt4MountedCount;
static uint32_t gExt4Serial;
static MUTEX gExt4MountMutex;
static MUTEX gExt4FsMutex;
static SPINLOCK gExt4MutexInitLock = SPINLOCK_INIT;
static int gExt4MutexesReady;
/* lwext4 also defines LIST_INIT for its BSD queue heads. Avoid that macro
* here because NTLite LIST_ENTRY uses the Fwd/Bwd intrusive-list layout. */
static void Ext4ListInit(LIST_ENTRY* head) {
head->Fwd = head;
head->Bwd = head;
}
static void Ext4InitStaticMutex(MUTEX* mutex) {
memset(mutex, 0, sizeof(*mutex));
ObInitializeObjectHeader(&mutex->Header, ObjectTypeMutex);
mutex->Lock = (SPINLOCK)SPINLOCK_INIT;
mutex->Owner = (THREAD*)0;
Ext4ListInit(&mutex->WaitList);
Ext4ListInit(&mutex->OwnerLink);
mutex->DonationClass = KeSchedulingNormal;
mutex->DonationPriority = PriorityLow;
mutex->HasDonation = 0;
mutex->Abandoned = 0;
}
static void Ext4EnsureMutexes(void) {
if (gExt4MutexesReady)
return;
uintptr_t flags = KeAcquireSpinlock(&gExt4MutexInitLock);
if (!gExt4MutexesReady) {
Ext4InitStaticMutex(&gExt4MountMutex);
Ext4InitStaticMutex(&gExt4FsMutex);
gExt4MutexesReady = 1;
}
KeReleaseSpinlock(&gExt4MutexInitLock, flags);
}
static KSTATUS Ext4AcquireFs(void) {
Ext4EnsureMutexes();
if (!KeCanWait())
return NTLSTATUS_INVALID_IRQL;
KSTATUS status = KeAcquireMutex(&gExt4FsMutex, KE_WAIT_INFINITE);
return KE_MUTEX_ACQUIRE_SUCCEEDED(status) ? NTLSTATUS_SUCCESS : status;
}
static void Ext4ReleaseFs(void) {
KeReleaseMutex(&gExt4FsMutex);
}
static KSTATUS Ext4MapError(int error) {
switch (error) {
case EOK: return NTLSTATUS_SUCCESS;
case ENOENT:
case ENXIO:
case ENODEV: return NTLSTATUS_NOT_FOUND;
case EEXIST: return NTLSTATUS_ALREADY_EXISTS;
case ENOMEM: return NTLSTATUS_NO_MEMORY;
case ENOSPC:
case EFBIG:
case EMLINK: return NTLSTATUS_LIMIT_REACHED;
case EACCES:
case EPERM:
case EROFS: return NTLSTATUS_ACCESS_DENIED;
case ENOTSUP: return NTLSTATUS_NOT_SUPPORTED;
case EIO: return NTLSTATUS_DEVICE_ERROR;
case EINVAL:
case EFAULT:
case E2BIG:
case EISDIR:
case ENOTDIR:
case ENOTEMPTY: return NTLSTATUS_INVALID_PARAMETER;
default: return NTLSTATUS_FAILURE;
}
}
static NTL_EXT4_VOLUME* Ext4Volume(IO_VOLUME_PROVIDER* provider) {
return provider ? (NTL_EXT4_VOLUME*)provider->Context : (NTL_EXT4_VOLUME*)0;
}
static bool Ext4PathValid(SYS_STRING_VIEW path, bool allowRoot) {
if (!SysSvIsValid(path))
return false;
if (!allowRoot && path.Length == 0)
return false;
return true;
}
/* Converts the VFS-relative path into lwext4's private mount namespace. */
static char* Ext4BuildPath(NTL_EXT4_VOLUME* volume, SYS_STRING_VIEW relative) {
if (!volume || !SysSvIsValid(relative))
return (char*)0;
size_t prefix = strlen(volume->MountName);
if (relative.Length > SIZE_MAX - prefix - 1)
return (char*)0;
size_t bytes = prefix + relative.Length + 1;
char* path = (char*)MmAllocatePool(bytes);
if (!path)
return (char*)0;
memcpy(path, volume->MountName, prefix);
if (relative.Length)
memcpy(path + prefix, relative.Data, relative.Length);
path[prefix + relative.Length] = '\0';
return path;
}
static uint32_t Ext4NowSeconds(void) {
uint64_t now = KeQueryUnixTimeSeconds();
return now > UINT32_MAX ? UINT32_MAX : (uint32_t)now;
}
#define EXT4_TOUCH_ATIME 0x1u
#define EXT4_TOUCH_MTIME 0x2u
#define EXT4_TOUCH_CTIME 0x4u
static void Ext4TouchLocked(const char* path, uint32_t mask) {
if (!path) return;
uint32_t now = Ext4NowSeconds();
if (mask & EXT4_TOUCH_ATIME) (void)ext4_atime_set(path, now);
if (mask & EXT4_TOUCH_MTIME) (void)ext4_mtime_set(path, now);
if (mask & EXT4_TOUCH_CTIME) (void)ext4_ctime_set(path, now);
}
static void Ext4SetEpochTime(SYS_SYSTEM_TIME* out, uint32_t seconds) {
if (!out)
return;
memset(out, 0, sizeof(*out));
out->Flags = SYS_TIME_FLAG_VALID;
out->TimeZoneMinutes = SYS_TIME_UNSPECIFIED_TZ;
out->EpochSeconds = seconds;
uint64_t days = (uint64_t)seconds / 86400ull;
uint32_t rem = seconds % 86400u;
out->Hour = (uint8_t)(rem / 3600u);
out->Minute = (uint8_t)((rem % 3600u) / 60u);
out->Second = (uint8_t)(rem % 60u);
out->DayOfWeek = (uint8_t)((days + 4u) % 7u);
int64_t z = (int64_t)days + 719468ll;
int64_t era = (z >= 0 ? z : z - 146096ll) / 146097ll;
uint32_t doe = (uint32_t)(z - era * 146097ll);
uint32_t yoe = (doe - doe / 1460u + doe / 36524u - doe / 146096u) / 365u;
int64_t year = (int64_t)yoe + era * 400ll;
uint32_t doy = doe - (365u * yoe + yoe / 4u - yoe / 100u);
uint32_t mp = (5u * doy + 2u) / 153u;
uint32_t day = doy - (153u * mp + 2u) / 5u + 1u;
uint32_t month = mp + (mp < 10u ? 3u : (uint32_t)-9);
year += (month <= 2u);
out->Year = (uint16_t)year;
out->Month = (uint8_t)month;
out->Day = (uint8_t)day;
}
static bool Ext4IsDotEntry(const ext4_direntry* entry) {
if (!entry)
return false;
if (entry->name_length == 1 && entry->name[0] == '.')
return true;
return entry->name_length == 2 && entry->name[0] == '.' && entry->name[1] == '.';
}
static char* Ext4BuildDirEntryPath(const NTL_EXT4_DIR* dir, const ext4_direntry* entry) {
if (!dir || !dir->Path || !entry)
return (char*)0;
size_t parentLength = strlen(dir->Path);
size_t nameLength = entry->name_length;
bool needSlash = parentLength != 0 && dir->Path[parentLength - 1] != '/';
size_t separator = needSlash ? 1u : 0u;
if (nameLength > SIZE_MAX - parentLength - separator - 1u)
return (char*)0;
size_t bytes = parentLength + separator + nameLength + 1u;
char* path = (char*)MmAllocatePool(bytes);
if (!path)
return (char*)0;
memcpy(path, dir->Path, parentLength);
size_t offset = parentLength;
if (needSlash)
path[offset++] = '/';
if (nameLength)
memcpy(path + offset, entry->name, nameLength);
path[offset + nameLength] = '\0';
return path;
}
/* lwext4 v0.0.3 only shrinks in ext4_ftruncate and refuses seeks beyond EOF. */
static int Ext4GrowFileLocked(ext4_file* file, uint64_t target) {
static const uint8_t zeros[NTL_EXT4_ZERO_CHUNK] = {0};
uint64_t current = ext4_fsize(file);
if (target <= current)
return EOK;
if (current > (uint64_t)INT64_MAX || target > (uint64_t)INT64_MAX)
return EFBIG;
int r = ext4_fseek(file, (int64_t)current, SEEK_SET);
if (r != EOK)
return r;
while (current < target) {
uint64_t left = target - current;
size_t chunk = left < sizeof(zeros) ? (size_t)left : sizeof(zeros);
size_t written = 0;
r = ext4_fwrite(file, zeros, chunk, &written);
if (r != EOK)
return r;
if (written != chunk)
return EIO;
current += written;
}
return EOK;
}
/* ---- lwext4 physical block device ---- */
static NTL_EXT4_VOLUME* Ext4BlockVolume(struct ext4_blockdev* bdev) {
if (!bdev || !bdev->bdif)
return (NTL_EXT4_VOLUME*)0;
return (NTL_EXT4_VOLUME*)bdev->bdif->p_user;
}
static int Ext4BlockOpen(struct ext4_blockdev* bdev) {
return Ext4BlockVolume(bdev) ? EOK : ENODEV;
}
static int Ext4BlockRead(struct ext4_blockdev* bdev, void* buffer,
uint64_t blockId, uint32_t blockCount) {
NTL_EXT4_VOLUME* volume = Ext4BlockVolume(bdev);
if (!volume || (!buffer && blockCount))
return EINVAL;
if (blockId > volume->PartitionSectors ||
(uint64_t)blockCount > volume->PartitionSectors - blockId)
return EIO;
if (blockCount == 0)
return EOK;
KSTATUS status = IoReadDiskSectors(volume->DiskIndex,
volume->PartitionLba + blockId,
blockCount,
buffer);
return KSUCCESS(status) ? EOK : EIO;
}
static int Ext4BlockWrite(struct ext4_blockdev* bdev, const void* buffer,
uint64_t blockId, uint32_t blockCount) {
NTL_EXT4_VOLUME* volume = Ext4BlockVolume(bdev);
if (!volume || (!buffer && blockCount))
return EINVAL;
if (blockId > volume->PartitionSectors ||
(uint64_t)blockCount > volume->PartitionSectors - blockId)
return EIO;
if (blockCount == 0)
return EOK;
KSTATUS status = IoWriteDiskSectors(volume->DiskIndex,
volume->PartitionLba + blockId,
blockCount,
buffer);
return KSUCCESS(status) ? EOK : EIO;
}
static int Ext4BlockClose(struct ext4_blockdev* bdev) {
NTL_EXT4_VOLUME* volume = Ext4BlockVolume(bdev);
if (!volume)
return ENODEV;
return KSUCCESS(IoFlushDisk(volume->DiskIndex)) ? EOK : EIO;
}
static void Ext4InitBlockDevice(NTL_EXT4_VOLUME* volume) {
memset(&volume->BlockIface, 0, sizeof(volume->BlockIface));
memset(&volume->BlockDev, 0, sizeof(volume->BlockDev));
volume->BlockIface.open = Ext4BlockOpen;
volume->BlockIface.bread = Ext4BlockRead;
volume->BlockIface.bwrite = Ext4BlockWrite;
volume->BlockIface.close = Ext4BlockClose;
volume->BlockIface.lock = (int (*)(struct ext4_blockdev*))0;
volume->BlockIface.unlock = (int (*)(struct ext4_blockdev*))0;
volume->BlockIface.ph_bsize = NTL_EXT4_SECTOR_SIZE;
volume->BlockIface.ph_bcnt = volume->PartitionSectors;
volume->BlockIface.p_user = volume;
volume->BlockDev.bdif = &volume->BlockIface;
volume->BlockDev.part_offset = 0;
volume->BlockDev.part_size = volume->PartitionSectors * NTL_EXT4_SECTOR_SIZE;
}
/* ---- IO_VOLUME_PROVIDER operations ---- */
static KSTATUS Ext4Open(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW path, void** fileCtx) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !fileCtx || !Ext4PathValid(path, false))
return NTLSTATUS_INVALID_PARAMETER;
char* fullPath = Ext4BuildPath(volume, path);
if (!fullPath)
return NTLSTATUS_NO_MEMORY;
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)MmAllocatePool(sizeof(*file));
if (!file) {
MmFreePool(fullPath);
return NTLSTATUS_NO_MEMORY;
}
memset(file, 0, sizeof(*file));
file->Volume = volume;
KSTATUS status = Ext4AcquireFs();
if (KSUCCESS(status)) {
int r = ext4_fopen2(&file->File, fullPath, O_RDWR);
status = Ext4MapError(r);
Ext4ReleaseFs();
}
if (!KSUCCESS(status)) {
MmFreePool(fullPath);
MmFreePool(file);
return status;
}
file->Path = fullPath;
*fileCtx = file;
return NTLSTATUS_SUCCESS;
}
static KSTATUS Ext4Read(IO_VOLUME_PROVIDER* provider, void* fileCtx, uint64_t offset,
void* buffer, size_t size, size_t* bytesRead) {
(void)provider;
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)fileCtx;
if (!file || (!buffer && size) || !bytesRead)
return NTLSTATUS_INVALID_PARAMETER;
*bytesRead = 0;
if (offset > (uint64_t)INT64_MAX)
return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = Ext4AcquireFs();
if (!KSUCCESS(status))
return status;
uint64_t fileSize = ext4_fsize(&file->File);
if (offset >= fileSize || size == 0) {
Ext4ReleaseFs();
return NTLSTATUS_SUCCESS;
}
uint64_t available = fileSize - offset;
size_t toRead = available < (uint64_t)size ? (size_t)available : size;
int r = ext4_fseek(&file->File, (int64_t)offset, SEEK_SET);
if (r == EOK)
r = ext4_fread(&file->File, buffer, toRead, bytesRead);
if (r == EOK && *bytesRead)
Ext4TouchLocked(file->Path, EXT4_TOUCH_ATIME);
status = Ext4MapError(r);
Ext4ReleaseFs();
return status;
}
static KSTATUS Ext4Write(IO_VOLUME_PROVIDER* provider, void* fileCtx, uint64_t offset,
const void* buffer, size_t size, size_t* bytesWritten) {
(void)provider;
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)fileCtx;
if (!file || (!buffer && size) || !bytesWritten)
return NTLSTATUS_INVALID_PARAMETER;
*bytesWritten = 0;
if (offset > (uint64_t)INT64_MAX || (uint64_t)size > (uint64_t)INT64_MAX - offset)
return NTLSTATUS_LIMIT_REACHED;
if (size == 0)
return NTLSTATUS_SUCCESS;
KSTATUS status = Ext4AcquireFs();
if (!KSUCCESS(status))
return status;
int r = EOK;
uint64_t fileSize = ext4_fsize(&file->File);
if (offset > fileSize)
r = Ext4GrowFileLocked(&file->File, offset);
if (r == EOK)
r = ext4_fseek(&file->File, (int64_t)offset, SEEK_SET);
if (r == EOK)
r = ext4_fwrite(&file->File, buffer, size, bytesWritten);
if (r == EOK && *bytesWritten)
Ext4TouchLocked(file->Path, EXT4_TOUCH_MTIME | EXT4_TOUCH_CTIME);
status = Ext4MapError(r);
Ext4ReleaseFs();
return status;
}
static KSTATUS Ext4Create(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW path, void** fileCtx) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !fileCtx || !Ext4PathValid(path, false))
return NTLSTATUS_INVALID_PARAMETER;
char* fullPath = Ext4BuildPath(volume, path);
if (!fullPath)
return NTLSTATUS_NO_MEMORY;
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)MmAllocatePool(sizeof(*file));
if (!file) {
MmFreePool(fullPath);
return NTLSTATUS_NO_MEMORY;
}
memset(file, 0, sizeof(*file));
file->Volume = volume;
KSTATUS status = Ext4AcquireFs();
if (KSUCCESS(status)) {
int r = ext4_fopen2(&file->File, fullPath, O_RDWR | O_CREAT | O_EXCL);
if (r == EOK)
Ext4TouchLocked(fullPath, EXT4_TOUCH_ATIME | EXT4_TOUCH_MTIME | EXT4_TOUCH_CTIME);
status = Ext4MapError(r);
Ext4ReleaseFs();
}
if (!KSUCCESS(status)) {
MmFreePool(fullPath);
MmFreePool(file);
return status;
}
file->Path = fullPath;
*fileCtx = file;
return NTLSTATUS_SUCCESS;
}
static KSTATUS Ext4Delete(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW path) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !Ext4PathValid(path, false))
return NTLSTATUS_INVALID_PARAMETER;
char* fullPath = Ext4BuildPath(volume, path);
if (!fullPath)
return NTLSTATUS_NO_MEMORY;
KSTATUS status = Ext4AcquireFs();
if (KSUCCESS(status)) {
status = Ext4MapError(ext4_fremove(fullPath));
Ext4ReleaseFs();
}
MmFreePool(fullPath);
return status;
}
static KSTATUS Ext4Rename(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW oldPath,
SYS_STRING_VIEW newPath) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !Ext4PathValid(oldPath, false) || !Ext4PathValid(newPath, false))
return NTLSTATUS_INVALID_PARAMETER;
char* oldFull = Ext4BuildPath(volume, oldPath);
char* newFull = Ext4BuildPath(volume, newPath);
if (!oldFull || !newFull) {
if (oldFull) MmFreePool(oldFull);
if (newFull) MmFreePool(newFull);
return NTLSTATUS_NO_MEMORY;
}
KSTATUS status = Ext4AcquireFs();
if (KSUCCESS(status)) {
int r;
if (ext4_inode_exist(oldFull, EXT4_DE_DIR) == EOK)
r = ext4_dir_mv(oldFull, newFull);
else
r = ext4_frename(oldFull, newFull);
status = Ext4MapError(r);
Ext4ReleaseFs();
}
MmFreePool(oldFull);
MmFreePool(newFull);
return status;
}
static KSTATUS Ext4CreateDir(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW path) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !Ext4PathValid(path, false))
return NTLSTATUS_INVALID_PARAMETER;
char* fullPath = Ext4BuildPath(volume, path);
if (!fullPath)
return NTLSTATUS_NO_MEMORY;
KSTATUS status = Ext4AcquireFs();
if (KSUCCESS(status)) {
int r = ext4_dir_mk(fullPath);
if (r == EOK)
Ext4TouchLocked(fullPath, EXT4_TOUCH_ATIME | EXT4_TOUCH_MTIME | EXT4_TOUCH_CTIME);
status = Ext4MapError(r);
Ext4ReleaseFs();
}
MmFreePool(fullPath);
return status;
}
static KSTATUS Ext4RemoveDir(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW path) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !Ext4PathValid(path, false))
return NTLSTATUS_INVALID_PARAMETER;
char* fullPath = Ext4BuildPath(volume, path);
if (!fullPath)
return NTLSTATUS_NO_MEMORY;
KSTATUS status = Ext4AcquireFs();
if (!KSUCCESS(status)) {
MmFreePool(fullPath);
return status;
}
ext4_dir dir;
memset(&dir, 0, sizeof(dir));
int r = ext4_dir_open(&dir, fullPath);
bool empty = true;
if (r == EOK) {
const ext4_direntry* entry;
while ((entry = ext4_dir_entry_next(&dir)) != (const ext4_direntry*)0) {
if (entry->inode != 0 && !Ext4IsDotEntry(entry)) {
empty = false;
break;
}
}
int closeResult = ext4_dir_close(&dir);
if (r == EOK)
r = closeResult;
}
if (r == EOK && !empty)
r = ENOTEMPTY;
if (r == EOK)
r = ext4_dir_rm(fullPath);
status = Ext4MapError(r);
Ext4ReleaseFs();
MmFreePool(fullPath);
return status;
}
static KSTATUS Ext4Truncate(IO_VOLUME_PROVIDER* provider, void* fileCtx,
uint64_t newSize, uint32_t fileFlags) {
(void)provider;
(void)fileFlags;
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)fileCtx;
if (!file)
return NTLSTATUS_INVALID_PARAMETER;
if (newSize > (uint64_t)INT64_MAX)
return NTLSTATUS_LIMIT_REACHED;
KSTATUS status = Ext4AcquireFs();
if (!KSUCCESS(status))
return status;
uint64_t oldSize = ext4_fsize(&file->File);
int r = newSize <= oldSize
? ext4_ftruncate(&file->File, newSize)
: Ext4GrowFileLocked(&file->File, newSize);
if (r == EOK && newSize != oldSize)
Ext4TouchLocked(file->Path, EXT4_TOUCH_MTIME | EXT4_TOUCH_CTIME);
status = Ext4MapError(r);
Ext4ReleaseFs();
return status;
}
static uint64_t Ext4GetSize(IO_VOLUME_PROVIDER* provider, void* fileCtx) {
(void)provider;
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)fileCtx;
if (!file)
return 0;
if (!KSUCCESS(Ext4AcquireFs()))
return file->File.fsize;
uint64_t size = ext4_fsize(&file->File);
Ext4ReleaseFs();
return size;
}
static KSTATUS Ext4QueryInfo(IO_VOLUME_PROVIDER* provider, void* fileCtx, SYS_FILE_STAT* out) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)fileCtx;
if (!volume || !file || !out)
return NTLSTATUS_INVALID_PARAMETER;
memset(out, 0, sizeof(*out));
out->Size = sizeof(*out);
out->Type = SYS_FILE_TYPE_REGULAR;
out->Attributes = SYS_FILE_ATTR_ARCHIVE;
out->LinkCount = 1;
out->FileSize = Ext4GetSize(provider, fileCtx);
out->FileId = file->File.inode;
if (volume->Volume)
out->VolumeId = IoGetVolumeInstanceId(volume->Volume);
KSTATUS status = Ext4AcquireFs();
if (KSUCCESS(status)) {
uint32_t atime = 0, mtime = 0, ctime = 0;
if (file->Path && ext4_atime_get(file->Path, &atime) == EOK)
Ext4SetEpochTime(&out->LastAccessTime, atime);
if (file->Path && ext4_mtime_get(file->Path, &mtime) == EOK)
Ext4SetEpochTime(&out->LastWriteTime, mtime);
if (file->Path && ext4_ctime_get(file->Path, &ctime) == EOK) {
Ext4SetEpochTime(&out->ChangeTime, ctime);
Ext4SetEpochTime(&out->CreationTime, ctime);
}
Ext4ReleaseFs();
}
return NTLSTATUS_SUCCESS;
}
static KSTATUS Ext4Stat(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW path, SYS_FILE_STAT* out) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !out || !Ext4PathValid(path, true))
return NTLSTATUS_INVALID_PARAMETER;
char* fullPath = Ext4BuildPath(volume, path);
if (!fullPath)
return NTLSTATUS_NO_MEMORY;
KSTATUS status = Ext4AcquireFs();
if (!KSUCCESS(status)) {
MmFreePool(fullPath);
return status;
}
memset(out, 0, sizeof(*out));
out->Size = sizeof(*out);
out->LinkCount = 1;
if (volume->Volume)
out->VolumeId = IoGetVolumeInstanceId(volume->Volume);
bool isDir = ext4_inode_exist(fullPath, EXT4_DE_DIR) == EOK;
int r = EOK;
if (isDir) {
ext4_dir dir;
memset(&dir, 0, sizeof(dir));
r = ext4_dir_open(&dir, fullPath);
if (r == EOK) {
out->Type = SYS_FILE_TYPE_DIRECTORY;
out->Attributes = SYS_FILE_ATTR_DIRECTORY;
out->FileId = dir.f.inode;
r = ext4_dir_close(&dir);
}
} else {
ext4_file file;
memset(&file, 0, sizeof(file));
r = ext4_fopen2(&file, fullPath, O_RDONLY);
if (r == EOK) {
out->Type = SYS_FILE_TYPE_REGULAR;
out->Attributes = SYS_FILE_ATTR_ARCHIVE;
out->FileId = file.inode;
out->FileSize = ext4_fsize(&file);
r = ext4_fclose(&file);
}
}
if (r == EOK) {
uint32_t atime = 0, mtime = 0, ctime = 0;
if (ext4_atime_get(fullPath, &atime) == EOK)
Ext4SetEpochTime(&out->LastAccessTime, atime);
if (ext4_mtime_get(fullPath, &mtime) == EOK)
Ext4SetEpochTime(&out->LastWriteTime, mtime);
if (ext4_ctime_get(fullPath, &ctime) == EOK) {
Ext4SetEpochTime(&out->ChangeTime, ctime);
Ext4SetEpochTime(&out->CreationTime, ctime);
}
}
status = Ext4MapError(r);
Ext4ReleaseFs();
MmFreePool(fullPath);
return status;
}
static KSTATUS Ext4QueryIdentity(IO_VOLUME_PROVIDER* provider, void* fileCtx,
IO_FILE_IDENTITY* out) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)fileCtx;
if (!volume || !file || !out)
return NTLSTATUS_INVALID_PARAMETER;
memset(out, 0, sizeof(*out));
if (volume->Volume)
out->VolumeId = IoGetVolumeInstanceId(volume->Volume);
out->FileId = file->File.inode;
out->Size = Ext4GetSize(provider, fileCtx);
return NTLSTATUS_SUCCESS;
}
static KSTATUS Ext4Flush(IO_VOLUME_PROVIDER* provider, void* fileCtx) {
(void)fileCtx;
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume)
return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = Ext4AcquireFs();
if (!KSUCCESS(status))
return status;
int r = ext4_cache_flush(volume->MountName);
Ext4ReleaseFs();
if (r != EOK)
return Ext4MapError(r);
return IoFlushDisk(volume->DiskIndex);
}
static void Ext4Close(IO_VOLUME_PROVIDER* provider, void* fileCtx) {
(void)provider;
NTL_EXT4_FILE* file = (NTL_EXT4_FILE*)fileCtx;
if (!file)
return;
if (KSUCCESS(Ext4AcquireFs())) {
(void)ext4_fclose(&file->File);
Ext4ReleaseFs();
}
if (file->Path) MmFreePool(file->Path);
MmFreePool(file);
}
static void Ext4QueryStats(IO_VOLUME_PROVIDER* provider, IO_FILESYSTEM_STATS* out) {
(void)provider;
if (out)
memset(out, 0, sizeof(*out));
}
static KSTATUS Ext4OpenDir(IO_VOLUME_PROVIDER* provider, SYS_STRING_VIEW path, void** dirCtx) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume || !dirCtx || !Ext4PathValid(path, true))
return NTLSTATUS_INVALID_PARAMETER;
char* fullPath = Ext4BuildPath(volume, path);
if (!fullPath)
return NTLSTATUS_NO_MEMORY;
NTL_EXT4_DIR* dir = (NTL_EXT4_DIR*)MmAllocatePool(sizeof(*dir));
if (!dir) {
MmFreePool(fullPath);
return NTLSTATUS_NO_MEMORY;
}
memset(dir, 0, sizeof(*dir));
dir->Volume = volume;
KSTATUS status = Ext4AcquireFs();
if (KSUCCESS(status)) {
status = Ext4MapError(ext4_dir_open(&dir->Dir, fullPath));
Ext4ReleaseFs();
}
if (!KSUCCESS(status)) {
MmFreePool(fullPath);
MmFreePool(dir);
return status;
}
dir->Path = fullPath;
*dirCtx = dir;
return NTLSTATUS_SUCCESS;
}
static KSTATUS Ext4ReadDir(IO_VOLUME_PROVIDER* provider, void* dirCtx, KDIR_ENTRY* out) {
(void)provider;
NTL_EXT4_DIR* dir = (NTL_EXT4_DIR*)dirCtx;
if (!dir || !out)
return NTLSTATUS_INVALID_PARAMETER;
KSTATUS status = Ext4AcquireFs();
if (!KSUCCESS(status))
return status;
const ext4_direntry* entry;
do {
entry = ext4_dir_entry_next(&dir->Dir);
} while (entry && (entry->inode == 0 || Ext4IsDotEntry(entry)));
if (!entry) {
Ext4ReleaseFs();
return NTLSTATUS_NO_MORE_FILES;
}
memset(out, 0, sizeof(*out));
size_t nameLength = entry->name_length;
if (nameLength >= SYS_DIR_NAME_MAX)
nameLength = SYS_DIR_NAME_MAX - 1;
memcpy(out->Name, entry->name, nameLength);
out->Name[nameLength] = '\0';
out->Attr = entry->inode_type == EXT4_DE_DIR
? SYS_FILE_ATTR_DIRECTORY
: SYS_FILE_ATTR_ARCHIVE;
/* SYS_DIR_ENTRY carries the size directly. NTFS/FAT populate it during
* enumeration, so do the same here instead of making `dir` report 0. */
char* childPath = Ext4BuildDirEntryPath(dir, entry);
if (childPath) {
if (entry->inode_type != EXT4_DE_DIR) {
ext4_file file;
memset(&file, 0, sizeof(file));
if (ext4_fopen2(&file, childPath, O_RDONLY) == EOK) {
uint64_t fileSize = ext4_fsize(&file);
out->FileSize = fileSize > UINT32_MAX ? UINT32_MAX : (uint32_t)fileSize;
(void)ext4_fclose(&file);
}
}
uint32_t timestamp = 0;
if (ext4_ctime_get(childPath, &timestamp) == EOK)
Ext4SetEpochTime(&out->CreateTime, timestamp);
if (ext4_atime_get(childPath, &timestamp) == EOK)
Ext4SetEpochTime(&out->LastAccessTime, timestamp);
if (ext4_mtime_get(childPath, &timestamp) == EOK)
Ext4SetEpochTime(&out->LastWriteTime, timestamp);
MmFreePool(childPath);
}
Ext4ReleaseFs();
return NTLSTATUS_SUCCESS;
}
static void Ext4CloseDir(IO_VOLUME_PROVIDER* provider, void* dirCtx) {
(void)provider;
NTL_EXT4_DIR* dir = (NTL_EXT4_DIR*)dirCtx;
if (!dir)
return;
if (KSUCCESS(Ext4AcquireFs())) {
(void)ext4_dir_close(&dir->Dir);
Ext4ReleaseFs();
}
if (dir->Path)
MmFreePool(dir->Path);
MmFreePool(dir);
}
static void Ext4VolumeDead(IO_VOLUME_PROVIDER* provider, IO_VOLUME_REMOVAL_REASON reason) {
(void)reason;
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (volume)
volume->Dead = true;
}
static void Ext4DestroyProvider(IO_VOLUME_PROVIDER* provider) {
NTL_EXT4_VOLUME* volume = Ext4Volume(provider);
if (!volume)
return;
bool canFree = true;
if (KSUCCESS(Ext4AcquireFs())) {
if (volume->Mounted) {
if (volume->JournalStarted)
(void)ext4_journal_stop(volume->MountName);
(void)ext4_cache_flush(volume->MountName);
int r = ext4_umount(volume->MountName);
if (r == EOK) {
volume->Mounted = false;
/* SharpExt4 v0.0.3 unregister clears the slot but returns ENOENT. */
(void)ext4_device_unregister(volume->DeviceName);
} else {
/* Static lwext4 tables may still reference this blockdev. Leak instead of UAF. */
canFree = false;
DbgPrint("EXT4: unmount failed for %s rc=%d; retaining mount context\n",
volume->MountName, r);
}
}
Ext4ReleaseFs();
} else {
canFree = false;
}
if (canFree) {
memset(volume, 0, sizeof(*volume));
MmFreePool(volume);
}
}
static void Ext4InitProvider(IO_VOLUME_PROVIDER* provider, NTL_EXT4_VOLUME* volume) {
memset(provider, 0, sizeof(*provider));
provider->Open = Ext4Open;
provider->Read = Ext4Read;
provider->Write = Ext4Write;
provider->Create = Ext4Create;
provider->Delete = Ext4Delete;
provider->Rename = Ext4Rename;
provider->CreateDir = Ext4CreateDir;
provider->RemoveDir = Ext4RemoveDir;
provider->Truncate = Ext4Truncate;
provider->GetSize = Ext4GetSize;
provider->QueryInfo = Ext4QueryInfo;
provider->Stat = Ext4Stat;
provider->QueryIdentity = Ext4QueryIdentity;
provider->Flush = Ext4Flush;
provider->Close = Ext4Close;
provider->QueryStats = Ext4QueryStats;
provider->OpenDir = Ext4OpenDir;
provider->ReadDir = Ext4ReadDir;
provider->CloseDir = Ext4CloseDir;
provider->VolumeDead = Ext4VolumeDead;
provider->Destroy = Ext4DestroyProvider;
provider->Context = volume;
}
/* ---- partition discovery / mount lifecycle ---- */
static NTL_EXT4_VOLUME* Ext4FindMountLocked(const IO_PARTITION_INFO* partition) {
if (!partition)
return (NTL_EXT4_VOLUME*)0;
for (LIST_ENTRY* entry = gExt4Mounts.Fwd; entry != &gExt4Mounts; entry = entry->Fwd) {
NTL_EXT4_VOLUME* volume = CONTAINING_RECORD(entry, NTL_EXT4_VOLUME, MountLink);
if (volume->DiskIndex == partition->DiskIndex &&
volume->PartitionLba == partition->StartLba)
return volume;
}
return (NTL_EXT4_VOLUME*)0;
}
static bool Ext4ProbePartition(const IO_PARTITION_INFO* partition) {
if (!partition || partition->SectorCount < 3)
return false;
uint8_t sector[NTL_EXT4_SECTOR_SIZE];
KSTATUS status = IoReadDiskSectors(partition->DiskIndex,
partition->StartLba + 2,
1,
sector);
if (!KSUCCESS(status))
return false;
/* ext superblock starts at byte 1024; s_magic is +56. */
return sector[56] == 0x53 && sector[57] == 0xEF;
}
static void Ext4Hex8(char* out, uint32_t value) {
static const char digits[] = "0123456789abcdef";
for (int i = 7; i >= 0; --i) {
out[i] = digits[value & 0xFu];
value >>= 4;
}
}
static void Ext4AssignNames(NTL_EXT4_VOLUME* volume) {
uint32_t serial = ++gExt4Serial;
memcpy(volume->DeviceName, "ntlext4", 7);
Ext4Hex8(volume->DeviceName + 7, serial);
volume->DeviceName[15] = '\0';
volume->MountName[0] = '/';
volume->MountName[1] = 'e';
Ext4Hex8(volume->MountName + 2, serial);
volume->MountName[10] = '/';
volume->MountName[11] = '\0';
}
static bool Ext4TryMountPartition(const IO_PARTITION_INFO* partition) {
if (!partition || partition->SectorCount < 3 ||
partition->SectorCount > UINT64_MAX / NTL_EXT4_SECTOR_SIZE ||
!IoDiskDevicePresent(partition->DiskIndex))
return false;
if (!Ext4ProbePartition(partition))
return false;
Ext4EnsureMutexes();
if (!KE_MUTEX_ACQUIRE_SUCCEEDED(KeAcquireMutex(&gExt4MountMutex, KE_WAIT_INFINITE)))
return false;
if (Ext4FindMountLocked(partition)) {
KeReleaseMutex(&gExt4MountMutex);
return false;
}
NTL_EXT4_VOLUME* volume = (NTL_EXT4_VOLUME*)MmAllocatePool(sizeof(*volume));
if (!volume) {
KeReleaseMutex(&gExt4MountMutex);
return false;
}
memset(volume, 0, sizeof(*volume));
Ext4ListInit(&volume->MountLink);
volume->DiskIndex = partition->DiskIndex;
volume->PartitionLba = partition->StartLba;
volume->PartitionSectors = partition->SectorCount;
Ext4AssignNames(volume);
Ext4InitBlockDevice(volume);
KSTATUS status = Ext4AcquireFs();
int r = EIO;
if (KSUCCESS(status)) {
r = ext4_device_register(&volume->BlockDev, volume->DeviceName);
if (r == EOK) {
r = ext4_mount(volume->DeviceName, volume->MountName, false);
if (r == EOK) {
volume->Mounted = true;
int recover = ext4_recover(volume->MountName);
if (recover != EOK && recover != ENOTSUP)
DbgPrint("EXT4: journal recovery %s rc=%d (continuing)\n",
volume->MountName, recover);
int journal = ext4_journal_start(volume->MountName);
if (journal == EOK)
volume->JournalStarted = true;
else
DbgPrint("EXT4: journal start %s rc=%d (continuing)\n",
volume->MountName, journal);
} else {
(void)ext4_device_unregister(volume->DeviceName);
}
}
Ext4ReleaseFs();
}
if (r != EOK) {
DbgPrint("EXT4: disk=%u lba=%llu lwext4 mount failed rc=%d\n",
partition->DiskIndex, (unsigned long long)partition->StartLba, r);
MmFreePool(volume);
KeReleaseMutex(&gExt4MountMutex);
return false;
}
IO_VOLUME_PROVIDER provider;
Ext4InitProvider(&provider, volume);
IO_VOLUME_SOURCE source;
source.Type = IoVolumeSourcePartition;
source.DiskIndex = volume->DiskIndex;
memset(source.Reserved, 0, sizeof(source.Reserved));
source.StartLba = volume->PartitionLba;
source.SectorCount = volume->PartitionSectors;
IO_VOLUME* ioVolume = (IO_VOLUME*)0;
status = IoCreateVolume(&provider, &source, &ioVolume);
if (!KSUCCESS(status)) {
if (ioVolume) {
(void)IoRemoveVolume(ioVolume, IoVolumeUnmounted);
IoReleaseVolume(ioVolume);
} else {
/* No IO_VOLUME owns the provider yet, so clean it here. */
Ext4DestroyProvider(&provider);
}
DbgPrint("EXT4: disk=%u lba=%llu IoCreateVolume failed status=0x%x\n",
partition->DiskIndex, (unsigned long long)partition->StartLba, status);
KeReleaseMutex(&gExt4MountMutex);
return false;
}
volume->Volume = ioVolume;
ListInsertTail(&gExt4Mounts, &volume->MountLink);
gExt4MountedCount++;
uint32_t instance = IoGetVolumeInstanceId(ioVolume);
uint32_t count = gExt4MountedCount;
DbgPrint("EXT4: disk=%u lba=%llu mounted volume=%u at %s; total=%u\n",
volume->DiskIndex, (unsigned long long)volume->PartitionLba,
instance, volume->MountName, count);
KeReleaseMutex(&gExt4MountMutex);
return true;
}
static void Ext4UnmountPartition(const IO_PARTITION_INFO* partition, IO_PARTITION_CHANGE change) {
if (!partition)
return;
Ext4EnsureMutexes();
if (!KE_MUTEX_ACQUIRE_SUCCEEDED(KeAcquireMutex(&gExt4MountMutex, KE_WAIT_INFINITE)))
return;
NTL_EXT4_VOLUME* volume = Ext4FindMountLocked(partition);
if (!volume) {
KeReleaseMutex(&gExt4MountMutex);
return;
}
IO_VOLUME* ioVolume = volume->Volume;
uint32_t instance = IoGetVolumeInstanceId(ioVolume);
ListRemove(&volume->MountLink);
volume->Volume = (IO_VOLUME*)0;
if (gExt4MountedCount)
gExt4MountedCount--;
uint32_t count = gExt4MountedCount;
KeReleaseMutex(&gExt4MountMutex);
(void)IoRemoveVolume(ioVolume, IoVolumeBackingRemoved);
IoReleaseVolume(ioVolume);
DbgPrint("EXT4: disk=%u lba=%llu %s; unmounted volume=%u; total=%u\n",
partition->DiskIndex, (unsigned long long)partition->StartLba,
change == IoPartitionSurpriseRemoved ? "surprise removed" : "removed",
instance, count);
}
static void Ext4PartitionChange(const IO_PARTITION_INFO* partition,
IO_PARTITION_CHANGE change,
void* context) {
(void)context;
switch (change) {
case IoPartitionArrived:
(void)Ext4TryMountPartition(partition);
break;
case IoPartitionSurpriseRemoved:
case IoPartitionRemoved:
Ext4UnmountPartition(partition, change);
break;
default:
break;
}
}
KSTATUS DriverEntry(const NTL_DDK* ddk, NTL_DRIVER_INIT* init) {
DbgPrint("EXT4: Init\n");
if (!ddk)
return NTLSTATUS_INVALID_PARAMETER;
if (init)
init->Name = "ext4";
Ext4ListInit(&gExt4Mounts);
gExt4MountedCount = 0;
gExt4Serial = 0;
Ext4EnsureMutexes();
KSTATUS status = IoRegisterPartitionChangeCallback(Ext4PartitionChange, (void*)0);
if (!KSUCCESS(status)) {
DbgPrint("EXT4: partition callback registration failed status=0x%x\n", status);
return status;
}
if (!gExt4MountedCount)
DbgPrint("EXT4: no mountable ext partitions found; driver resident for partition changes.\n");
DbgPrint("EXT4: Init complete; mounted %u volume(s).\n", gExt4MountedCount);
return NTLSTATUS_SUCCESS;
}