#include /* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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, ×tamp) == EOK) Ext4SetEpochTime(&out->CreateTime, timestamp); if (ext4_atime_get(childPath, ×tamp) == EOK) Ext4SetEpochTime(&out->LastAccessTime, timestamp); if (ext4_mtime_get(childPath, ×tamp) == 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);