Files
nt-lite/code/drivers/fs/ext4/ext4_driver.c
T

1180 lines
38 KiB
C

#include <ntlite/ddk/module.h>
/*
* 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));
// ext4 allows arbitrary non-NUL filename bytes. Project malformed UTF-8
// to U+FFFD only for enumeration; keep raw bytes for ext4 metadata lookups.
// The displayed name is not a reversible path for malformed disk names.
(void)SysSvCopyUtf8WithReplacement(
SysSvFromParts(entry->name, entry->name_length), out->Name, sizeof(out->Name));
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;
}
}
static KSTATUS DriverInitialize(NTL_MODULE* module) {
(void)module;
DbgPrint("EXT4: Init\n");
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;
}
NTL_DECLARE_MODULE(DriverInitialize);