diff --git a/Ext4Pkg/Ext4.dsc.inc b/Ext4Pkg/Ext4.dsc.inc new file mode 100644 index 0000000000..c060cb6687 --- /dev/null +++ b/Ext4Pkg/Ext4.dsc.inc @@ -0,0 +1,16 @@ +## @file +# Ext4 DSC include file for Platform DSC +# +# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +[Defines] +!include Ext4Pkg/Ext4Defines.dsc.inc + +[LibraryClasses] +!include Ext4Pkg/Ext4Libs.dsc.inc + +[Components.common] +!include Ext4Pkg/Ext4Components.dsc.inc diff --git a/Ext4Pkg/Ext4.fdf.inc b/Ext4Pkg/Ext4.fdf.inc new file mode 100644 index 0000000000..ae3ee60938 --- /dev/null +++ b/Ext4Pkg/Ext4.fdf.inc @@ -0,0 +1,11 @@ +## @file +# Ext4 FDF include file for All Architectures. +# +# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +!if $(EXT4_ENABLE) == TRUE + INF Ext4Pkg/Ext4Dxe/Ext4Dxe.inf +!endif diff --git a/Ext4Pkg/Ext4Components.dsc.inc b/Ext4Pkg/Ext4Components.dsc.inc new file mode 100644 index 0000000000..853b35312b --- /dev/null +++ b/Ext4Pkg/Ext4Components.dsc.inc @@ -0,0 +1,14 @@ +## @file +# Ext4 DSC include file for [Components] section of all Architectures. +# +# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +!if $(EXT4_ENABLE) == TRUE + Ext4Pkg/Ext4Dxe/Ext4Dxe.inf { + + gEfiMdePkgTokenSpaceGuid.PcdDebugPrintErrorLevel|0x80000007 + } +!endif diff --git a/Ext4Pkg/Ext4Defines.dsc.inc b/Ext4Pkg/Ext4Defines.dsc.inc new file mode 100644 index 0000000000..b02eac59cf --- /dev/null +++ b/Ext4Pkg/Ext4Defines.dsc.inc @@ -0,0 +1,14 @@ +## @file +# Ext4 DSC include file for [Defines] section of all Architectures. +# +# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +!ifndef EXT4_ENABLE + # + # This flag is to enable or disable the ext4 feature. + # + DEFINE EXT4_ENABLE = TRUE +!endif diff --git a/Ext4Pkg/Ext4Dxe/BlockGroup.c b/Ext4Pkg/Ext4Dxe/BlockGroup.c new file mode 100644 index 0000000000..cba96cd95a --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/BlockGroup.c @@ -0,0 +1,228 @@ +/** @file + Block group related routines + + Copyright (c) 2021 Pedro Falcato All rights reserved. + Copyright (c) 2007 - 2018, Intel Corporation. All rights reserved.
+ + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +/** + Retrieves a block group descriptor of the ext4 filesystem. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[in] BlockGroup Block group number. + + @return A pointer to the block group descriptor. +**/ +EXT4_BLOCK_GROUP_DESC * +Ext4GetBlockGroupDesc ( + IN EXT4_PARTITION *Partition, + IN UINT32 BlockGroup + ) +{ + // Maybe assert that the block group nr isn't a nonsense number? + return (EXT4_BLOCK_GROUP_DESC *)((CHAR8 *)Partition->BlockGroups + BlockGroup * Partition->DescSize); +} + +/** + Reads an inode from disk. + + @param[in] Partition Pointer to the opened partition. + @param[in] InodeNum Number of the desired Inode + @param[out] OutIno Pointer to where it will be stored a pointer to the read inode. + + @return Status of the inode read. +**/ +EFI_STATUS +Ext4ReadInode ( + IN EXT4_PARTITION *Partition, + IN EXT4_INO_NR InodeNum, + OUT EXT4_INODE **OutIno + ) +{ + UINT64 InodeOffset; + UINT32 BlockGroupNumber; + EXT4_INODE *Inode; + EXT4_BLOCK_GROUP_DESC *BlockGroup; + EXT4_BLOCK_NR InodeTableStart; + EFI_STATUS Status; + + BlockGroupNumber = (UINT32)DivU64x64Remainder ( + InodeNum - 1, + Partition->SuperBlock.s_inodes_per_group, + &InodeOffset + ); + + // Check for the block group number's correctness + if (BlockGroupNumber >= Partition->NumberBlockGroups) { + return EFI_VOLUME_CORRUPTED; + } + + Inode = Ext4AllocateInode (Partition); + + if (Inode == NULL) { + return EFI_OUT_OF_RESOURCES; + } + + BlockGroup = Ext4GetBlockGroupDesc (Partition, BlockGroupNumber); + + // Note: We'll need to check INODE_UNINIT and friends when/if we add write support + + InodeTableStart = EXT4_BLOCK_NR_FROM_HALFS ( + Partition, + BlockGroup->bg_inode_table_lo, + BlockGroup->bg_inode_table_hi + ); + + Status = Ext4ReadDiskIo ( + Partition, + Inode, + Partition->InodeSize, + EXT4_BLOCK_TO_BYTES (Partition, InodeTableStart) + MultU64x32 (InodeOffset, Partition->InodeSize) + ); + + if (EFI_ERROR (Status)) { + DEBUG (( + DEBUG_ERROR, + "[ext4] Error reading inode: status %x; inode offset %lx" + " inode table start %lu block group %lu\n", + Status, + InodeOffset, + InodeTableStart, + BlockGroupNumber + )); + FreePool (Inode); + return Status; + } + + if (!Ext4CheckInodeChecksum (Partition, Inode, InodeNum)) { + DEBUG (( + DEBUG_ERROR, + "[ext4] Inode %llu has invalid checksum (calculated %x)\n", + InodeNum, + Ext4CalculateInodeChecksum (Partition, Inode, InodeNum) + )); + FreePool (Inode); + return EFI_VOLUME_CORRUPTED; + } + + *OutIno = Inode; + return EFI_SUCCESS; +} + +/** + Calculates the checksum of the block group descriptor for METADATA_CSUM enabled filesystems. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] BlockGroupDesc Pointer to the block group descriptor. + @param[in] BlockGroupNum Number of the block group. + + @return The checksum. +**/ +STATIC +UINT16 +Ext4CalculateBlockGroupDescChecksumMetadataCsum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_BLOCK_GROUP_DESC *BlockGroupDesc, + IN UINT32 BlockGroupNum + ) +{ + UINT32 Csum; + UINT16 Dummy; + + Dummy = 0; + + Csum = Ext4CalculateChecksum (Partition, &BlockGroupNum, sizeof (BlockGroupNum), Partition->InitialSeed); + Csum = Ext4CalculateChecksum (Partition, BlockGroupDesc, OFFSET_OF (EXT4_BLOCK_GROUP_DESC, bg_checksum), Csum); + Csum = Ext4CalculateChecksum (Partition, &Dummy, sizeof (Dummy), Csum); + Csum = + Ext4CalculateChecksum ( + Partition, + &BlockGroupDesc->bg_block_bitmap_hi, + Partition->DescSize - OFFSET_OF (EXT4_BLOCK_GROUP_DESC, bg_block_bitmap_hi), + Csum + ); + return (UINT16)Csum; +} + +/** + Calculates the checksum of the block group descriptor for GDT_CSUM enabled filesystems. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] BlockGroupDesc Pointer to the block group descriptor. + @param[in] BlockGroupNum Number of the block group. + + @return The checksum. +**/ +STATIC +UINT16 +Ext4CalculateBlockGroupDescChecksumGdtCsum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_BLOCK_GROUP_DESC *BlockGroupDesc, + IN UINT32 BlockGroupNum + ) +{ + UINT16 Csum; + UINT16 Dummy; + + Dummy = 0; + + Csum = CalculateCrc16Ansi (Partition->SuperBlock.s_uuid, 16, 0); + Csum = CalculateCrc16Ansi (&BlockGroupNum, sizeof (BlockGroupNum), Csum); + Csum = CalculateCrc16Ansi (BlockGroupDesc, OFFSET_OF (EXT4_BLOCK_GROUP_DESC, bg_checksum), Csum); + Csum = CalculateCrc16Ansi (&Dummy, sizeof (Dummy), Csum); + Csum = + CalculateCrc16Ansi ( + &BlockGroupDesc->bg_block_bitmap_hi, + Partition->DescSize - OFFSET_OF (EXT4_BLOCK_GROUP_DESC, bg_block_bitmap_hi), + Csum + ); + return Csum; +} + +/** + Checks if the checksum of the block group descriptor is correct. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] BlockGroupDesc Pointer to the block group descriptor. + @param[in] BlockGroupNum Number of the block group. + + @return TRUE if checksum is correct, FALSE if there is corruption. +**/ +BOOLEAN +Ext4VerifyBlockGroupDescChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_BLOCK_GROUP_DESC *BlockGroupDesc, + IN UINT32 BlockGroupNum + ) +{ + if (!EXT4_HAS_METADATA_CSUM (Partition) && !EXT4_HAS_GDT_CSUM (Partition)) { + return TRUE; + } + + return Ext4CalculateBlockGroupDescChecksum (Partition, BlockGroupDesc, BlockGroupNum) == BlockGroupDesc->bg_checksum; +} + +/** + Calculates the checksum of the block group descriptor. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] BlockGroupDesc Pointer to the block group descriptor. + @param[in] BlockGroupNum Number of the block group. + + @return The checksum. +**/ +UINT16 +Ext4CalculateBlockGroupDescChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_BLOCK_GROUP_DESC *BlockGroupDesc, + IN UINT32 BlockGroupNum + ) +{ + if (EXT4_HAS_METADATA_CSUM (Partition)) { + return Ext4CalculateBlockGroupDescChecksumMetadataCsum (Partition, BlockGroupDesc, BlockGroupNum); + } else if (EXT4_HAS_GDT_CSUM (Partition)) { + return Ext4CalculateBlockGroupDescChecksumGdtCsum (Partition, BlockGroupDesc, BlockGroupNum); + } + + return 0; +} diff --git a/Ext4Pkg/Ext4Dxe/BlockMap.c b/Ext4Pkg/Ext4Dxe/BlockMap.c new file mode 100644 index 0000000000..2bc629fe9d --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/BlockMap.c @@ -0,0 +1,285 @@ +/** @file + Implementation of routines that deal with ext2/3 block maps. + + Copyright (c) 2022 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include + +// Note: The largest path we can take uses up 4 indices +#define EXT4_MAX_BLOCK_PATH 4 + +typedef enum ext4_logical_block_type { + EXT4_TYPE_DIRECT_BLOCK = 0, + EXT4_TYPE_SINGLY_BLOCK, + EXT4_TYPE_DOUBLY_BLOCK, + EXT4_TYPE_TREBLY_BLOCK, + EXT4_TYPE_BAD_BLOCK +} EXT4_LOGICAL_BLOCK_TYPE; + +/** + @brief Detect the type of path the logical block will follow + + @param[in] LogicalBlock The logical block + @param[in] Partition Pointer to an EXT4_PARTITION + @return The type of path the logical block will need to follow + */ +STATIC +EXT4_LOGICAL_BLOCK_TYPE +Ext4DetectBlockType ( + IN UINT32 LogicalBlock, + IN CONST EXT4_PARTITION *Partition + ) +{ + UINT32 Entries; + UINT32 MinSinglyBlock; + UINT32 MinDoublyBlock; + UINT32 MinTreblyBlock; + UINT32 MinQuadBlock; + + Entries = (Partition->BlockSize / sizeof (UINT32)); + MinSinglyBlock = EXT4_DBLOCKS; + MinDoublyBlock = Entries + MinSinglyBlock; + MinTreblyBlock = Entries * Entries + MinDoublyBlock; + MinQuadBlock = Entries * Entries * Entries + MinTreblyBlock; // Doesn't actually exist + + if (LogicalBlock < MinSinglyBlock) { + return EXT4_TYPE_DIRECT_BLOCK; + } else if ((LogicalBlock >= MinSinglyBlock) && (LogicalBlock < MinDoublyBlock)) { + return EXT4_TYPE_SINGLY_BLOCK; + } else if ((LogicalBlock >= MinDoublyBlock) && (LogicalBlock < MinTreblyBlock)) { + return EXT4_TYPE_DOUBLY_BLOCK; + } else if (((LogicalBlock >= MinTreblyBlock) && (LogicalBlock < MinQuadBlock))) { + return EXT4_TYPE_TREBLY_BLOCK; + } else { + return EXT4_TYPE_BAD_BLOCK; + } +} + +/** + @brief Get a block's path in indices + + @param[in] Partition Pointer to an EXT4_PARTITION + @param[in] LogicalBlock Logical block + @param[out] BlockPath Pointer to an array of EXT4_MAX_BLOCK_PATH elements, where the + indices we'll need to read are inserted. + @return The number of path elements that are required (and were inserted in BlockPath) + */ +UINTN +Ext4GetBlockPath ( + IN CONST EXT4_PARTITION *Partition, + IN UINT32 LogicalBlock, + OUT EXT2_BLOCK_NR BlockPath[EXT4_MAX_BLOCK_PATH] + ) +{ + // The logic behind the block map is very much like a page table + // Let's think of blocks with 512 entries (exactly like a page table on x64). + // On doubly indirect block paths, we subtract the min doubly blocks from the logical block. + // The top 9 bits of the result are the index inside the dind block, the bottom 9 bits are the + // index inside the ind block. Since Entries is always a power of 2, entries - 1 will give us + // a mask of the BlockMapBits. + // Note that all this math could be done with ands and shifts (similar implementations exist + // in a bunch of other places), but I'm doing it a simplified way with divs and modulus, + // since it's not going to be a bottleneck anyway. + + UINT32 Entries; + UINT32 EntriesEntries; + UINT32 MinSinglyBlock; + UINT32 MinDoublyBlock; + UINT32 MinTreblyBlock; + + EXT4_LOGICAL_BLOCK_TYPE Type; + + Entries = (Partition->BlockSize / sizeof (UINT32)); + EntriesEntries = Entries * Entries; + + MinSinglyBlock = EXT4_DBLOCKS; + MinDoublyBlock = Entries + MinSinglyBlock; + MinTreblyBlock = EntriesEntries + MinDoublyBlock; + + Type = Ext4DetectBlockType (LogicalBlock, Partition); + + switch (Type) { + case EXT4_TYPE_DIRECT_BLOCK: + BlockPath[0] = LogicalBlock; + break; + case EXT4_TYPE_SINGLY_BLOCK: + BlockPath[0] = EXT4_IND_BLOCK; + BlockPath[1] = LogicalBlock - EXT4_DBLOCKS; + break; + case EXT4_TYPE_DOUBLY_BLOCK: + BlockPath[0] = EXT4_DIND_BLOCK; + LogicalBlock -= MinDoublyBlock; + BlockPath[1] = LogicalBlock / Entries; + BlockPath[2] = LogicalBlock % Entries; + break; + case EXT4_TYPE_TREBLY_BLOCK: + BlockPath[0] = EXT4_DIND_BLOCK; + LogicalBlock -= MinTreblyBlock; + BlockPath[1] = LogicalBlock / EntriesEntries; + BlockPath[2] = (LogicalBlock % EntriesEntries) / Entries; + BlockPath[3] = (LogicalBlock % EntriesEntries) % Entries; + break; + default: + // EXT4_TYPE_BAD_BLOCK + break; + } + + return Type + 1; +} + +/** + @brief Get an extent from a block map + Note: Also parses file holes and creates uninitialised extents from them. + + @param[in] Buffer Buffer of block pointers + @param[in] IndEntries Number of entries in this block pointer table + @param[in] StartIndex The start index from which we want to find a contiguous extent + @param[out] Extent Pointer to the resulting EXT4_EXTENT + */ +VOID +Ext4GetExtentInBlockMap ( + IN CONST UINT32 *Buffer, + IN CONST UINT32 IndEntries, + IN UINT32 StartIndex, + OUT EXT4_EXTENT *Extent + ) +{ + UINT32 Index; + UINT32 FirstBlock; + UINT32 LastBlock; + UINT16 Count; + + Count = 1; + LastBlock = Buffer[StartIndex]; + FirstBlock = LastBlock; + + if (FirstBlock == EXT4_BLOCK_FILE_HOLE) { + // File hole, let's see how many blocks this hole spans + Extent->ee_start_hi = 0; + Extent->ee_start_lo = 0; + + for (Index = StartIndex + 1; Index < IndEntries; Index++) { + if (Count == EXT4_EXTENT_MAX_INITIALIZED - 1) { + // We've reached the max size of an uninit extent, break + break; + } + + if (Buffer[Index] == EXT4_BLOCK_FILE_HOLE) { + Count++; + } else { + break; + } + } + + // We mark the extent as uninitialised, although there's a difference between uninit + // extents and file holes. + Extent->ee_len = EXT4_EXTENT_MAX_INITIALIZED + Count; + return; + } + + for (Index = StartIndex + 1; Index < IndEntries; Index++) { + if (Count == EXT4_EXTENT_MAX_INITIALIZED) { + // We've reached the max size of an extent, break + break; + } + + if ((Buffer[Index] == LastBlock + 1) && (Buffer[Index] != EXT4_BLOCK_FILE_HOLE)) { + Count++; + } else { + break; + } + + LastBlock = Buffer[Index]; + } + + Extent->ee_start_lo = FirstBlock; + Extent->ee_start_hi = 0; + Extent->ee_len = Count; +} + +/** + Retrieves an extent from an EXT2/3 inode (with a blockmap). + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] File Pointer to the opened file. + @param[in] LogicalBlock Block number which the returned extent must cover. + @param[out] Extent Pointer to the output buffer, where the extent will be copied to. + + @retval EFI_SUCCESS Retrieval was succesful. + @retval EFI_NO_MAPPING Block has no mapping. +**/ +EFI_STATUS +Ext4GetBlocks ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + IN EXT2_BLOCK_NR LogicalBlock, + OUT EXT4_EXTENT *Extent + ) +{ + EXT4_INODE *Inode; + EXT2_BLOCK_NR BlockPath[EXT4_MAX_BLOCK_PATH]; + UINTN BlockPathLength; + UINTN Index; + UINT32 *Buffer; + EFI_STATUS Status; + UINT32 Block; + UINT32 BlockIndex; + + Inode = File->Inode; + + BlockPathLength = Ext4GetBlockPath (Partition, LogicalBlock, BlockPath); + + if (BlockPathLength - 1 == EXT4_TYPE_BAD_BLOCK) { + // Bad logical block (out of range) + return EFI_NO_MAPPING; + } + + Extent->ee_block = LogicalBlock; + + if (BlockPathLength == 1) { + // Fast path for blocks 0 - 12 that skips allocations + Ext4GetExtentInBlockMap (Inode->i_data, EXT4_DBLOCKS, BlockPath[0], Extent); + + return EFI_SUCCESS; + } + + Buffer = AllocatePool (Partition->BlockSize); + if (Buffer == NULL) { + return EFI_OUT_OF_RESOURCES; + } + + // Note the BlockPathLength - 1 so we don't end up reading the final block + for (Index = 0; Index < BlockPathLength - 1; Index++) { + BlockIndex = BlockPath[Index]; + + if (Index == 0) { + Block = Inode->i_data[BlockIndex]; + } else { + Block = Buffer[BlockIndex]; + } + + if (Block == EXT4_BLOCK_FILE_HOLE) { + FreePool (Buffer); + return EFI_NO_MAPPING; + } + + Status = Ext4ReadBlocks (Partition, Buffer, 1, Block); + + if (EFI_ERROR (Status)) { + FreePool (Buffer); + return Status; + } + } + + Ext4GetExtentInBlockMap ( + Buffer, + Partition->BlockSize / sizeof (UINT32), + BlockPath[BlockPathLength - 1], + Extent + ); + + FreePool (Buffer); + + return EFI_SUCCESS; +} diff --git a/Ext4Pkg/Ext4Dxe/Collation.c b/Ext4Pkg/Ext4Dxe/Collation.c new file mode 100644 index 0000000000..91d172b1cb --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Collation.c @@ -0,0 +1,173 @@ +/** @file + Unicode collation routines + + Copyright (c) 2021 Pedro Falcato All rights reserved. + Copyright (c) 2005 - 2017, Intel Corporation. All rights reserved. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include + +#include +#include +#include + +#include + +STATIC EFI_UNICODE_COLLATION_PROTOCOL *gUnicodeCollationInterface = NULL; + +/* + * Note: This code is heavily based on FatPkg's Unicode collation, since they seem to know what + * they're doing. + * PS: Maybe all this code could be put in a library? It looks heavily shareable. +**/ + +/** + Worker function to initialize Unicode Collation support. + + It tries to locate Unicode Collation (2) protocol and matches it with current + platform language code. + + @param[in] DriverHandle The handle used to open Unicode Collation (2) protocol. + @param[in] ProtocolGuid The pointer to Unicode Collation (2) protocol GUID. + @param[in] VariableName The name of the RFC 4646 or ISO 639-2 language variable. + @param[in] DefaultLanguage The default language in case the RFC 4646 or ISO 639-2 language is absent. + + @retval EFI_SUCCESS The Unicode Collation (2) protocol has been successfully located. + @retval Others The Unicode Collation (2) protocol has not been located. + +**/ +STATIC +EFI_STATUS +Ext4InitialiseUnicodeCollationInternal ( + IN EFI_HANDLE DriverHandle, + IN EFI_GUID *ProtocolGuid, + IN CONST CHAR16 *VariableName, + IN CONST CHAR8 *DefaultLanguage + ) +{ + UINTN NumHandles; + EFI_HANDLE *Handles; + EFI_UNICODE_COLLATION_PROTOCOL *Uci; + BOOLEAN Iso639Language; + CHAR8 *Language; + EFI_STATUS RetStatus; + EFI_STATUS Status; + UINTN Idx; + CHAR8 *BestLanguage; + + Iso639Language = (BOOLEAN)(ProtocolGuid == &gEfiUnicodeCollationProtocolGuid); + RetStatus = EFI_UNSUPPORTED; + GetEfiGlobalVariable2 (VariableName, (VOID **)&Language, NULL); + + Status = gBS->LocateHandleBuffer ( + ByProtocol, + ProtocolGuid, + NULL, + &NumHandles, + &Handles + ); + if (EFI_ERROR (Status)) { + return Status; + } + + for (Idx = 0; Idx < NumHandles; Idx++) { + Status = gBS->OpenProtocol ( + Handles[Idx], + ProtocolGuid, + (VOID **)&Uci, + DriverHandle, + NULL, + EFI_OPEN_PROTOCOL_GET_PROTOCOL + ); + + if (EFI_ERROR (Status)) { + continue; + } + + BestLanguage = GetBestLanguage ( + Uci->SupportedLanguages, + Iso639Language, + (Language == NULL) ? "" : Language, + DefaultLanguage, + NULL + ); + if (BestLanguage != NULL) { + FreePool (BestLanguage); + gUnicodeCollationInterface = Uci; + RetStatus = EFI_SUCCESS; + break; + } + } + + if (Language != NULL) { + FreePool (Language); + } + + FreePool (Handles); + return RetStatus; +} + +/** + Initialises Unicode collation, which is needed for case-insensitive string comparisons + within the driver (a good example of an application of this is filename comparison). + + @param[in] DriverHandle Handle to the driver image. + + @retval EFI_SUCCESS Unicode collation was successfully initialised. + @retval !EFI_SUCCESS Failure. +**/ +EFI_STATUS +Ext4InitialiseUnicodeCollation ( + EFI_HANDLE DriverHandle + ) +{ + EFI_STATUS Status; + + Status = EFI_UNSUPPORTED; + + // + // First try to use RFC 4646 Unicode Collation 2 Protocol. + // + Status = Ext4InitialiseUnicodeCollationInternal ( + DriverHandle, + &gEfiUnicodeCollation2ProtocolGuid, + L"PlatformLang", + (CONST CHAR8 *)PcdGetPtr (PcdUefiVariableDefaultPlatformLang) + ); + // + // If the attempt to use Unicode Collation 2 Protocol fails, then we fall back + // on the ISO 639-2 Unicode Collation Protocol. + // + if (EFI_ERROR (Status)) { + Status = Ext4InitialiseUnicodeCollationInternal ( + DriverHandle, + &gEfiUnicodeCollationProtocolGuid, + L"Lang", + (CONST CHAR8 *)PcdGetPtr (PcdUefiVariableDefaultLang) + ); + } + + return Status; +} + +/** + Does a case-insensitive string comparison. Refer to EFI_UNICODE_COLLATION_PROTOCOL's StriColl + for more details. + + @param[in] Str1 Pointer to a null terminated string. + @param[in] Str2 Pointer to a null terminated string. + + @retval 0 Str1 is equivalent to Str2. + @retval >0 Str1 is lexically greater than Str2. + @retval <0 Str1 is lexically less than Str2. +**/ +INTN +Ext4StrCmpInsensitive ( + IN CHAR16 *Str1, + IN CHAR16 *Str2 + ) +{ + return gUnicodeCollationInterface->StriColl (gUnicodeCollationInterface, Str1, Str2); +} diff --git a/Ext4Pkg/Ext4Dxe/Directory.c b/Ext4Pkg/Ext4Dxe/Directory.c new file mode 100644 index 0000000000..4441e6d192 --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Directory.c @@ -0,0 +1,669 @@ +/** @file + Directory related routines + + Copyright (c) 2021 Pedro Falcato All rights reserved. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +#include + +/** + Retrieves the filename of the directory entry and converts it to UTF-16/UCS-2 + + @param[in] Entry Pointer to a EXT4_DIR_ENTRY. + @param[out] Ucs2FileName Pointer to an array of CHAR16's, of size EXT4_NAME_MAX + 1. + + @retval EFI_SUCCESS The filename was succesfully retrieved and converted to UCS2. + @retval !EFI_SUCCESS Failure. +**/ +EFI_STATUS +Ext4GetUcs2DirentName ( + IN EXT4_DIR_ENTRY *Entry, + OUT CHAR16 Ucs2FileName[EXT4_NAME_MAX + 1] + ) +{ + CHAR8 Utf8NameBuf[EXT4_NAME_MAX + 1]; + UINT16 *Str; + EFI_STATUS Status; + + CopyMem (Utf8NameBuf, Entry->name, Entry->name_len); + + Utf8NameBuf[Entry->name_len] = '\0'; + + // Unfortunately, BaseUcs2Utf8Lib doesn't have a convert-buffer-to-buffer-like + // function. Therefore, we need to allocate from the pool (inside UTF8StrToUCS2), + // copy it to our out buffer (Ucs2FileName) and free. + + Status = UTF8StrToUCS2 (Utf8NameBuf, &Str); + + if (EFI_ERROR (Status)) { + return Status; + } + + Status = StrCpyS (Ucs2FileName, EXT4_NAME_MAX + 1, Str); + + FreePool (Str); + + return Status; +} + +/** + Validates a directory entry. + + @param[in] Dirent Pointer to the directory entry. + + @retval TRUE Valid directory entry. + FALSE Invalid directory entry. +**/ +STATIC +BOOLEAN +Ext4ValidDirent ( + IN CONST EXT4_DIR_ENTRY *Dirent + ) +{ + UINTN RequiredSize; + + RequiredSize = Dirent->name_len + EXT4_MIN_DIR_ENTRY_LEN; + + if (Dirent->rec_len < RequiredSize) { + DEBUG ((DEBUG_ERROR, "[ext4] dirent size %lu too small (compared to %lu)\n", Dirent->rec_len, RequiredSize)); + return FALSE; + } + + // Dirent sizes need to be 4 byte aligned + if ((Dirent->rec_len % 4) != 0) { + return FALSE; + } + + return TRUE; +} + +/** + Retrieves a directory entry. + + @param[in] Directory Pointer to the opened directory. + @param[in] NameUnicode Pointer to the UCS-2 formatted filename. + @param[in] Partition Pointer to the ext4 partition. + @param[out] Result Pointer to the destination directory entry. + + @return The result of the operation. +**/ +EFI_STATUS +Ext4RetrieveDirent ( + IN EXT4_FILE *Directory, + IN CONST CHAR16 *Name, + IN EXT4_PARTITION *Partition, + OUT EXT4_DIR_ENTRY *Result + ) +{ + EFI_STATUS Status; + CHAR8 *Buf; + UINT64 Off; + EXT4_INODE *Inode; + UINT64 DirInoSize; + UINT32 BlockRemainder; + UINTN Length; + EXT4_DIR_ENTRY *Entry; + UINTN RemainingBlock; + CHAR16 DirentUcs2Name[EXT4_NAME_MAX + 1]; + UINTN ToCopy; + UINTN BlockOffset; + + Status = EFI_NOT_FOUND; + Buf = AllocatePool (Partition->BlockSize); + + if (Buf == NULL) { + return EFI_OUT_OF_RESOURCES; + } + + Off = 0; + + Inode = Directory->Inode; + DirInoSize = EXT4_INODE_SIZE (Inode); + + DivU64x32Remainder (DirInoSize, Partition->BlockSize, &BlockRemainder); + if (BlockRemainder != 0) { + // Directory inodes need to have block aligned sizes + return EFI_VOLUME_CORRUPTED; + } + + while (Off < DirInoSize) { + Length = Partition->BlockSize; + + Status = Ext4Read (Partition, Directory, Buf, Off, &Length); + + if (Status != EFI_SUCCESS) { + FreePool (Buf); + return Status; + } + + for (BlockOffset = 0; BlockOffset < Partition->BlockSize; ) { + Entry = (EXT4_DIR_ENTRY *)(Buf + BlockOffset); + RemainingBlock = Partition->BlockSize - BlockOffset; + // Check if the minimum directory entry fits inside [BlockOffset, EndOfBlock] + if (RemainingBlock < EXT4_MIN_DIR_ENTRY_LEN) { + FreePool (Buf); + return EFI_VOLUME_CORRUPTED; + } + + if (!Ext4ValidDirent (Entry)) { + FreePool (Buf); + return EFI_VOLUME_CORRUPTED; + } + + if ((Entry->name_len > RemainingBlock) || (Entry->rec_len > RemainingBlock)) { + // Corrupted filesystem + FreePool (Buf); + return EFI_VOLUME_CORRUPTED; + } + + // Unused entry + if (Entry->inode == 0) { + BlockOffset += Entry->rec_len; + continue; + } + + Status = Ext4GetUcs2DirentName (Entry, DirentUcs2Name); + + /* In theory, this should never fail. + * In reality, it's quite possible that it can fail, considering filenames in + * Linux (and probably other nixes) are just null-terminated bags of bytes, and don't + * need to form valid ASCII/UTF-8 sequences. + */ + if (EFI_ERROR (Status)) { + // If we error out, skip this entry + // I'm not sure if this is correct behaviour, but I don't think there's a precedent here. + BlockOffset += Entry->rec_len; + continue; + } + + if ((Entry->name_len == StrLen (Name)) && + !Ext4StrCmpInsensitive (DirentUcs2Name, (CHAR16 *)Name)) + { + ToCopy = MIN (Entry->rec_len, sizeof (EXT4_DIR_ENTRY)); + + CopyMem (Result, Entry, ToCopy); + FreePool (Buf); + return EFI_SUCCESS; + } + + BlockOffset += Entry->rec_len; + } + + Off += Partition->BlockSize; + } + + FreePool (Buf); + return EFI_NOT_FOUND; +} + +/** + Opens a file using a directory entry. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] OpenMode Mode in which the file is supposed to be open. + @param[out] OutFile Pointer to the newly opened file. + @param[in] Entry Directory entry to be used. + @param[in] Directory Pointer to the opened directory. + + @retval EFI_STATUS Result of the operation +**/ +EFI_STATUS +Ext4OpenDirent ( + IN EXT4_PARTITION *Partition, + IN UINT64 OpenMode, + OUT EXT4_FILE **OutFile, + IN EXT4_DIR_ENTRY *Entry, + IN EXT4_FILE *Directory + ) +{ + EFI_STATUS Status; + CHAR16 FileName[EXT4_NAME_MAX + 1]; + EXT4_FILE *File; + + File = AllocateZeroPool (sizeof (EXT4_FILE)); + + if (File == NULL) { + Status = EFI_OUT_OF_RESOURCES; + goto Error; + } + + Status = Ext4GetUcs2DirentName (Entry, FileName); + + if (EFI_ERROR (Status)) { + goto Error; + } + + if (StrCmp (FileName, L".") == 0) { + // We're using the parent directory's dentry + File->Dentry = Directory->Dentry; + + ASSERT (File->Dentry != NULL); + + Ext4RefDentry (File->Dentry); + } else if (StrCmp (FileName, L"..") == 0) { + // Using the parent's parent's dentry + File->Dentry = Directory->Dentry->Parent; + + ASSERT (File->Dentry != NULL); + + Ext4RefDentry (File->Dentry); + } else { + File->Dentry = Ext4CreateDentry (FileName, Directory->Dentry); + + if (!File->Dentry) { + goto Error; + } + } + + Status = Ext4InitExtentsMap (File); + + if (EFI_ERROR (Status)) { + goto Error; + } + + File->InodeNum = Entry->inode; + + Ext4SetupFile (File, Partition); + + Status = Ext4ReadInode (Partition, Entry->inode, &File->Inode); + + if (EFI_ERROR (Status)) { + goto Error; + } + + *OutFile = File; + + InsertTailList (&Partition->OpenFiles, &File->OpenFilesListNode); + + return EFI_SUCCESS; + +Error: + if (File != NULL) { + if (File->Dentry != NULL) { + Ext4UnrefDentry (File->Dentry); + } + + if (File->ExtentsMap != NULL) { + OrderedCollectionUninit (File->ExtentsMap); + } + + FreePool (File); + } + + return Status; +} + +/** + Opens a file. + + @param[in] Directory Pointer to the opened directory. + @param[in] Name Pointer to the UCS-2 formatted filename. + @param[in] Partition Pointer to the ext4 partition. + @param[in] OpenMode Mode in which the file is supposed to be open. + @param[out] OutFile Pointer to the newly opened file. + + @return Result of the operation. +**/ +EFI_STATUS +Ext4OpenFile ( + IN EXT4_FILE *Directory, + IN CONST CHAR16 *Name, + IN EXT4_PARTITION *Partition, + IN UINT64 OpenMode, + OUT EXT4_FILE **OutFile + ) +{ + EXT4_DIR_ENTRY Entry; + EFI_STATUS Status; + + Status = Ext4RetrieveDirent (Directory, Name, Partition, &Entry); + + if (EFI_ERROR (Status)) { + return Status; + } + + // EFI requires us to error out on ".." opens for the root directory + if (Entry.inode == Directory->InodeNum) { + return EFI_NOT_FOUND; + } + + return Ext4OpenDirent (Partition, OpenMode, OutFile, &Entry, Directory); +} + +/** + Open the root directory on a volume. + + @param[in] This A pointer to the volume to open the root directory. + @param[out] Root A pointer to the location to return the opened file handle for the + root directory. + + @retval EFI_SUCCESS The device was opened. + @retval EFI_UNSUPPORTED This volume does not support the requested file system type. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_ACCESS_DENIED The service denied access to the file. + @retval EFI_OUT_OF_RESOURCES The volume was not opened due to lack of resources. + @retval EFI_MEDIA_CHANGED The device has a different medium in it or the medium is no + longer supported. Any existing file handles for this volume are + no longer valid. To access the files on the new medium, the + volume must be reopened with OpenVolume(). + +**/ +EFI_STATUS +EFIAPI +Ext4OpenVolume ( + IN EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This, + OUT EFI_FILE_PROTOCOL **Root + ) +{ + EXT4_INODE *RootInode; + EFI_STATUS Status; + EXT4_FILE *RootDir; + EXT4_PARTITION *Partition; + + Partition = (EXT4_PARTITION *)This; + + Status = Ext4ReadInode (Partition, EXT4_ROOT_INODE_NR, &RootInode); + + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "[ext4] Could not open root inode - error %r\n", Status)); + return Status; + } + + RootDir = AllocateZeroPool (sizeof (EXT4_FILE)); + + if (RootDir == NULL) { + FreePool (RootInode); + return EFI_OUT_OF_RESOURCES; + } + + RootDir->Inode = RootInode; + RootDir->InodeNum = EXT4_ROOT_INODE_NR; + + Status = Ext4InitExtentsMap (RootDir); + + if (EFI_ERROR (Status)) { + FreePool (RootInode); + FreePool (RootDir); + return EFI_OUT_OF_RESOURCES; + } + + Ext4SetupFile (RootDir, Partition); + *Root = &RootDir->Protocol; + + InsertTailList (&Partition->OpenFiles, &RootDir->OpenFilesListNode); + + ASSERT (Partition->RootDentry != NULL); + RootDir->Dentry = Partition->RootDentry; + + Ext4RefDentry (RootDir->Dentry); + + return EFI_SUCCESS; +} + +/** + Reads a directory entry. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] File Pointer to the open directory. + @param[out] Buffer Pointer to the output buffer. + @param[in] Offset Initial directory position. + @param[in out] OutLength Pointer to a UINTN that contains the length of the buffer, + and the length of the actual EFI_FILE_INFO after the call. + + @return Result of the operation. +**/ +EFI_STATUS +Ext4ReadDir ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT VOID *Buffer, + IN UINT64 Offset, + IN OUT UINTN *OutLength + ) +{ + EXT4_INODE *DirIno; + EFI_STATUS Status; + UINT64 DirInoSize; + UINTN Len; + UINT32 BlockRemainder; + EXT4_DIR_ENTRY Entry; + EXT4_FILE *TempFile; + BOOLEAN ShouldSkip; + BOOLEAN IsDotOrDotDot; + + DirIno = File->Inode; + Status = EFI_SUCCESS; + DirInoSize = EXT4_INODE_SIZE (DirIno); + + DivU64x32Remainder (DirInoSize, Partition->BlockSize, &BlockRemainder); + if (BlockRemainder != 0) { + // Directory inodes need to have block aligned sizes + return EFI_VOLUME_CORRUPTED; + } + + while (TRUE) { + TempFile = NULL; + + // We (try to) read the maximum size of a directory entry at a time + // Note that we don't need to read any padding that may exist after it. + Len = sizeof (Entry); + Status = Ext4Read (Partition, File, &Entry, Offset, &Len); + + if (EFI_ERROR (Status)) { + goto Out; + } + + if (Len == 0) { + *OutLength = 0; + Status = EFI_SUCCESS; + goto Out; + } + + if (Len < EXT4_MIN_DIR_ENTRY_LEN) { + Status = EFI_VOLUME_CORRUPTED; + goto Out; + } + + // Invalid directory entry length + if (!Ext4ValidDirent (&Entry)) { + DEBUG ((DEBUG_ERROR, "[ext4] Invalid dirent at offset %lu\n", Offset)); + Status = EFI_VOLUME_CORRUPTED; + goto Out; + } + + // Check if the entire dir entry length fits in Len + if (Len < (UINTN)(EXT4_MIN_DIR_ENTRY_LEN + Entry.name_len)) { + Status = EFI_VOLUME_CORRUPTED; + goto Out; + } + + // We don't care about passing . or .. entries to the caller of ReadDir(), + // since they're generally useless entries *and* may break things if too + // many callers assume FAT32. + + // Entry.name_len may be 0 if it's a nameless entry, like an unused entry + // or a checksum at the end of the directory block. + // memcmp (and CompareMem) return 0 when the passed length is 0. + + IsDotOrDotDot = Entry.name_len != 0 && + (CompareMem (Entry.name, ".", Entry.name_len) == 0 || + CompareMem (Entry.name, "..", Entry.name_len) == 0); + + // When inode = 0, it's unused. + ShouldSkip = Entry.inode == 0 || IsDotOrDotDot; + + if (ShouldSkip) { + Offset += Entry.rec_len; + continue; + } + + Status = Ext4OpenDirent (Partition, EFI_FILE_MODE_READ, &TempFile, &Entry, File); + + if (EFI_ERROR (Status)) { + goto Out; + } + + Status = Ext4GetFileInfo (TempFile, Buffer, OutLength); + if (!EFI_ERROR (Status)) { + File->Position = Offset + Entry.rec_len; + } + + Ext4CloseInternal (TempFile); + + goto Out; + } + + Status = EFI_SUCCESS; +Out: + return Status; +} + +/** + Removes a dentry from the other's list. + + @param[in out] Parent Pointer to the parent EXT4_DENTRY. + @param[in out] ToBeRemoved Pointer to the child EXT4_DENTRY. +**/ +STATIC +VOID +Ext4RemoveDentry ( + IN OUT EXT4_DENTRY *Parent, + IN OUT EXT4_DENTRY *ToBeRemoved + ) +{ + ASSERT (IsNodeInList (&ToBeRemoved->ListNode, &Parent->Children)); + RemoveEntryList (&ToBeRemoved->ListNode); +} + +/** + Adds a dentry to the other's list. + + The dentry that is added to the other one's list gets ->Parent set to Parent, + and the parent gets its reference count incremented. + + @param[in out] Parent Pointer to the parent EXT4_DENTRY. + @param[in out] ToBeAdded Pointer to the child EXT4_DENTRY. +**/ +STATIC +VOID +Ext4AddDentry ( + IN OUT EXT4_DENTRY *Parent, + IN OUT EXT4_DENTRY *ToBeAdded + ) +{ + ToBeAdded->Parent = Parent; + InsertTailList (&Parent->Children, &ToBeAdded->ListNode); + Ext4RefDentry (Parent); +} + +/** + Creates a new dentry object. + + @param[in] Name Name of the dentry. + @param[in out opt] Parent Parent dentry, if it's not NULL. + + @return The new allocated and initialised dentry. + The ref count will be set to 1. +**/ +EXT4_DENTRY * +Ext4CreateDentry ( + IN CONST CHAR16 *Name, + IN OUT EXT4_DENTRY *Parent OPTIONAL + ) +{ + EXT4_DENTRY *Dentry; + EFI_STATUS Status; + + Dentry = AllocateZeroPool (sizeof (EXT4_DENTRY)); + + if (Dentry == NULL) { + return NULL; + } + + Dentry->RefCount = 1; + + // This StrCpyS should not fail. + Status = StrCpyS (Dentry->Name, ARRAY_SIZE (Dentry->Name), Name); + + ASSERT_EFI_ERROR (Status); + + InitializeListHead (&Dentry->Children); + + if (Parent != NULL) { + Ext4AddDentry (Parent, Dentry); + } + + DEBUG ((DEBUG_FS, "[ext4] Created dentry %s\n", Name)); + + return Dentry; +} + +/** + Increments the ref count of the dentry. + + @param[in out] Dentry Pointer to a valid EXT4_DENTRY. +**/ +VOID +Ext4RefDentry ( + IN OUT EXT4_DENTRY *Dentry + ) +{ + UINTN OldRef; + + OldRef = Dentry->RefCount; + + Dentry->RefCount++; + + // I'm not sure if this (Refcount overflow) is a valid concern, + // but it's better to be safe than sorry. + ASSERT (OldRef < Dentry->RefCount); +} + +/** + Deletes the dentry. + + @param[in out] Dentry Pointer to a valid EXT4_DENTRY. +**/ +STATIC +VOID +Ext4DeleteDentry ( + IN OUT EXT4_DENTRY *Dentry + ) +{ + if (Dentry->Parent) { + Ext4RemoveDentry (Dentry->Parent, Dentry); + Ext4UnrefDentry (Dentry->Parent); + } + + DEBUG ((DEBUG_FS, "[ext4] Deleted dentry %s\n", Dentry->Name)); + FreePool (Dentry); +} + +/** + Decrements the ref count of the dentry. + If the ref count is 0, it's destroyed. + + @param[in out] Dentry Pointer to a valid EXT4_DENTRY. + + @retval True if it was destroyed, false if it's alive. +**/ +BOOLEAN +Ext4UnrefDentry ( + IN OUT EXT4_DENTRY *Dentry + ) +{ + Dentry->RefCount--; + + if (Dentry->RefCount == 0) { + Ext4DeleteDentry (Dentry); + return TRUE; + } + + return FALSE; +} diff --git a/Ext4Pkg/Ext4Dxe/DiskUtil.c b/Ext4Pkg/Ext4Dxe/DiskUtil.c new file mode 100644 index 0000000000..32da35f7d9 --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/DiskUtil.c @@ -0,0 +1,113 @@ +/** @file + Disk utilities + + Copyright (c) 2021 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ +#include "Ext4Dxe.h" + +/** + Reads from the partition's disk using the DISK_IO protocol. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[out] Buffer Pointer to a destination buffer. + @param[in] Length Length of the destination buffer. + @param[in] Offset Offset, in bytes, of the location to read. + + @return Success status of the disk read. +**/ +EFI_STATUS +Ext4ReadDiskIo ( + IN EXT4_PARTITION *Partition, + OUT VOID *Buffer, + IN UINTN Length, + IN UINT64 Offset + ) +{ + return EXT4_DISK_IO (Partition)->ReadDisk ( + EXT4_DISK_IO (Partition), + EXT4_MEDIA_ID (Partition), + Offset, + Length, + Buffer + ); +} + +/** + Reads blocks from the partition's disk using the DISK_IO protocol. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[out] Buffer Pointer to a destination buffer. + @param[in] NumberBlocks Length of the read, in filesystem blocks. + @param[in] BlockNumber Starting block number. + + @return Success status of the read. +**/ +EFI_STATUS +Ext4ReadBlocks ( + IN EXT4_PARTITION *Partition, + OUT VOID *Buffer, + IN UINTN NumberBlocks, + IN EXT4_BLOCK_NR BlockNumber + ) +{ + UINT64 Offset; + UINTN Length; + + Offset = MultU64x32 (BlockNumber, Partition->BlockSize); + Length = NumberBlocks * Partition->BlockSize; + + // Check for overflow on the block -> byte conversions. + // Partition->BlockSize is never 0, so we don't need to check for that. + + if (Offset > DivU64x32 ((UINT64)-1, Partition->BlockSize)) { + return EFI_INVALID_PARAMETER; + } + + if (Length > (UINTN)-1/Partition->BlockSize) { + return EFI_INVALID_PARAMETER; + } + + return Ext4ReadDiskIo (Partition, Buffer, Length, Offset); +} + +/** + Allocates a buffer and reads blocks from the partition's disk using the DISK_IO protocol. + This function is deprecated and will be removed in the future. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[in] NumberBlocks Length of the read, in filesystem blocks. + @param[in] BlockNumber Starting block number. + + @return Buffer allocated by AllocatePool, or NULL if some part of the process + failed. +**/ +VOID * +Ext4AllocAndReadBlocks ( + IN EXT4_PARTITION *Partition, + IN UINTN NumberBlocks, + IN EXT4_BLOCK_NR BlockNumber + ) +{ + VOID *Buf; + UINTN Length; + + Length = NumberBlocks * Partition->BlockSize; + + if (Length > (UINTN)-1/Partition->BlockSize) { + return NULL; + } + + Buf = AllocatePool (Length); + + if (Buf == NULL) { + return NULL; + } + + if (Ext4ReadBlocks (Partition, Buf, NumberBlocks, BlockNumber) != EFI_SUCCESS) { + FreePool (Buf); + return NULL; + } + + return Buf; +} diff --git a/Ext4Pkg/Ext4Dxe/Ext4Disk.h b/Ext4Pkg/Ext4Dxe/Ext4Disk.h new file mode 100644 index 0000000000..4fd91a4233 --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Ext4Disk.h @@ -0,0 +1,475 @@ +/** @file + Raw filesystem data structures + + Copyright (c) 2021 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent + + Layout of an EXT2/3/4 filesystem: + (note: this driver has been developed using + https://www.kernel.org/doc/html/latest/filesystems/ext4/index.html as + documentation). + + An ext2/3/4 filesystem (here on out referred to as simply an ext4 filesystem, + due to the similarities) is composed of various concepts: + + 1) Superblock + The superblock is the structure near (1024 bytes offset from the start) + the start of the partition, and describes the filesystem in general. + Here, we get to know the size of the filesystem's blocks, which features + it supports or not, whether it's been cleanly unmounted, how many blocks + we have, etc. + + 2) Block groups + EXT4 filesystems are divided into block groups, and each block group covers + s_blocks_per_group(8 * Block Size) blocks. Each block group has an + associated block group descriptor; these are present directly after the + superblock. Each block group descriptor contains the location of the + inode table, and the inode and block bitmaps (note these bitmaps are only + a block long, which gets us the 8 * Block Size formula covered previously). + + 3) Blocks + The ext4 filesystem is divided in blocks, of size s_log_block_size ^ 1024. + Blocks can be allocated using individual block groups's bitmaps. Note + that block 0 is invalid and its presence on extents/block tables means + it's part of a file hole, and that particular location must be read as + a block full of zeros. + + 4) Inodes + The ext4 filesystem divides files/directories into inodes (originally + index nodes). Each file/socket/symlink/directory/etc (here on out referred + to as a file, since there is no distinction under the ext4 filesystem) is + stored as a /nameless/ inode, that is stored in some block group's inode + table. Each inode has s_inode_size size (or GOOD_OLD_INODE_SIZE if it's + an old filesystem), and holds various metadata about the file. Since the + largest inode structure right now is ~160 bytes, the rest of the inode + contains inline extended attributes. Inodes' data is stored using either + data blocks (under ext2/3) or extents (under ext4). + + 5) Extents + Ext4 inodes store data in extents. These let N contiguous logical blocks + that are represented by N contiguous physical blocks be represented by a + single extent structure, which minimizes filesystem metadata bloat and + speeds up block mapping (particularly due to the fact that high-quality + ext4 implementations like linux's try /really/ hard to make the file + contiguous, so it's common to have files with almost 0 fragmentation). + Inodes that use extents store them in a tree, and the top of the tree + is stored on i_data. The tree's leaves always start with an + EXT4_EXTENT_HEADER and contain EXT4_EXTENT_INDEX on eh_depth != 0 and + EXT4_EXTENT on eh_depth = 0; these entries are always sorted by logical + block. + + 6) Directories + Ext4 directories are files that store name -> inode mappings for the + logical directory; this is where files get their names, which means ext4 + inodes do not themselves have names, since they can be linked (present) + multiple times with different names. Directories can store entries in two + different ways: + 1) Classical linear directories: They store entries as a mostly-linked + mostly-list of EXT4_DIR_ENTRY. + 2) Hash tree directories: These are used for larger directories, with + hundreds of entries, and are designed in a backwards compatible way. + These are not yet implemented in the Ext4Dxe driver. + + 7) Journal + Ext3/4 filesystems have a journal to help protect the filesystem against + system crashes. This is not yet implemented in Ext4Dxe but is described + in detail in the Linux kernel's documentation. +**/ + +#ifndef EXT4_DISK_H_ +#define EXT4_DISK_H_ + +#include + +#define EXT4_SUPERBLOCK_OFFSET 1024U + +#define EXT4_SIGNATURE 0xEF53U + +#define EXT4_FS_STATE_UNMOUNTED 0x1 +#define EXT4_FS_STATE_ERRORS_DETECTED 0x2 +#define EXT4_FS_STATE_RECOVERING_ORPHANS 0x4 + +#define EXT4_ERRORS_CONTINUE 1 +#define EXT4_ERRORS_RO 2 +#define EXT4_ERRORS_PANIC 3 + +#define EXT4_LINUX_ID 0 +#define EXT4_GNU_HURD_ID 1 +#define EXT4_MASIX_ID 2 +#define EXT4_FREEBSD_ID 3 +#define EXT4_LITES_ID 4 + +#define EXT4_GOOD_OLD_REV 0 +#define EXT4_DYNAMIC_REV 1 + +#define EXT4_CHECKSUM_CRC32C 0x1 + +#define EXT4_FEATURE_COMPAT_DIR_PREALLOC 0x01 +#define EXT4_FEATURE_COMPAT_IMAGIC_INODES 0x02 +#define EXT3_FEATURE_COMPAT_HAS_JOURNAL 0x04 +#define EXT4_FEATURE_COMPAT_EXT_ATTR 0x08 +#define EXT4_FEATURE_COMPAT_RESIZE_INO 0x10 +#define EXT4_FEATURE_COMPAT_DIR_INDEX 0x20 + +#define EXT4_FEATURE_INCOMPAT_COMPRESSION 0x00001 +#define EXT4_FEATURE_INCOMPAT_FILETYPE 0x00002 +#define EXT4_FEATURE_INCOMPAT_RECOVER 0x00004 +#define EXT4_FEATURE_INCOMPAT_JOURNAL_DEV 0x00008 +#define EXT4_FEATURE_INCOMPAT_META_BG 0x00010 +#define EXT4_FEATURE_INCOMPAT_EXTENTS 0x00040 +#define EXT4_FEATURE_INCOMPAT_64BIT 0x00080 +#define EXT4_FEATURE_INCOMPAT_MMP 0x00100 +#define EXT4_FEATURE_INCOMPAT_FLEX_BG 0x00200 +#define EXT4_FEATURE_INCOMPAT_EA_INODE 0x00400 +// It's not clear whether or not this feature (below) is used right now +#define EXT4_FEATURE_INCOMPAT_DIRDATA 0x01000 +#define EXT4_FEATURE_INCOMPAT_CSUM_SEED 0x02000 +#define EXT4_FEATURE_INCOMPAT_LARGEDIR 0x04000 +#define EXT4_FEATURE_INCOMPAT_INLINE_DATA 0x08000 +#define EXT4_FEATURE_INCOMPAT_ENCRYPT 0x10000 + +#define EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER 0x0001 +#define EXT4_FEATURE_RO_COMPAT_LARGE_FILE 0x0002 +#define EXT4_FEATURE_RO_COMPAT_BTREE_DIR 0x0004// Unused +#define EXT4_FEATURE_RO_COMPAT_HUGE_FILE 0x0008 +#define EXT4_FEATURE_RO_COMPAT_GDT_CSUM 0x0010 +#define EXT4_FEATURE_RO_COMPAT_DIR_NLINK 0x0020 +#define EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE 0x0040 +#define EXT4_FEATURE_RO_COMPAT_HAS_SNAPSHOT 0x0080// Not implemented in ext4 +#define EXT4_FEATURE_RO_COMPAT_QUOTA 0x0100 +#define EXT4_FEATURE_RO_COMPAT_BIGALLOC 0x0200 +#define EXT4_FEATURE_RO_COMPAT_METADATA_CSUM 0x0400 +#define EXT4_FEATURE_RO_COMPAT_REPLICA 0x0800// Not used + +// We explicitly don't recognise this, so we get read only. +#define EXT4_FEATURE_RO_COMPAT_READONLY 0x1000 +#define EXT4_FEATURE_RO_COMPAT_PROJECT 0x2000 + +/* Important notes about the features + * Absolutely needed features: + * 1) Every incompat, because we might want to mount root filesystems + * 2) Relevant RO_COMPATs(I'm not sure of what to do wrt quota, project) + **/ + +#define EXT4_INO_TYPE_FIFO 0x1000 +#define EXT4_INO_TYPE_CHARDEV 0x2000 +#define EXT4_INO_TYPE_DIR 0x4000 +#define EXT4_INO_TYPE_BLOCKDEV 0x6000 +#define EXT4_INO_TYPE_REGFILE 0x8000 +#define EXT4_INO_TYPE_SYMLINK 0xA000 +#define EXT4_INO_TYPE_UNIX_SOCK 0xC000 + +/* Inode flags */ +#define EXT4_SECRM_FL 0x00000001 +#define EXT4_UNRM_FL 0x00000002 +#define EXT4_COMPR_FL 0x00000004 +#define EXT4_SYNC_FL 0x00000008 +#define EXT4_IMMUTABLE_FL 0x00000010 +#define EXT4_APPEND_FL 0x00000020 +#define EXT4_NODUMP_FL 0x00000040 +#define EXT4_NOATIME_FL 0x00000080 +#define EXT4_DIRTY_FL 0x00000100 +#define EXT4_COMPRBLK_FL 0x00000200 +#define EXT4_NOCOMPR_FL 0x00000400 +#define EXT4_ENCRYPT_FL 0x00000800 +#define EXT4_BTREE_FL 0x00001000 +#define EXT4_INDEX_FL 0x00002000 +#define EXT4_JOURNAL_DATA_FL 0x00004000 +#define EXT4_NOTAIL_FL 0x00008000 +#define EXT4_DIRSYNC_FL 0x00010000 +#define EXT4_TOPDIR_FL 0x00020000 +#define EXT4_HUGE_FILE_FL 0x00040000 +#define EXT4_EXTENTS_FL 0x00080000 +#define EXT4_VERITY_FL 0x00100000 +#define EXT4_EA_INODE_FL 0x00200000 +#define EXT4_RESERVED_FL 0x80000000 + +/* File type flags that are stored in the directory entries */ +#define EXT4_FT_UNKNOWN 0 +#define EXT4_FT_REG_FILE 1 +#define EXT4_FT_DIR 2 +#define EXT4_FT_CHRDEV 3 +#define EXT4_FT_BLKDEV 4 +#define EXT4_FT_FIFO 5 +#define EXT4_FT_SOCK 6 +#define EXT4_FT_SYMLINK 7 + +typedef struct { + UINT32 s_inodes_count; + UINT32 s_blocks_count; + UINT32 s_r_blocks_count; + UINT32 s_free_blocks_count; + UINT32 s_free_inodes_count; + UINT32 s_first_data_block; + UINT32 s_log_block_size; + UINT32 s_log_frag_size; + UINT32 s_blocks_per_group; + UINT32 s_frags_per_group; + UINT32 s_inodes_per_group; + UINT32 s_mtime; + UINT32 s_wtime; + UINT16 s_mnt_count; + UINT16 s_max_mnt_count; + UINT16 s_magic; + UINT16 s_state; + UINT16 s_errors; + UINT16 s_minor_rev_level; + UINT32 s_lastcheck; + UINT32 s_check_interval; + UINT32 s_creator_os; + UINT32 s_rev_level; + UINT16 s_def_resuid; + UINT16 s_def_resgid; + + /* Every field after this comment is revision >= 1 */ + + UINT32 s_first_ino; + UINT16 s_inode_size; + UINT16 s_block_group_nr; + UINT32 s_feature_compat; + UINT32 s_feature_incompat; + UINT32 s_feature_ro_compat; + UINT8 s_uuid[16]; + UINT8 s_volume_name[16]; + UINT8 s_last_mounted[64]; + UINT32 s_algo_bitmap; + UINT8 s_prealloc_blocks; + UINT8 s_prealloc_dir_blocks; + UINT16 unused; + UINT8 s_journal_uuid[16]; + UINT32 s_journal_inum; + UINT32 s_journal_dev; + UINT32 s_last_orphan; + UINT32 s_hash_seed[4]; + UINT8 s_def_hash_version; + UINT8 s_jnl_backup_type; + UINT16 s_desc_size; + UINT32 s_default_mount_options; + UINT32 s_first_meta_bg; + UINT32 s_mkfs_time; + UINT32 s_jnl_blocks[17]; + UINT32 s_blocks_count_hi; + UINT32 s_r_blocks_count_hi; + UINT32 s_free_blocks_count_hi; + UINT16 s_min_extra_isize; + UINT16 s_want_extra_isize; + UINT32 s_flags; + UINT16 s_raid_stride; + UINT16 s_mmp_interval; + UINT64 s_mmp_block; + UINT32 s_raid_stride_width; + UINT8 s_log_groups_per_flex; + UINT8 s_checksum_type; // Only valid value is 1 - CRC32C + UINT16 s_reserved_pad; + UINT64 s_kbytes_written; + + // Snapshot stuff isn't used in Linux and isn't implemented here + UINT32 s_snapshot_inum; + UINT32 s_snapshot_id; + UINT64 s_snapshot_r_blocks_count; + UINT32 s_snapshot_list; + UINT32 s_error_count; + UINT32 s_first_error_time; + UINT32 s_first_error_ino; + UINT64 s_first_error_block; + UINT8 s_first_error_func[32]; + UINT32 s_first_error_line; + UINT32 s_last_error_time; + UINT32 s_last_error_ino; + UINT32 s_last_error_line; + UINT64 s_last_error_block; + UINT8 s_last_error_func[32]; + UINT8 s_mount_opts[64]; + UINT32 s_usr_quota_inum; + UINT32 s_grp_quota_inum; + UINT32 s_overhead_blocks; + UINT32 s_backup_bgs[2]; // sparse_super2 + UINT8 s_encrypt_algos[4]; + UINT8 s_encrypt_pw_salt[16]; + UINT32 s_lpf_ino; + UINT32 s_prj_quota_inum; + UINT32 s_checksum_seed; + UINT32 s_reserved[98]; + UINT32 s_checksum; +} EXT4_SUPERBLOCK; + +STATIC_ASSERT ( + sizeof (EXT4_SUPERBLOCK) == 1024, + "ext4 superblock struct has incorrect size" + ); + +typedef struct { + UINT32 bg_block_bitmap_lo; + UINT32 bg_inode_bitmap_lo; + UINT32 bg_inode_table_lo; + UINT16 bg_free_blocks_count_lo; + UINT16 bg_free_inodes_count_lo; + UINT16 bg_used_dirs_count_lo; + UINT16 bg_flags; + UINT32 bg_exclude_bitmap_lo; + UINT16 bg_block_bitmap_csum_lo; + UINT16 bg_inode_bitmap_csum_lo; + UINT16 bg_itable_unused_lo; + UINT16 bg_checksum; + UINT32 bg_block_bitmap_hi; + UINT32 bg_inode_bitmap_hi; + UINT32 bg_inode_table_hi; + UINT16 bg_free_blocks_count_hi; + UINT16 bg_free_inodes_count_hi; + UINT16 bg_used_dirs_count_hi; + UINT16 bg_itable_unused_hi; + UINT32 bg_exclude_bitmap_hi; + UINT16 bg_block_bitmap_csum_hi; + UINT16 bg_inode_bitmap_csum_hi; + UINT32 bg_reserved; +} EXT4_BLOCK_GROUP_DESC; + +#define EXT4_OLD_BLOCK_DESC_SIZE 32 +#define EXT4_64BIT_BLOCK_DESC_SIZE 64 + +STATIC_ASSERT ( + sizeof (EXT4_BLOCK_GROUP_DESC) == EXT4_64BIT_BLOCK_DESC_SIZE, + "ext4 block group descriptor struct has incorrect size" + ); + +#define EXT4_DBLOCKS 12 +#define EXT4_IND_BLOCK 12 +#define EXT4_DIND_BLOCK 13 +#define EXT4_TIND_BLOCK 14 +#define EXT4_NR_BLOCKS 15 +#define EXT4_FAST_SYMLINK_MAX_SIZE EXT4_NR_BLOCKS * sizeof(UINT32) + +#define EXT4_GOOD_OLD_INODE_SIZE 128U + +typedef struct _Ext4_I_OSD2_Linux { + UINT16 l_i_blocks_high; + UINT16 l_i_file_acl_high; + UINT16 l_i_uid_high; + UINT16 l_i_gid_high; + UINT16 l_i_checksum_lo; + UINT16 l_i_reserved; +} EXT4_OSD2_LINUX; + +typedef struct _Ext4_I_OSD2_Hurd { + UINT16 h_i_reserved1; + UINT16 h_i_mode_high; + UINT16 h_i_uid_high; + UINT16 h_i_gid_high; + UINT32 h_i_author; +} EXT4_OSD2_HURD; + +typedef union { + // Note: Toolchain-specific defines (such as "linux") stops us from using + // simpler names down here. + EXT4_OSD2_LINUX data_linux; + EXT4_OSD2_HURD data_hurd; +} EXT4_OSD2; + +typedef struct _Ext4Inode { + UINT16 i_mode; + UINT16 i_uid; + UINT32 i_size_lo; + UINT32 i_atime; + UINT32 i_ctime; + UINT32 i_mtime; + UINT32 i_dtime; + UINT16 i_gid; + UINT16 i_links; + UINT32 i_blocks; + UINT32 i_flags; + UINT32 i_os_spec; + UINT32 i_data[EXT4_NR_BLOCKS]; + UINT32 i_generation; + UINT32 i_file_acl; + UINT32 i_size_hi; + UINT32 i_faddr; + + EXT4_OSD2 i_osd2; + + UINT16 i_extra_isize; + UINT16 i_checksum_hi; + UINT32 i_ctime_extra; + UINT32 i_mtime_extra; + UINT32 i_atime_extra; + UINT32 i_crtime; + UINT32 i_crtime_extra; + UINT32 i_version_hi; + UINT32 i_projid; +} EXT4_INODE; + +typedef struct { + UINT32 inode; + UINT16 rec_len; + UINT8 name_len; + UINT8 file_type; + CHAR8 name[255]; +} EXT4_DIR_ENTRY; + +#define EXT4_MIN_DIR_ENTRY_LEN 8 + +// This on-disk structure is present at the bottom of the extent tree +typedef struct { + // First logical block + UINT32 ee_block; + // Length of the extent, in blocks + UINT16 ee_len; + // The physical (filesystem-relative) block is split between the high 16 bits + // and the low 32 bits - this forms a 48-bit block number + UINT16 ee_start_hi; + UINT32 ee_start_lo; +} EXT4_EXTENT; + +// This on-disk structure is present at all levels except the bottom +typedef struct { + // This index covers logical blocks from 'ei_block' + UINT32 ei_block; + // Block of the next level of the extent tree, similarly split in a high and + // low portion. + UINT32 ei_leaf_lo; + UINT16 ei_leaf_hi; + + UINT16 ei_unused; +} EXT4_EXTENT_INDEX; + +typedef struct { + // Needs to be EXT4_EXTENT_HEADER_MAGIC + UINT16 eh_magic; + // Number of entries + UINT16 eh_entries; + // Maximum number of entries that could follow this header + UINT16 eh_max; + // Depth of this node in the tree - the tree can be at most 5 levels deep + UINT16 eh_depth; + // Unused by standard ext4 + UINT32 eh_generation; +} EXT4_EXTENT_HEADER; + +#define EXT4_EXTENT_HEADER_MAGIC 0xF30A + +// Specified by ext4 docs and backed by a bunch of math +#define EXT4_EXTENT_TREE_MAX_DEPTH 5 + +typedef struct { + // CRC32C of UUID + inode number + igeneration + extent block + UINT32 eb_checksum; +} EXT4_EXTENT_TAIL; + +/** + * EXT4 has this feature called uninitialized extents: + * An extent has a maximum of 32768 blocks (2^15 or 1 << 15). + * When we find an extent with > 32768 blocks, this extent is called + * uninitialized. Long story short, it's an extent that behaves as a file hole + * but has blocks already allocated. + */ +#define EXT4_EXTENT_MAX_INITIALIZED (1 << 15) + +typedef UINT64 EXT4_BLOCK_NR; +typedef UINT32 EXT2_BLOCK_NR; +typedef UINT32 EXT4_INO_NR; + +// 2 is always the root inode number in ext4 +#define EXT4_ROOT_INODE_NR 2 + +#define EXT4_BLOCK_FILE_HOLE 0 + +#endif diff --git a/Ext4Pkg/Ext4Dxe/Ext4Dxe.c b/Ext4Pkg/Ext4Dxe/Ext4Dxe.c new file mode 100644 index 0000000000..2a4f5a7bd0 --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Ext4Dxe.c @@ -0,0 +1,844 @@ +/** @file + Driver entry point + + Copyright (c) 2021 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +GLOBAL_REMOVE_IF_UNREFERENCED EFI_UNICODE_STRING_TABLE mExt4DriverNameTable[] = { + { + "eng;en", + L"Ext4 File System Driver" + }, + { + NULL, + NULL + } +}; + +GLOBAL_REMOVE_IF_UNREFERENCED EFI_UNICODE_STRING_TABLE mExt4ControllerNameTable[] = { + { + "eng;en", + L"Ext4 File System" + }, + { + NULL, + NULL + } +}; + +// Needed by gExt4ComponentName* + +/** + Retrieves a Unicode string that is the user-readable name of the EFI Driver. + + @param[in] This A pointer to the EFI_COMPONENT_NAME_PROTOCOL instance. + @param[in] Language A pointer to a three-character ISO 639-2 language identifier. + This is the language of the driver name that that the caller + is requesting, and it must match one of the languages specified + in SupportedLanguages. The number of languages supported by a + driver is up to the driver writer. + @param[out] DriverName A pointer to the Unicode string to return. This Unicode string + is the name of the driver specified by This in the language + specified by Language. + + @retval EFI_SUCCESS The Unicode string for the Driver specified by This + and the language specified by Language was returned + in DriverName. + @retval EFI_INVALID_PARAMETER Language is NULL. + @retval EFI_INVALID_PARAMETER DriverName is NULL. + @retval EFI_UNSUPPORTED The driver specified by This does not support the + language specified by Language. + +**/ +EFI_STATUS +EFIAPI +Ext4ComponentNameGetDriverName ( + IN EFI_COMPONENT_NAME_PROTOCOL *This, + IN CHAR8 *Language, + OUT CHAR16 **DriverName + ); + +/** + Retrieves a Unicode string that is the user readable name of the controller + that is being managed by an EFI Driver. + + @param[in] This A pointer to the EFI_COMPONENT_NAME_PROTOCOL instance. + @param[in] ControllerHandle The handle of a controller that the driver specified by + This is managing. This handle specifies the controller + whose name is to be returned. + @param[in] ChildHandle The handle of the child controller to retrieve the name + of. This is an optional parameter that may be NULL. It + will be NULL for device drivers. It will also be NULL + for a bus drivers that wish to retrieve the name of the + bus controller. It will not be NULL for a bus driver + that wishes to retrieve the name of a child controller. + @param[in] Language A pointer to a three character ISO 639-2 language + identifier. This is the language of the controller name + that the caller is requesting, and it must match one + of the languages specified in SupportedLanguages. The + number of languages supported by a driver is up to the + driver writer. + @param[out] ControllerName A pointer to the Unicode string to return. This Unicode + string is the name of the controller specified by + ControllerHandle and ChildHandle in the language specified + by Language, from the point of view of the driver specified + by This. + + @retval EFI_SUCCESS The Unicode string for the user-readable name in the + language specified by Language for the driver + specified by This was returned in DriverName. + @retval EFI_INVALID_PARAMETER ControllerHandle is NULL. + @retval EFI_INVALID_PARAMETER ChildHandle is not NULL and it is not a valid EFI_HANDLE. + @retval EFI_INVALID_PARAMETER Language is NULL. + @retval EFI_INVALID_PARAMETER ControllerName is NULL. + @retval EFI_UNSUPPORTED The driver specified by This is not currently managing + the controller specified by ControllerHandle and + ChildHandle. + @retval EFI_UNSUPPORTED The driver specified by This does not support the + language specified by Language. + +**/ +EFI_STATUS +EFIAPI +Ext4ComponentNameGetControllerName ( + IN EFI_COMPONENT_NAME_PROTOCOL *This, + IN EFI_HANDLE ControllerHandle, + IN EFI_HANDLE ChildHandle OPTIONAL, + IN CHAR8 *Language, + OUT CHAR16 **ControllerName + ); + +extern EFI_COMPONENT_NAME_PROTOCOL gExt4ComponentName; + +GLOBAL_REMOVE_IF_UNREFERENCED EFI_COMPONENT_NAME_PROTOCOL gExt4ComponentName = { + Ext4ComponentNameGetDriverName, + Ext4ComponentNameGetControllerName, + "eng" +}; + +// +// EFI Component Name 2 Protocol +// +GLOBAL_REMOVE_IF_UNREFERENCED EFI_COMPONENT_NAME2_PROTOCOL gExt4ComponentName2 = { + (EFI_COMPONENT_NAME2_GET_DRIVER_NAME)Ext4ComponentNameGetDriverName, + (EFI_COMPONENT_NAME2_GET_CONTROLLER_NAME)Ext4ComponentNameGetControllerName, + "en" +}; + +// Needed by gExt4BindingProtocol + +/** + Tests to see if this driver supports a given controller. If a child device is provided, + it further tests to see if this driver supports creating a handle for the specified child device. + + This function checks to see if the driver specified by This supports the device specified by + ControllerHandle. Drivers will typically use the device path attached to + ControllerHandle and/or the services from the bus I/O abstraction attached to + ControllerHandle to determine if the driver supports ControllerHandle. This function + may be called many times during platform initialization. In order to reduce boot times, the tests + performed by this function must be very small, and take as little time as possible to execute. This + function must not change the state of any hardware devices, and this function must be aware that the + device specified by ControllerHandle may already be managed by the same driver or a + different driver. This function must match its calls to AllocatePages() with FreePages(), + AllocatePool() with FreePool(), and OpenProtocol() with CloseProtocol(). + Because ControllerHandle may have been previously started by the same driver, if a protocol is + already in the opened state, then it must not be closed with CloseProtocol(). This is required + to guarantee the state of ControllerHandle is not modified by this function. + + @param[in] This A pointer to the EFI_DRIVER_BINDING_PROTOCOL instance. + @param[in] ControllerHandle The handle of the controller to test. This handle + must support a protocol interface that supplies + an I/O abstraction to the driver. + @param[in] RemainingDevicePath A pointer to the remaining portion of a device path. This + parameter is ignored by device drivers, and is optional for bus + drivers. For bus drivers, if this parameter is not NULL, then + the bus driver must determine if the bus controller specified + by ControllerHandle and the child controller specified + by RemainingDevicePath are both supported by this + bus driver. + + @retval EFI_SUCCESS The device specified by ControllerHandle and + RemainingDevicePath is supported by the driver specified by This. + @retval EFI_ALREADY_STARTED The device specified by ControllerHandle and + RemainingDevicePath is already being managed by the driver + specified by This. + @retval EFI_ACCESS_DENIED The device specified by ControllerHandle and + RemainingDevicePath is already being managed by a different + driver or an application that requires exclusive access. + Currently not implemented. + @retval EFI_UNSUPPORTED The device specified by ControllerHandle and + RemainingDevicePath is not supported by the driver specified by This. +**/ +EFI_STATUS +EFIAPI +Ext4IsBindingSupported ( + IN EFI_DRIVER_BINDING_PROTOCOL *BindingProtocol, + IN EFI_HANDLE ControllerHandle, + IN EFI_DEVICE_PATH *RemainingDevicePath OPTIONAL + ); + +/** + Starts a device controller or a bus controller. + + The Start() function is designed to be invoked from the EFI boot service ConnectController(). + As a result, much of the error checking on the parameters to Start() has been moved into this + common boot service. It is legal to call Start() from other locations, + but the following calling restrictions must be followed, or the system behavior will not be deterministic. + 1. ControllerHandle must be a valid EFI_HANDLE. + 2. If RemainingDevicePath is not NULL, then it must be a pointer to a naturally aligned + EFI_DEVICE_PATH_PROTOCOL. + 3. Prior to calling Start(), the Supported() function for the driver specified by This must + have been called with the same calling parameters, and Supported() must have returned EFI_SUCCESS. + + @param[in] This A pointer to the EFI_DRIVER_BINDING_PROTOCOL instance. + @param[in] ControllerHandle The handle of the controller to start. This handle + must support a protocol interface that supplies + an I/O abstraction to the driver. + @param[in] RemainingDevicePath A pointer to the remaining portion of a device path. This + parameter is ignored by device drivers, and is optional for bus + drivers. For a bus driver, if this parameter is NULL, then handles + for all the children of Controller are created by this driver. + If this parameter is not NULL and the first Device Path Node is + not the End of Device Path Node, then only the handle for the + child device specified by the first Device Path Node of + RemainingDevicePath is created by this driver. + If the first Device Path Node of RemainingDevicePath is + the End of Device Path Node, no child handle is created by this + driver. + + @retval EFI_SUCCESS The device was started. + @retval EFI_DEVICE_ERROR The device could not be started due to a device error.Currently not implemented. + @retval EFI_OUT_OF_RESOURCES The request could not be completed due to a lack of resources. + @retval Others The driver failded to start the device. + +**/ +EFI_STATUS +EFIAPI +Ext4Bind ( + IN EFI_DRIVER_BINDING_PROTOCOL *BindingProtocol, + IN EFI_HANDLE ControllerHandle, + IN EFI_DEVICE_PATH *RemainingDevicePath OPTIONAL + ); + +/** + Stops a device controller or a bus controller. + + The Stop() function is designed to be invoked from the EFI boot service DisconnectController(). + As a result, much of the error checking on the parameters to Stop() has been moved + into this common boot service. It is legal to call Stop() from other locations, + but the following calling restrictions must be followed, or the system behavior will not be deterministic. + 1. ControllerHandle must be a valid EFI_HANDLE that was used on a previous call to this + same driver's Start() function. + 2. The first NumberOfChildren handles of ChildHandleBuffer must all be a valid + EFI_HANDLE. In addition, all of these handles must have been created in this driver's + Start() function, and the Start() function must have called OpenProtocol() on + ControllerHandle with an Attribute of EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER. + + @param[in] This A pointer to the EFI_DRIVER_BINDING_PROTOCOL instance. + @param[in] ControllerHandle A handle to the device being stopped. The handle must + support a bus specific I/O protocol for the driver + to use to stop the device. + @param[in] NumberOfChildren The number of child device handles in ChildHandleBuffer. + @param[in] ChildHandleBuffer An array of child handles to be freed. May be NULL + if NumberOfChildren is 0. + + @retval EFI_SUCCESS The device was stopped. + @retval EFI_DEVICE_ERROR The device could not be stopped due to a device error. + +**/ +EFI_STATUS +EFIAPI +Ext4Stop ( + IN EFI_DRIVER_BINDING_PROTOCOL *This, + IN EFI_HANDLE ControllerHandle, + IN UINTN NumberOfChildren, + IN EFI_HANDLE *ChildHandleBuffer OPTIONAL + ); + +EFI_DRIVER_BINDING_PROTOCOL gExt4BindingProtocol = +{ + .Supported = Ext4IsBindingSupported, + .Start = Ext4Bind, + .Stop = Ext4Stop, + .Version = EXT4_DRIVER_VERSION, + .ImageHandle = NULL, + .DriverBindingHandle = NULL +}; + +/** + Retrieves a Unicode string that is the user readable name of the controller + that is being managed by an EFI Driver. + + @param[in] This A pointer to the EFI_COMPONENT_NAME_PROTOCOL instance. + @param[in] ControllerHandle The handle of a controller that the driver specified by + This is managing. This handle specifies the controller + whose name is to be returned. + @param[in] ChildHandle The handle of the child controller to retrieve the name + of. This is an optional parameter that may be NULL. It + will be NULL for device drivers. It will also be NULL + for a bus drivers that wish to retrieve the name of the + bus controller. It will not be NULL for a bus driver + that wishes to retrieve the name of a child controller. + @param[in] Language A pointer to a three character ISO 639-2 language + identifier. This is the language of the controller name + that the caller is requesting, and it must match one + of the languages specified in SupportedLanguages. The + number of languages supported by a driver is up to the + driver writer. + @param[out] ControllerName A pointer to the Unicode string to return. This Unicode + string is the name of the controller specified by + ControllerHandle and ChildHandle in the language specified + by Language, from the point of view of the driver specified + by This. + + @retval EFI_SUCCESS The Unicode string for the user-readable name in the + language specified by Language for the driver + specified by This was returned in DriverName. + @retval EFI_INVALID_PARAMETER ControllerHandle is NULL. + @retval EFI_INVALID_PARAMETER ChildHandle is not NULL and it is not a valid EFI_HANDLE. + @retval EFI_INVALID_PARAMETER Language is NULL. + @retval EFI_INVALID_PARAMETER ControllerName is NULL. + @retval EFI_UNSUPPORTED The driver specified by This is not currently managing + the controller specified by ControllerHandle and + ChildHandle. + @retval EFI_UNSUPPORTED The driver specified by This does not support the + language specified by Language. + +**/ +EFI_STATUS +EFIAPI +Ext4ComponentNameGetControllerName ( + IN EFI_COMPONENT_NAME_PROTOCOL *This, + IN EFI_HANDLE ControllerHandle, + IN EFI_HANDLE ChildHandle OPTIONAL, + IN CHAR8 *Language, + OUT CHAR16 **ControllerName + ) +{ + EFI_STATUS Status; + + if (ChildHandle != NULL) { + return EFI_UNSUPPORTED; + } + + // Test if the driver manages ControllHandle + Status = EfiTestManagedDevice ( + ControllerHandle, + gExt4BindingProtocol.DriverBindingHandle, + &gEfiDiskIoProtocolGuid + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + return LookupUnicodeString2 ( + Language, + This->SupportedLanguages, + mExt4ControllerNameTable, + ControllerName, + (BOOLEAN)(This == &gExt4ComponentName) + ); +} + +/** + Retrieves a Unicode string that is the user-readable name of the EFI Driver. + + @param[in] This A pointer to the EFI_COMPONENT_NAME_PROTOCOL instance. + @param[in] Language A pointer to a three-character ISO 639-2 language identifier. + This is the language of the driver name that that the caller + is requesting, and it must match one of the languages specified + in SupportedLanguages. The number of languages supported by a + driver is up to the driver writer. + @param[out] DriverName A pointer to the Unicode string to return. This Unicode string + is the name of the driver specified by This in the language + specified by Language. + + @retval EFI_SUCCESS The Unicode string for the Driver specified by This + and the language specified by Language was returned + in DriverName. + @retval EFI_INVALID_PARAMETER Language is NULL. + @retval EFI_INVALID_PARAMETER DriverName is NULL. + @retval EFI_UNSUPPORTED The driver specified by This does not support the + language specified by Language. + +**/ +EFI_STATUS +EFIAPI +Ext4ComponentNameGetDriverName ( + IN EFI_COMPONENT_NAME_PROTOCOL *This, + IN CHAR8 *Language, + OUT CHAR16 **DriverName + ) +{ + return LookupUnicodeString2 ( + Language, + This->SupportedLanguages, + mExt4DriverNameTable, + DriverName, + (BOOLEAN)(This == &gExt4ComponentName) + ); +} + +/** + Stops a device controller or a bus controller. + + The Stop() function is designed to be invoked from the EFI boot service DisconnectController(). + As a result, much of the error checking on the parameters to Stop() has been moved + into this common boot service. It is legal to call Stop() from other locations, + but the following calling restrictions must be followed, or the system behavior will not be deterministic. + 1. ControllerHandle must be a valid EFI_HANDLE that was used on a previous call to this + same driver's Start() function. + 2. The first NumberOfChildren handles of ChildHandleBuffer must all be a valid + EFI_HANDLE. In addition, all of these handles must have been created in this driver's + Start() function, and the Start() function must have called OpenProtocol() on + ControllerHandle with an Attribute of EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER. + + @param[in] This A pointer to the EFI_DRIVER_BINDING_PROTOCOL instance. + @param[in] ControllerHandle A handle to the device being stopped. The handle must + support a bus specific I/O protocol for the driver + to use to stop the device. + @param[in] NumberOfChildren The number of child device handles in ChildHandleBuffer. + @param[in] ChildHandleBuffer An array of child handles to be freed. May be NULL + if NumberOfChildren is 0. + + @retval EFI_SUCCESS The device was stopped. + @retval EFI_DEVICE_ERROR The device could not be stopped due to a device error. + +**/ +EFI_STATUS +EFIAPI +Ext4Stop ( + IN EFI_DRIVER_BINDING_PROTOCOL *This, + IN EFI_HANDLE ControllerHandle, + IN UINTN NumberOfChildren, + IN EFI_HANDLE *ChildHandleBuffer OPTIONAL + ) +{ + EFI_STATUS Status; + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *Sfs; + EXT4_PARTITION *Partition; + BOOLEAN HasDiskIo2; + + Status = gBS->OpenProtocol ( + ControllerHandle, + &gEfiSimpleFileSystemProtocolGuid, + (VOID **)&Sfs, + This->DriverBindingHandle, + ControllerHandle, + EFI_OPEN_PROTOCOL_GET_PROTOCOL + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + Partition = (EXT4_PARTITION *)Sfs; + + HasDiskIo2 = EXT4_DISK_IO2 (Partition) != NULL; + + Status = Ext4UnmountAndFreePartition (Partition); + + if (EFI_ERROR (Status)) { + return Status; + } + + Status = gBS->UninstallMultipleProtocolInterfaces ( + ControllerHandle, + &gEfiSimpleFileSystemProtocolGuid, + &Partition->Interface, + NULL + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + // Close all open protocols (DiskIo, DiskIo2, BlockIo) + + Status = gBS->CloseProtocol ( + ControllerHandle, + &gEfiDiskIoProtocolGuid, + This->DriverBindingHandle, + ControllerHandle + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + Status = gBS->CloseProtocol ( + ControllerHandle, + &gEfiBlockIoProtocolGuid, + This->DriverBindingHandle, + ControllerHandle + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + if (HasDiskIo2) { + Status = gBS->CloseProtocol ( + ControllerHandle, + &gEfiDiskIo2ProtocolGuid, + This->DriverBindingHandle, + ControllerHandle + ); + + if (EFI_ERROR (Status)) { + return Status; + } + } + + return Status; +} + +/** + Ext4Dxe Driver's entry point. + + Called at load time. + + @param[in] ImageHandle Handle to the image. + @param[in] SystemTable Pointer to the EFI_SYSTEM_TABLE. + @return Result of the load. +**/ +EFI_STATUS +EFIAPI +Ext4EntryPoint ( + IN EFI_HANDLE ImageHandle, + IN EFI_SYSTEM_TABLE *SystemTable + ) +{ + EFI_STATUS Status; + + Status = EfiLibInstallAllDriverProtocols2 ( + ImageHandle, + SystemTable, + &gExt4BindingProtocol, + ImageHandle, + &gExt4ComponentName, + &gExt4ComponentName2, + NULL, + NULL, + NULL, + NULL + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + return Ext4InitialiseUnicodeCollation (ImageHandle); +} + +/** + Ext4Dxe Driver's unload callback. + + Called at unload time. + + @param[in] ImageHandle Handle to the image. + @return Result of the unload. +**/ +EFI_STATUS +EFIAPI +Ext4Unload ( + IN EFI_HANDLE ImageHandle + ) +{ + EFI_STATUS Status; + EFI_HANDLE *DeviceHandleBuffer; + UINTN DeviceHandleCount; + UINTN Index; + EFI_HANDLE Handle; + + Status = gBS->LocateHandleBuffer ( + AllHandles, + NULL, + NULL, + &DeviceHandleCount, + &DeviceHandleBuffer + ); + if (EFI_ERROR (Status)) { + return Status; + } + + for (Index = 0; Index < DeviceHandleCount; Index++) { + Handle = DeviceHandleBuffer[Index]; + + Status = EfiTestManagedDevice (Handle, ImageHandle, &gEfiDiskIoProtocolGuid); + + if (Status == EFI_SUCCESS) { + Status = gBS->DisconnectController (Handle, ImageHandle, NULL); + + if (EFI_ERROR (Status)) { + break; + } + } + } + + FreePool (DeviceHandleBuffer); + + Status = EfiLibUninstallAllDriverProtocols2 ( + &gExt4BindingProtocol, + &gExt4ComponentName, + &gExt4ComponentName2, + NULL, + NULL, + NULL, + NULL + ); + + return Status; +} + +/** + Tests to see if this driver supports a given controller. If a child device is provided, + it further tests to see if this driver supports creating a handle for the specified child device. + + This function checks to see if the driver specified by This supports the device specified by + ControllerHandle. Drivers will typically use the device path attached to + ControllerHandle and/or the services from the bus I/O abstraction attached to + ControllerHandle to determine if the driver supports ControllerHandle. This function + may be called many times during platform initialization. In order to reduce boot times, the tests + performed by this function must be very small, and take as little time as possible to execute. This + function must not change the state of any hardware devices, and this function must be aware that the + device specified by ControllerHandle may already be managed by the same driver or a + different driver. This function must match its calls to AllocatePages() with FreePages(), + AllocatePool() with FreePool(), and OpenProtocol() with CloseProtocol(). + Because ControllerHandle may have been previously started by the same driver, if a protocol is + already in the opened state, then it must not be closed with CloseProtocol(). This is required + to guarantee the state of ControllerHandle is not modified by this function. + + @param[in] This A pointer to the EFI_DRIVER_BINDING_PROTOCOL instance. + @param[in] ControllerHandle The handle of the controller to test. This handle + must support a protocol interface that supplies + an I/O abstraction to the driver. + @param[in] RemainingDevicePath A pointer to the remaining portion of a device path. This + parameter is ignored by device drivers, and is optional for bus + drivers. For bus drivers, if this parameter is not NULL, then + the bus driver must determine if the bus controller specified + by ControllerHandle and the child controller specified + by RemainingDevicePath are both supported by this + bus driver. + + @retval EFI_SUCCESS The device specified by ControllerHandle and + RemainingDevicePath is supported by the driver specified by This. + @retval EFI_ALREADY_STARTED The device specified by ControllerHandle and + RemainingDevicePath is already being managed by the driver + specified by This. + @retval EFI_ACCESS_DENIED The device specified by ControllerHandle and + RemainingDevicePath is already being managed by a different + driver or an application that requires exclusive access. + @retval EFI_UNSUPPORTED The device specified by ControllerHandle and + RemainingDevicePath is not supported by the driver specified by This. +**/ +EFI_STATUS +EFIAPI +Ext4IsBindingSupported ( + IN EFI_DRIVER_BINDING_PROTOCOL *BindingProtocol, + IN EFI_HANDLE ControllerHandle, + IN EFI_DEVICE_PATH *RemainingDevicePath OPTIONAL + ) +{ + EFI_STATUS Status; + EFI_DISK_IO_PROTOCOL *DiskIo; + EFI_BLOCK_IO_PROTOCOL *BlockIo; + + DiskIo = NULL; + BlockIo = NULL; + + // + // Open the IO Abstraction(s) needed to perform the supported test + // + Status = gBS->OpenProtocol ( + ControllerHandle, + &gEfiDiskIoProtocolGuid, + (VOID **)&DiskIo, + BindingProtocol->DriverBindingHandle, + ControllerHandle, + EFI_OPEN_PROTOCOL_BY_DRIVER + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + // + // Open the IO Abstraction(s) needed to perform the supported test + // + Status = gBS->OpenProtocol ( + ControllerHandle, + &gEfiBlockIoProtocolGuid, + (VOID **)&BlockIo, + BindingProtocol->DriverBindingHandle, + ControllerHandle, + EFI_OPEN_PROTOCOL_GET_PROTOCOL + ); + + if (!EFI_ERROR (Status)) { + if (!Ext4SuperblockCheckMagic (DiskIo, BlockIo)) { + Status = EFI_UNSUPPORTED; + } + } + + // + // Close the I/O Abstraction(s) used to perform the supported test + // + if (DiskIo != NULL) { + gBS->CloseProtocol ( + ControllerHandle, + &gEfiDiskIoProtocolGuid, + BindingProtocol->DriverBindingHandle, + ControllerHandle + ); + } + + if (BlockIo != NULL) { + gBS->CloseProtocol ( + ControllerHandle, + &gEfiBlockIoProtocolGuid, + BindingProtocol->DriverBindingHandle, + ControllerHandle + ); + } + + return Status; +} + +/** + Starts a device controller or a bus controller. + + The Start() function is designed to be invoked from the EFI boot service ConnectController(). + As a result, much of the error checking on the parameters to Start() has been moved into this + common boot service. It is legal to call Start() from other locations, + but the following calling restrictions must be followed, or the system behavior will not be deterministic. + 1. ControllerHandle must be a valid EFI_HANDLE. + 2. If RemainingDevicePath is not NULL, then it must be a pointer to a naturally aligned + EFI_DEVICE_PATH_PROTOCOL. + 3. Prior to calling Start(), the Supported() function for the driver specified by This must + have been called with the same calling parameters, and Supported() must have returned EFI_SUCCESS. + + @param[in] This A pointer to the EFI_DRIVER_BINDING_PROTOCOL instance. + @param[in] ControllerHandle The handle of the controller to start. This handle + must support a protocol interface that supplies + an I/O abstraction to the driver. + @param[in] RemainingDevicePath A pointer to the remaining portion of a device path. This + parameter is ignored by device drivers, and is optional for bus + drivers. For a bus driver, if this parameter is NULL, then handles + for all the children of Controller are created by this driver. + If this parameter is not NULL and the first Device Path Node is + not the End of Device Path Node, then only the handle for the + child device specified by the first Device Path Node of + RemainingDevicePath is created by this driver. + If the first Device Path Node of RemainingDevicePath is + the End of Device Path Node, no child handle is created by this + driver. + + @retval EFI_SUCCESS The device was started. + @retval EFI_DEVICE_ERROR The device could not be started due to a device error.Currently not implemented. + @retval EFI_OUT_OF_RESOURCES The request could not be completed due to a lack of resources. + @retval Others The driver failded to start the device. + +**/ +EFI_STATUS +EFIAPI +Ext4Bind ( + IN EFI_DRIVER_BINDING_PROTOCOL *BindingProtocol, + IN EFI_HANDLE ControllerHandle, + IN EFI_DEVICE_PATH *RemainingDevicePath OPTIONAL + ) +{ + EFI_DISK_IO_PROTOCOL *DiskIo; + EFI_DISK_IO2_PROTOCOL *DiskIo2; + EFI_BLOCK_IO_PROTOCOL *BlockIo; + EFI_STATUS Status; + + DiskIo2 = NULL; + BlockIo = NULL; + DiskIo = NULL; + + Status = gBS->OpenProtocol ( + ControllerHandle, + &gEfiDiskIoProtocolGuid, + (VOID **)&DiskIo, + BindingProtocol->ImageHandle, + ControllerHandle, + EFI_OPEN_PROTOCOL_BY_DRIVER + ); + + if (EFI_ERROR (Status)) { + goto Error; + } + + DEBUG ((DEBUG_INFO, "[Ext4] Controller supports DISK_IO\n")); + + Status = gBS->OpenProtocol ( + ControllerHandle, + &gEfiDiskIo2ProtocolGuid, + (VOID **)&DiskIo2, + BindingProtocol->ImageHandle, + ControllerHandle, + EFI_OPEN_PROTOCOL_BY_DRIVER + ); + // It's okay to not support DISK_IO2 + + if (DiskIo2 != NULL) { + DEBUG ((DEBUG_INFO, "[Ext4] Controller supports DISK_IO2\n")); + } + + Status = gBS->OpenProtocol ( + ControllerHandle, + &gEfiBlockIoProtocolGuid, + (VOID **)&BlockIo, + BindingProtocol->ImageHandle, + ControllerHandle, + EFI_OPEN_PROTOCOL_GET_PROTOCOL + ); + + if (EFI_ERROR (Status)) { + goto Error; + } + + Status = Ext4OpenPartition (ControllerHandle, DiskIo, DiskIo2, BlockIo); + + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_ERROR, "[ext4] Error mounting: %r\n", Status)); + // Falls through to Error + } else { + return Status; + } + +Error: + if (DiskIo) { + gBS->CloseProtocol ( + ControllerHandle, + &gEfiDiskIoProtocolGuid, + BindingProtocol->ImageHandle, + ControllerHandle + ); + } + + if (DiskIo2) { + gBS->CloseProtocol ( + ControllerHandle, + &gEfiDiskIo2ProtocolGuid, + BindingProtocol->ImageHandle, + ControllerHandle + ); + } + + if (BlockIo) { + gBS->CloseProtocol ( + ControllerHandle, + &gEfiBlockIoProtocolGuid, + BindingProtocol->ImageHandle, + ControllerHandle + ); + } + + return Status; +} diff --git a/Ext4Pkg/Ext4Dxe/Ext4Dxe.h b/Ext4Pkg/Ext4Dxe/Ext4Dxe.h new file mode 100644 index 0000000000..adf3c13f6e --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Ext4Dxe.h @@ -0,0 +1,1232 @@ +/** @file + Common header for the driver + + Copyright (c) 2021 - 2022 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#ifndef EXT4_H_ +#define EXT4_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Ext4Disk.h" + +#define SYMLOOP_MAX 8 +#define EXT4_NAME_MAX 255 +// +// We need to specify path length limit for security purposes, to prevent possible +// overflows and dead-loop conditions. Originally this limit is absent in FS design, +// but present in UNIX distros and shell environments, which may varies from 1024 to 4096. +// +#define EXT4_EFI_PATH_MAX 4096 +#define EXT4_DRIVER_VERSION 0x0000 + +/** + Opens an ext4 partition and installs the Simple File System protocol. + + @param[in] DeviceHandle Handle to the block device. + @param[in] DiskIo Pointer to an EFI_DISK_IO_PROTOCOL. + @param[in opt] DiskIo2 Pointer to an EFI_DISK_IO2_PROTOCOL, +if supported. + @param[in] BlockIo Pointer to an EFI_BLOCK_IO_PROTOCOL. + + @retval EFI_SUCCESS The opening was successful. + !EFI_SUCCESS Opening failed. +**/ +EFI_STATUS +Ext4OpenPartition ( + IN EFI_HANDLE DeviceHandle, + IN EFI_DISK_IO_PROTOCOL *DiskIo, + IN OPTIONAL EFI_DISK_IO2_PROTOCOL *DiskIo2, + IN EFI_BLOCK_IO_PROTOCOL *BlockIo + ); + +typedef struct _Ext4File EXT4_FILE; +typedef struct _Ext4_Dentry EXT4_DENTRY; + +typedef struct _Ext4_PARTITION { + EFI_SIMPLE_FILE_SYSTEM_PROTOCOL Interface; + EFI_DISK_IO_PROTOCOL *DiskIo; + EFI_DISK_IO2_PROTOCOL *DiskIo2; + EFI_BLOCK_IO_PROTOCOL *BlockIo; + + EXT4_SUPERBLOCK SuperBlock; + BOOLEAN Unmounting; + + UINT32 FeaturesIncompat; + UINT32 FeaturesCompat; + UINT32 FeaturesRoCompat; + UINT32 InodeSize; + UINT32 BlockSize; + BOOLEAN ReadOnly; + UINT64 NumberBlockGroups; + EXT4_BLOCK_NR NumberBlocks; + + EXT4_BLOCK_GROUP_DESC *BlockGroups; + UINT32 DescSize; + EXT4_FILE *Root; + + UINT32 InitialSeed; + + LIST_ENTRY OpenFiles; + + EXT4_DENTRY *RootDentry; +} EXT4_PARTITION; + +/** + This structure represents a directory entry inside our directory entry tree. + For now, it will be used as a way to track file names inside our opening + code, but it may very well be used as a directory cache in the future. + Because it's not being used as a directory cache right now, + an EXT4_DENTRY structure is not necessarily unique name-wise in the list of + children. Therefore, the dentry tree does not accurately reflect the + filesystem structure. + */ +struct _Ext4_Dentry { + UINTN RefCount; + CHAR16 Name[EXT4_NAME_MAX + 1]; + EXT4_INO_NR Inode; + struct _Ext4_Dentry *Parent; + LIST_ENTRY Children; + LIST_ENTRY ListNode; +}; + +#define EXT4_DENTRY_FROM_DENTRY_LIST(Node) BASE_CR(Node, EXT4_DENTRY, ListNode) + +/** + Creates a new dentry object. + + @param[in] Name Name of the dentry. + @param[in out opt] Parent Parent dentry, if it's not NULL. + + @return The new allocated and initialised dentry. + The ref count will be set to 1. +**/ +EXT4_DENTRY * +Ext4CreateDentry ( + IN CONST CHAR16 *Name, + IN OUT EXT4_DENTRY *Parent OPTIONAL + ); + +/** + Increments the ref count of the dentry. + + @param[in out] Dentry Pointer to a valid EXT4_DENTRY. +**/ +VOID +Ext4RefDentry ( + IN OUT EXT4_DENTRY *Dentry + ); + +/** + Decrements the ref count of the dentry. + If the ref count is 0, it's destroyed. + + @param[in out] Dentry Pointer to a valid EXT4_DENTRY. + + @retval True if it was destroyed, false if it's alive. +**/ +BOOLEAN +Ext4UnrefDentry ( + IN OUT EXT4_DENTRY *Dentry + ); + +/** + Opens and parses the superblock. + + @param[out] Partition Partition structure to fill with filesystem +details. + @retval EFI_SUCCESS Parsing was succesful and the partition is a + valid ext4 partition. +**/ +EFI_STATUS +Ext4OpenSuperblock ( + OUT EXT4_PARTITION *Partition + ); + +/** + Retrieves the EFI_BLOCK_IO_PROTOCOL of the partition. + + @param[in] Partition Pointer to the opened ext4 partition. + @return The Block IO protocol associated with the partition. +**/ +#define EXT4_BLOCK_IO(Partition) Partition->BlockIo + +/** + Retrieves the EFI_DISK_IO_PROTOCOL of the partition. + + @param[in] Partition Pointer to the opened ext4 partition. + @return The Disk IO protocol associated with the partition. +**/ +#define EXT4_DISK_IO(Partition) Partition->DiskIo + +/** + Retrieves the EFI_DISK_IO2_PROTOCOL of the partition. + + @param[in] Partition Pointer to the opened ext4 partition. + @return The Disk IO 2 protocol associated with the partition, or NULL if + not supported. +**/ +#define EXT4_DISK_IO2(Partition) Partition->DiskIo2 + +/** + Retrieves the media ID of the partition. + + @param[in] Partition Pointer to the opened ext4 partition. + @return The media ID associated with the partition. +**/ +#define EXT4_MEDIA_ID(Partition) Partition->BlockIo->Media->MediaId + +/** + Reads from the partition's disk using the DISK_IO protocol. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[out] Buffer Pointer to a destination buffer. + @param[in] Length Length of the destination buffer. + @param[in] Offset Offset, in bytes, of the location to read. + + @return Success status of the disk read. +**/ +EFI_STATUS +Ext4ReadDiskIo ( + IN EXT4_PARTITION *Partition, + OUT VOID *Buffer, + IN UINTN Length, + IN UINT64 Offset + ); + +/** + Reads blocks from the partition's disk using the DISK_IO protocol. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[out] Buffer Pointer to a destination buffer. + @param[in] NumberBlocks Length of the read, in filesystem blocks. + @param[in] BlockNumber Starting block number. + + @return Success status of the read. +**/ +EFI_STATUS +Ext4ReadBlocks ( + IN EXT4_PARTITION *Partition, + OUT VOID *Buffer, + IN UINTN NumberBlocks, + IN EXT4_BLOCK_NR BlockNumber + ); + +/** + Allocates a buffer and reads blocks from the partition's disk using the +DISK_IO protocol. This function is deprecated and will be removed in the future. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[in] NumberBlocks Length of the read, in filesystem blocks. + @param[in] BlockNumber Starting block number. + + @return Buffer allocated by AllocatePool, or NULL if some part of the process + failed. +**/ +VOID * +Ext4AllocAndReadBlocks ( + IN EXT4_PARTITION *Partition, + IN UINTN NumberBlocks, + IN EXT4_BLOCK_NR BlockNumber + ); + +/** + Checks if the opened partition has the 64-bit feature (see +EXT4_FEATURE_INCOMPAT_64BIT). + + @param[in] Partition Pointer to the opened ext4 partition. + + @return TRUE if EXT4_FEATURE_INCOMPAT_64BIT is enabled, else FALSE. +**/ +#define EXT4_IS_64_BIT(Partition) \ + ((Partition->FeaturesIncompat & EXT4_FEATURE_INCOMPAT_64BIT) != 0) + +/** + Composes an EXT4_BLOCK_NR safely, from two halfs. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[in] Low Low half of the block number. + @param[in] High High half of the block number. + + @return The block number formed by Low, and if 64 bit is enabled, High. +**/ +#define EXT4_BLOCK_NR_FROM_HALFS(Partition, Low, High) \ + EXT4_IS_64_BIT(Partition) ? (Low | LShiftU64(High, 32)) : Low + +/** + Retrieves a block group descriptor of the ext4 filesystem. + + @param[in] Partition Pointer to the opened ext4 partition. + @param[in] BlockGroup Block group number. + + @return A pointer to the block group descriptor. +**/ +EXT4_BLOCK_GROUP_DESC * +Ext4GetBlockGroupDesc ( + IN EXT4_PARTITION *Partition, + IN UINT32 BlockGroup + ); + +/** + Reads an inode from disk. + + @param[in] Partition Pointer to the opened partition. + @param[in] InodeNum Number of the desired Inode + @param[out] OutIno Pointer to where it will be stored a pointer to the +read inode. + + @return Status of the inode read. +**/ +EFI_STATUS +Ext4ReadInode ( + IN EXT4_PARTITION *Partition, + IN EXT4_INO_NR InodeNum, + OUT EXT4_INODE **OutIno + ); + +/** + Converts blocks to bytes. + + @param[in] Partition Pointer to the opened partition. + @param[in] Block Block number/number of blocks. + + @return The number of bytes. +**/ +#define EXT4_BLOCK_TO_BYTES(Partition, Block) \ + MultU64x32(Block, Partition->BlockSize) + +/** + Reads from an EXT4 inode. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] File Pointer to the opened file. + @param[out] Buffer Pointer to the buffer. + @param[in] Offset Offset of the read. + @param[in out] Length Pointer to the length of the buffer, in bytes. + After a succesful read, it's updated to the +number of read bytes. + + @return Status of the read operation. +**/ +EFI_STATUS +Ext4Read ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT VOID *Buffer, + IN UINT64 Offset, + IN OUT UINTN *Length + ); + +/** + Retrieves the size of the inode. + + @param[in] Inode Pointer to the ext4 inode. + + @return The size of the inode, in bytes. +**/ +#define EXT4_INODE_SIZE(Inode) \ + (LShiftU64(Inode->i_size_hi, 32) | Inode->i_size_lo) + +/** + Retrieves an extent from an EXT4 inode. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] File Pointer to the opened file. + @param[in] LogicalBlock Block number which the returned extent must +cover. + @param[out] Extent Pointer to the output buffer, where the extent +will be copied to. + + @retval EFI_SUCCESS Retrieval was succesful. + @retval EFI_NO_MAPPING Block has no mapping. +**/ +EFI_STATUS +Ext4GetExtent ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + IN EXT4_BLOCK_NR LogicalBlock, + OUT EXT4_EXTENT *Extent + ); + +struct _Ext4File { + EFI_FILE_PROTOCOL Protocol; + EXT4_INODE *Inode; + EXT4_INO_NR InodeNum; + + UINT64 OpenMode; + UINT64 Position; + UINT32 SymLoops; + + EXT4_PARTITION *Partition; + + ORDERED_COLLECTION *ExtentsMap; + + LIST_ENTRY OpenFilesListNode; + + // Owning reference to this file's directory entry. + EXT4_DENTRY *Dentry; +}; + +#define EXT4_FILE_FROM_THIS(This) BASE_CR ((This), EXT4_FILE, Protocol) + +#define EXT4_FILE_FROM_OPEN_FILES_NODE(Node) \ + BASE_CR(Node, EXT4_FILE, OpenFilesListNode) + +/** + Retrieves a directory entry. + + @param[in] Directory Pointer to the opened directory. + @param[in] NameUnicode Pointer to the UCS-2 formatted filename. + @param[in] Partition Pointer to the ext4 partition. + @param[out] Result Pointer to the destination directory entry. + + @return The result of the operation. +**/ +EFI_STATUS +Ext4RetrieveDirent ( + IN EXT4_FILE *Directory, + IN CONST CHAR16 *NameUnicode, + IN EXT4_PARTITION *Partition, + OUT EXT4_DIR_ENTRY *Result + ); + +/** + Opens a file. + + @param[in] Directory Pointer to the opened directory. + @param[in] Name Pointer to the UCS-2 formatted filename. + @param[in] Partition Pointer to the ext4 partition. + @param[in] OpenMode Mode in which the file is supposed to be open. + @param[out] OutFile Pointer to the newly opened file. + + @return Result of the operation. +**/ +EFI_STATUS +Ext4OpenFile ( + IN EXT4_FILE *Directory, + IN CONST CHAR16 *Name, + IN EXT4_PARTITION *Partition, + IN UINT64 OpenMode, + OUT EXT4_FILE **OutFile + ); + +/** + Opens a file using a directory entry. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] OpenMode Mode in which the file is supposed to be open. + @param[out] OutFile Pointer to the newly opened file. + @param[in] Entry Directory entry to be used. + @param[in] Directory Pointer to the opened directory. + + @retval EFI_STATUS Result of the operation +**/ +EFI_STATUS +Ext4OpenDirent ( + IN EXT4_PARTITION *Partition, + IN UINT64 OpenMode, + OUT EXT4_FILE **OutFile, + IN EXT4_DIR_ENTRY *Entry, + IN EXT4_FILE *Directory + ); + +/** + Allocates a zeroed inode structure. + @param[in] Partition Pointer to the opened EXT4 partition. + + @return Pointer to the allocated structure, from the pool, + with size Partition->InodeSize. +**/ +EXT4_INODE * +Ext4AllocateInode ( + IN EXT4_PARTITION *Partition + ); + +// Part of the EFI_SIMPLE_FILE_SYSTEM_PROTOCOL + +/** + Open the root directory on a volume. + + @param[in] This A pointer to the volume to open the root directory. + @param[out] Root A pointer to the location to return the opened file handle +for the root directory. + + @retval EFI_SUCCESS The device was opened. + @retval EFI_UNSUPPORTED This volume does not support the requested file +system type. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_ACCESS_DENIED The service denied access to the file. + @retval EFI_OUT_OF_RESOURCES The volume was not opened due to lack of +resources. + @retval EFI_MEDIA_CHANGED The device has a different medium in it or the +medium is no longer supported. Any existing file handles for this volume are no +longer valid. To access the files on the new medium, the volume must be reopened +with OpenVolume(). + +**/ +EFI_STATUS +EFIAPI +Ext4OpenVolume ( + IN EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *This, + IN EFI_FILE_PROTOCOL **Root + ); + +// End of EFI_SIMPLE_FILE_SYSTEM_PROTOCOL + +/** + Sets up the protocol and metadata of a file that is being opened. + + @param[in out] File Pointer to the file. + @param[in] Partition Pointer to the opened partition. +**/ +VOID +Ext4SetupFile ( + IN OUT EXT4_FILE *File, + IN EXT4_PARTITION *Partition + ); + +/** + Opens a new file relative to the source file's location. + + @param[out] FoundFile A pointer to the location to return the opened handle for the new + file. + @param[in] Source A pointer to the EXT4_FILE instance that is the file + handle to the source location. This would typically be an open + handle to a directory. + @param[in] FileName The Null-terminated string of the name of the file to be opened. + The file name may contain the following path modifiers: "\", ".", + and "..". + @param[in] OpenMode The mode to open the file. The only valid combinations that the + file may be opened with are: Read, Read/Write, or Create/Read/Write. + @param[in] Attributes Only valid for EFI_FILE_MODE_CREATE, in which case these are the + attribute bits for the newly created file. + + @retval EFI_SUCCESS The file was opened. + @retval EFI_NOT_FOUND The specified file could not be found on the device. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_MEDIA_CHANGED The device has a different medium in it or the medium is no + longer supported. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED An attempt was made to create a file, or open a file for write + when the media is write-protected. + @retval EFI_ACCESS_DENIED The service denied access to the file. + @retval EFI_OUT_OF_RESOURCES Not enough resources were available to open the file. + @retval EFI_VOLUME_FULL The volume is full. + +**/ +EFI_STATUS +Ext4OpenInternal ( + OUT EXT4_FILE **FoundFile, + IN EXT4_FILE *Source, + IN CHAR16 *FileName, + IN UINT64 OpenMode, + IN UINT64 Attributes + ); + +/** + Closes a file. + + @param[in] File Pointer to the file. + + @return Status of the closing of the file. +**/ +EFI_STATUS +Ext4CloseInternal ( + IN EXT4_FILE *File + ); + +// Part of the EFI_FILE_PROTOCOL + +/** + Opens a new file relative to the source file's location. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the +file handle to the source location. This would typically be an open handle to a +directory. + @param[out] NewHandle A pointer to the location to return the opened handle +for the new file. + @param[in] FileName The Null-terminated string of the name of the file to +be opened. The file name may contain the following path modifiers: "\", ".", and +"..". + @param[in] OpenMode The mode to open the file. The only valid combinations +that the file may be opened with are: Read, Read/Write, or Create/Read/Write. + @param[in] Attributes Only valid for EFI_FILE_MODE_CREATE, in which case +these are the attribute bits for the newly created file. + + @retval EFI_SUCCESS The file was opened. + @retval EFI_NOT_FOUND The specified file could not be found on the +device. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_MEDIA_CHANGED The device has a different medium in it or the +medium is no longer supported. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED An attempt was made to create a file, or open a +file for write when the media is write-protected. + @retval EFI_ACCESS_DENIED The service denied access to the file. + @retval EFI_OUT_OF_RESOURCES Not enough resources were available to open the +file. + @retval EFI_VOLUME_FULL The volume is full. + +**/ +EFI_STATUS +EFIAPI +Ext4Open ( + IN EFI_FILE_PROTOCOL *This, + OUT EFI_FILE_PROTOCOL **NewHandle, + IN CHAR16 *FileName, + IN UINT64 OpenMode, + IN UINT64 Attributes + ); + +/** + Closes a specified file handle. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is +the file handle to close. + + @retval EFI_SUCCESS The file was closed. + +**/ +EFI_STATUS +EFIAPI +Ext4Close ( + IN EFI_FILE_PROTOCOL *This + ); + +/** + Close and delete the file handle. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL +instance that is the handle to the file to delete. + + @retval EFI_SUCCESS The file was closed and deleted, and the +handle was closed. + @retval EFI_WARN_DELETE_FAILURE The handle was closed, but the file was not +deleted. + +**/ +EFI_STATUS +EFIAPI +Ext4Delete ( + IN EFI_FILE_PROTOCOL *This + ); + +/** + Reads data from a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance +that is the file handle to read data from. + @param[in out] BufferSize On input, the size of the Buffer. On output, +the amount of data returned in Buffer. In both cases, the size is measured in +bytes. + @param[out] Buffer The buffer into which the data is read. + + @retval EFI_SUCCESS Data was read. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_DEVICE_ERROR An attempt was made to read from a deleted file. + @retval EFI_DEVICE_ERROR On entry, the current file position is beyond the +end of the file. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_BUFFER_TOO_SMALL The BufferSize is too small to read the current +directory entry. BufferSize has been updated with the size needed to complete +the request. + +**/ +EFI_STATUS +EFIAPI +Ext4ReadFile ( + IN EFI_FILE_PROTOCOL *This, + IN OUT UINTN *BufferSize, + OUT VOID *Buffer + ); + +/** + Writes data to a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that +is the file handle to write data to. + @param[in out] BufferSize On input, the size of the Buffer. On output, the +amount of data actually written. In both cases, the size is measured in bytes. + @param[in] Buffer The buffer of data to write. + + @retval EFI_SUCCESS Data was written. + @retval EFI_UNSUPPORTED Writes to open directory files are not supported. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_DEVICE_ERROR An attempt was made to write to a deleted file. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED The file or medium is write-protected. + @retval EFI_ACCESS_DENIED The file was opened read only. + @retval EFI_VOLUME_FULL The volume is full. + +**/ +EFI_STATUS +EFIAPI +Ext4WriteFile ( + IN EFI_FILE_PROTOCOL *This, + IN OUT UINTN *BufferSize, + IN VOID *Buffer + ); + +/** + Returns a file's current position. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that +is the file handle to get the current position on. + @param[out] Position The address to return the file's current position +value. + + @retval EFI_SUCCESS The position was returned. + @retval EFI_UNSUPPORTED The request is not valid on open directories. + @retval EFI_DEVICE_ERROR An attempt was made to get the position from a +deleted file. + +**/ +EFI_STATUS +EFIAPI +Ext4GetPosition ( + IN EFI_FILE_PROTOCOL *This, + OUT UINT64 *Position + ); + +/** + Sets a file's current position. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that +is the file handle to set the requested position on. + @param[in] Position The byte position from the start of the file to +set. + + @retval EFI_SUCCESS The position was set. + @retval EFI_UNSUPPORTED The seek request for nonzero is not valid on open + directories. + @retval EFI_DEVICE_ERROR An attempt was made to set the position of a deleted +file. + +**/ +EFI_STATUS +EFIAPI +Ext4SetPosition ( + IN EFI_FILE_PROTOCOL *This, + IN UINT64 Position + ); + +/** + Returns information about a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance +that is the file handle the requested information is for. + @param[in] InformationType The type identifier for the information being +requested. + @param[in out] BufferSize On input, the size of Buffer. On output, the +amount of data returned in Buffer. In both cases, the size is measured in bytes. + @param[out] Buffer A pointer to the data buffer to return. The +buffer's type is indicated by InformationType. + + @retval EFI_SUCCESS The information was returned. + @retval EFI_UNSUPPORTED The InformationType is not known. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_BUFFER_TOO_SMALL The BufferSize is too small to read the current +directory entry. BufferSize has been updated with the size needed to complete + the request. +**/ +EFI_STATUS +EFIAPI +Ext4GetInfo ( + IN EFI_FILE_PROTOCOL *This, + IN EFI_GUID *InformationType, + IN OUT UINTN *BufferSize, + OUT VOID *Buffer + ); + +/** + Sets information about a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that +is the file handle the information is for. + @param[in] InformationType The type identifier for the information being set. + @param[in] BufferSize The size, in bytes, of Buffer. + @param[in] Buffer A pointer to the data buffer to write. The +buffer's type is indicated by InformationType. + + @retval EFI_SUCCESS The information was set. + @retval EFI_UNSUPPORTED The InformationType is not known. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED InformationType is EFI_FILE_INFO_ID and the media +is read-only. + @retval EFI_WRITE_PROTECTED InformationType is +EFI_FILE_PROTOCOL_SYSTEM_INFO_ID and the media is read only. + @retval EFI_WRITE_PROTECTED InformationType is +EFI_FILE_SYSTEM_VOLUME_LABEL_ID and the media is read-only. + @retval EFI_ACCESS_DENIED An attempt is made to change the name of a file +to a file that is already present. + @retval EFI_ACCESS_DENIED An attempt is being made to change the +EFI_FILE_DIRECTORY Attribute. + @retval EFI_ACCESS_DENIED An attempt is being made to change the size of a +directory. + @retval EFI_ACCESS_DENIED InformationType is EFI_FILE_INFO_ID and the file +was opened read-only and an attempt is being made to modify a field other than +Attribute. + @retval EFI_VOLUME_FULL The volume is full. + @retval EFI_BAD_BUFFER_SIZE BufferSize is smaller than the size of the type +indicated by InformationType. + +**/ +EFI_STATUS +EFIAPI +Ext4SetInfo ( + IN EFI_FILE_PROTOCOL *This, + IN EFI_GUID *InformationType, + IN UINTN BufferSize, + IN VOID *Buffer + ); + +// EFI_FILE_PROTOCOL implementation ends here. + +/** + Checks if a file is a directory. + @param[in] File Pointer to the opened file. + + @return TRUE if file is a directory. +**/ +BOOLEAN +Ext4FileIsDir ( + IN CONST EXT4_FILE *File + ); + +/** + Checks if a file is a symlink. + + @param[in] File Pointer to the opened file. + + @return BOOLEAN Whether file is a symlink +**/ +BOOLEAN +Ext4FileIsSymlink ( + IN CONST EXT4_FILE *File + ); + +/** + Checks if a file is a regular file. + @param[in] File Pointer to the opened file. + + @return BOOLEAN TRUE if file is a regular file. +**/ +BOOLEAN +Ext4FileIsReg ( + IN CONST EXT4_FILE *File + ); + +// In EFI we can't open FIFO pipes, UNIX sockets, character/block devices since +// these concepts are at the kernel level and are OS dependent. + +/** + Checks if a file is openable. + @param[in] File Pointer to the file trying to be opened. + + + @return TRUE if file is openable. A file is considered openable if + it's a regular file or a directory, since most other file types + don't make sense under UEFI. +**/ +#define Ext4FileIsOpenable(File) (Ext4FileIsReg (File) || Ext4FileIsDir (File) || Ext4FileIsSymlink (File)) + +#define EXT4_INODE_HAS_FIELD(Inode, Field) \ + (Inode->i_extra_isize + EXT4_GOOD_OLD_INODE_SIZE >= \ + OFFSET_OF(EXT4_INODE, Field) + sizeof(((EXT4_INODE *)NULL)->Field)) + +/** + Calculates the physical space used by a file. + @param[in] File Pointer to the opened file. + + @return Physical space used by a file, in bytes. +**/ +UINT64 +Ext4FilePhysicalSpace ( + IN EXT4_FILE *File + ); + +/** + Gets the file's last access time. + @param[in] File Pointer to the opened file. + @param[out] Time Pointer to an EFI_TIME structure. +**/ +VOID +Ext4FileATime ( + IN EXT4_FILE *File, + OUT EFI_TIME *Time + ); + +/** + Gets the file's last (data) modification time. + @param[in] File Pointer to the opened file. + @param[out] Time Pointer to an EFI_TIME structure. +**/ +VOID +Ext4FileMTime ( + IN EXT4_FILE *File, + OUT EFI_TIME *Time + ); + +/** + Gets the file's creation time, if possible. + @param[in] File Pointer to the opened file. + @param[out] Time Pointer to an EFI_TIME structure. + In the case where the the creation time isn't +recorded, Time is zeroed. +**/ +VOID +Ext4FileCreateTime ( + IN EXT4_FILE *File, + OUT EFI_TIME *Time + ); + +/** + Initialises Unicode collation, which is needed for case-insensitive string +comparisons within the driver (a good example of an application of this is +filename comparison). + + @param[in] DriverHandle Handle to the driver image. + + @retval EFI_SUCCESS Unicode collation was successfully initialised. + @retval !EFI_SUCCESS Failure. +**/ +EFI_STATUS +Ext4InitialiseUnicodeCollation ( + EFI_HANDLE DriverHandle + ); + +/** + Does a case-insensitive string comparison. Refer to +EFI_UNICODE_COLLATION_PROTOCOL's StriColl for more details. + + @param[in] Str1 Pointer to a null terminated string. + @param[in] Str2 Pointer to a null terminated string. + + @retval 0 Str1 is equivalent to Str2. + @retval >0 Str1 is lexically greater than Str2. + @retval <0 Str1 is lexically less than Str2. +**/ +INTN +Ext4StrCmpInsensitive ( + IN CHAR16 *Str1, + IN CHAR16 *Str2 + ); + +/** + Retrieves the filename of the directory entry and converts it to UTF-16/UCS-2 + + @param[in] Entry Pointer to a EXT4_DIR_ENTRY. + @param[out] Ucs2FileName Pointer to an array of CHAR16's, of size +EXT4_NAME_MAX + 1. + + @retval EFI_SUCCESS Unicode collation was successfully initialised. + @retval !EFI_SUCCESS Failure. +**/ +EFI_STATUS +Ext4GetUcs2DirentName ( + IN EXT4_DIR_ENTRY *Entry, + OUT CHAR16 Ucs2FileName[EXT4_NAME_MAX + 1] + ); + +/** + Retrieves information about the file and stores it in the EFI_FILE_INFO +format. + + @param[in] File Pointer to an opened file. + @param[out] Info Pointer to a EFI_FILE_INFO. + @param[in out] BufferSize Pointer to the buffer size + + @return Status of the file information request. +**/ +EFI_STATUS +Ext4GetFileInfo ( + IN EXT4_FILE *File, + OUT EFI_FILE_INFO *Info, + IN OUT UINTN *BufferSize + ); + +/** + Reads a directory entry. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] File Pointer to the open directory. + @param[out] Buffer Pointer to the output buffer. + @param[in] Offset Initial directory position. + @param[in out] OutLength Pointer to a UINTN that contains the length of +the buffer, and the length of the actual EFI_FILE_INFO after the call. + + @return Result of the operation. +**/ +EFI_STATUS +Ext4ReadDir ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT VOID *Buffer, + IN UINT64 Offset, + IN OUT UINTN *OutLength + ); + +/** + Initialises the (empty) extents map, that will work as a cache of extents. + + @param[in] File Pointer to the open file. + + @return Result of the operation. +**/ +EFI_STATUS +Ext4InitExtentsMap ( + IN EXT4_FILE *File + ); + +/** + Frees the extents map, deleting every extent stored. + + @param[in] File Pointer to the open file. +**/ +VOID +Ext4FreeExtentsMap ( + IN EXT4_FILE *File + ); + +/** + Calculates the checksum of the given buffer. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] Buffer Pointer to the buffer. + @param[in] Length Length of the buffer, in bytes. + @param[in] InitialValue Initial value of the CRC. + + @return The checksum. +**/ +UINT32 +Ext4CalculateChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST VOID *Buffer, + IN UINTN Length, + IN UINT32 InitialValue + ); + +/** + Calculates the checksum of the given inode. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] Inode Pointer to the inode. + @param[in] InodeNum Inode number. + + @return The checksum. +**/ +UINT32 +Ext4CalculateInodeChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_INODE *Inode, + IN EXT4_INO_NR InodeNum + ); + +/** + Checks if the checksum of the inode is correct. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] Inode Pointer to the inode. + @param[in] InodeNum Inode number. + + @return TRUE if checksum is correct, FALSE if there is corruption. +**/ +BOOLEAN +Ext4CheckInodeChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_INODE *Inode, + IN EXT4_INO_NR InodeNum + ); + +/** + Unmounts and frees an ext4 partition. + + @param[in] Partition Pointer to the opened partition. + + @return Status of the unmount. +**/ +EFI_STATUS +Ext4UnmountAndFreePartition ( + IN EXT4_PARTITION *Partition + ); + +/** + Checks if the checksum of the block group descriptor is correct. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] BlockGroupDesc Pointer to the block group descriptor. + @param[in] BlockGroupNum Number of the block group. + + @return TRUE if checksum is correct, FALSE if there is corruption. +**/ +BOOLEAN +Ext4VerifyBlockGroupDescChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_BLOCK_GROUP_DESC *BlockGroupDesc, + IN UINT32 BlockGroupNum + ); + +/** + Calculates the checksum of the block group descriptor. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] BlockGroupDesc Pointer to the block group descriptor. + @param[in] BlockGroupNum Number of the block group. + + @return The checksum. +**/ +UINT16 +Ext4CalculateBlockGroupDescChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_BLOCK_GROUP_DESC *BlockGroupDesc, + IN UINT32 BlockGroupNum + ); + +/** + Verifies the existance of a particular RO compat feature set. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] RoCompatFeatureSet Feature set to test. + + @return TRUE if all features are supported, else FALSE. +**/ +#define EXT4_HAS_RO_COMPAT(Partition, RoCompatFeatureSet) \ + ((Partition->FeaturesRoCompat & RoCompatFeatureSet) == RoCompatFeatureSet) + +/** + Verifies the existance of a particular compat feature set. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] CompatFeatureSet Feature set to test. + + @return TRUE if all features are supported, else FALSE. +**/ +#define EXT4_HAS_COMPAT(Partition, CompatFeatureSet) \ + ((Partition->FeaturesCompat & CompatFeatureSet) == CompatFeatureSet) + +/** + Verifies the existance of a particular compat feature set. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] IncompatFeatureSet Feature set to test. + + @return TRUE if all features are supported, else FALSE. +**/ +#define EXT4_HAS_INCOMPAT(Partition, IncompatFeatureSet) \ + ((Partition->FeaturesIncompat & IncompatFeatureSet) == IncompatFeatureSet) + +// Note: Might be a good idea to provide generic Ext4Has$feature() through +// macros. + +/** + Checks if metadata_csum is enabled on the partition. + @param[in] Partition Pointer to the opened EXT4 partition. + + @return TRUE if the metadata_csum is supported, else FALSE. +**/ +#define EXT4_HAS_METADATA_CSUM(Partition) \ + EXT4_HAS_RO_COMPAT(Partition, EXT4_FEATURE_RO_COMPAT_METADATA_CSUM) + +/** + Checks if gdt_csum is enabled on the partition. + @param[in] Partition Pointer to the opened EXT4 partition. + + @return TRUE if the gdt_csum is supported, else FALSE. +**/ +#define EXT4_HAS_GDT_CSUM(Partition) \ + EXT4_HAS_RO_COMPAT(Partition, EXT4_FEATURE_RO_COMPAT_GDT_CSUM) + +/** + Retrieves the volume name. + + @param[in] Part Pointer to the opened partition. + @param[out] Info Pointer to a CHAR16*. + @param[out] BufferSize Pointer to a UINTN, where the string length + of the name will be put. + + @return Status of the volume name request. +**/ +EFI_STATUS +Ext4GetVolumeName ( + IN EXT4_PARTITION *Partition, + OUT CHAR16 **OutVolName, + OUT UINTN *VolNameLen + ); + +/** + Checks the superblock's magic value. + + @param[in] DiskIo Pointer to the DiskIo. + @param[in] BlockIo Pointer to the BlockIo. + + @returns Whether the partition has a valid EXT4 superblock magic value. +**/ +BOOLEAN +Ext4SuperblockCheckMagic ( + IN EFI_DISK_IO_PROTOCOL *DiskIo, + IN EFI_BLOCK_IO_PROTOCOL *BlockIo + ); + +/** + Check if the extent is uninitialized + + @param[in] Extent Pointer to the EXT4_EXTENT + + @returns True if uninitialized, else false. +**/ +#define EXT4_EXTENT_IS_UNINITIALIZED(Extent) \ + ((Extent)->ee_len > EXT4_EXTENT_MAX_INITIALIZED) + +/** + Retrieves the extent's length, dealing with uninitialized extents in the +process. + + @param[in] Extent Pointer to the EXT4_EXTENT + + @returns Extent's length, in filesystem blocks. +**/ +EXT4_BLOCK_NR +Ext4GetExtentLength ( + IN CONST EXT4_EXTENT *Extent + ); + +/** + Retrieves an extent from an EXT2/3 inode (with a blockmap). + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] File Pointer to the opened file. + @param[in] LogicalBlock Block number which the returned extent must cover. + @param[out] Extent Pointer to the output buffer, where the extent will be copied to. + + @retval EFI_SUCCESS Retrieval was succesful. + @retval EFI_NO_MAPPING Block has no mapping. +**/ +EFI_STATUS +Ext4GetBlocks ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + IN EXT2_BLOCK_NR LogicalBlock, + OUT EXT4_EXTENT *Extent + ); + +#endif diff --git a/Ext4Pkg/Ext4Dxe/Ext4Dxe.inf b/Ext4Pkg/Ext4Dxe/Ext4Dxe.inf new file mode 100644 index 0000000000..a153fc41cc --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Ext4Dxe.inf @@ -0,0 +1,149 @@ +## @file +# Ext4 Package +# +# UEFI Driver that produces the Simple File System Protocol for a partition that is formatted +# with the EXT4 file system. +# More details are available at: https://www.kernel.org/doc/html/v5.4/filesystems/ext4/index.html +# +# Copyright (c) 2021 Pedro Falcato +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +# Layout of an EXT2/3/4 filesystem: +# (note: this driver has been developed using +# https://www.kernel.org/doc/html/latest/filesystems/ext4/index.html as +# documentation). +# +# An ext2/3/4 filesystem (here on out referred to as simply an ext4 filesystem, +# due to the similarities) is composed of various concepts: +# +# 1) Superblock +# The superblock is the structure near (1024 bytes offset from the start) +# the start of the partition, and describes the filesystem in general. +# Here, we get to know the size of the filesystem's blocks, which features +# it supports or not, whether it's been cleanly unmounted, how many blocks +# we have, etc. +# +# 2) Block groups +# EXT4 filesystems are divided into block groups, and each block group covers +# s_blocks_per_group(8 * Block Size) blocks. Each block group has an +# associated block group descriptor; these are present directly after the +# superblock. Each block group descriptor contains the location of the +# inode table, and the inode and block bitmaps (note these bitmaps are only +# a block long, which gets us the 8 * Block Size formula covered previously). +# +# 3) Blocks +# The ext4 filesystem is divided in blocks, of size s_log_block_size ^ 1024. +# Blocks can be allocated using individual block groups's bitmaps. Note +# that block 0 is invalid and its presence on extents/block tables means +# it's part of a file hole, and that particular location must be read as +# a block full of zeros. +# +# 4) Inodes +# The ext4 filesystem divides files/directories into inodes (originally +# index nodes). Each file/socket/symlink/directory/etc (here on out referred +# to as a file, since there is no distinction under the ext4 filesystem) is +# stored as a /nameless/ inode, that is stored in some block group's inode +# table. Each inode has s_inode_size size (or GOOD_OLD_INODE_SIZE if it's +# an old filesystem), and holds various metadata about the file. Since the +# largest inode structure right now is ~160 bytes, the rest of the inode +# contains inline extended attributes. Inodes' data is stored using either +# data blocks (under ext2/3) or extents (under ext4). +# +# 5) Extents +# Ext4 inodes store data in extents. These let N contiguous logical blocks +# that are represented by N contiguous physical blocks be represented by a +# single extent structure, which minimizes filesystem metadata bloat and +# speeds up block mapping (particularly due to the fact that high-quality +# ext4 implementations like linux's try /really/ hard to make the file +# contiguous, so it's common to have files with almost 0 fragmentation). +# Inodes that use extents store them in a tree, and the top of the tree +# is stored on i_data. The tree's leaves always start with an +# EXT4_EXTENT_HEADER and contain EXT4_EXTENT_INDEX on eh_depth != 0 and +# EXT4_EXTENT on eh_depth = 0; these entries are always sorted by logical +# block. +# +# 6) Directories +# Ext4 directories are files that store name -> inode mappings for the +# logical directory; this is where files get their names, which means ext4 +# inodes do not themselves have names, since they can be linked (present) +# multiple times with different names. Directories can store entries in two +# different ways: +# 1) Classical linear directories: They store entries as a mostly-linked +# mostly-list of EXT4_DIR_ENTRY. +# 2) Hash tree directories: These are used for larger directories, with +# hundreds of entries, and are designed in a backwards compatible way. +# These are not yet implemented in the Ext4Dxe driver. +# +# 7) Journal +# Ext3/4 filesystems have a journal to help protect the filesystem against +# system crashes. This is not yet implemented in Ext4Dxe but is described +# in detail in the Linux kernel's documentation. +## + + +[Defines] + INF_VERSION = 0x00010005 + BASE_NAME = Ext4Dxe + MODULE_UNI_FILE = Ext4Dxe.uni + FILE_GUID = 75F2B676-D73B-45CB-B7C1-303C7F4E6FD6 + MODULE_TYPE = UEFI_DRIVER + VERSION_STRING = 1.0 + + ENTRY_POINT = Ext4EntryPoint + UNLOAD_IMAGE = Ext4Unload + +# +# The following information is for reference only and not required by the build tools. +# +# VALID_ARCHITECTURES = IA32 X64 EBC +# + +[Sources] + Ext4Dxe.c + Partition.c + DiskUtil.c + Superblock.c + BlockGroup.c + Inode.c + Directory.c + Extents.c + File.c + Symlink.c + Collation.c + Ext4Disk.h + Ext4Dxe.h + BlockMap.c + +[Packages] + MdePkg/MdePkg.dec + RedfishPkg/RedfishPkg.dec + +[LibraryClasses] + UefiRuntimeServicesTableLib + UefiBootServicesTableLib + MemoryAllocationLib + BaseMemoryLib + BaseLib + UefiLib + UefiDriverEntryPoint + DebugLib + PcdLib + OrderedCollectionLib + BaseUcs2Utf8Lib + +[Guids] + gEfiFileInfoGuid ## SOMETIMES_CONSUMES ## UNDEFINED + gEfiFileSystemInfoGuid ## SOMETIMES_CONSUMES ## UNDEFINED + gEfiFileSystemVolumeLabelInfoIdGuid ## SOMETIMES_CONSUMES ## UNDEFINED + +[Protocols] + gEfiDiskIoProtocolGuid ## TO_START + gEfiDiskIo2ProtocolGuid ## TO_START + gEfiBlockIoProtocolGuid ## TO_START + gEfiSimpleFileSystemProtocolGuid ## BY_START + gEfiUnicodeCollationProtocolGuid ## TO_START + gEfiUnicodeCollation2ProtocolGuid ## TO_START + +[Pcd] + gEfiMdePkgTokenSpaceGuid.PcdUefiVariableDefaultLang ## SOMETIMES_CONSUMES + gEfiMdePkgTokenSpaceGuid.PcdUefiVariableDefaultPlatformLang ## SOMETIMES_CONSUMES diff --git a/Ext4Pkg/Ext4Dxe/Ext4Dxe.uni b/Ext4Pkg/Ext4Dxe/Ext4Dxe.uni new file mode 100644 index 0000000000..7476fbf9bd --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Ext4Dxe.uni @@ -0,0 +1,15 @@ +## @file +# Ext4 Package +# +# UEFI Driver that produces the Simple File System Protocol for a partition that is formatted +# with the EXT4 file system. +# More details are available at: https://www.kernel.org/doc/html/v5.4/filesystems/ext4/index.html +# +# Copyright (c) 2021 Pedro Falcato +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +#string STR_MODULE_ABSTRACT #language en-US "UEFI driver for the EXT4 file system." + +#string STR_MODULE_DESCRIPTION #language en-US "Produces the EFI Simple File System protocol." diff --git a/Ext4Pkg/Ext4Dxe/Extents.c b/Ext4Pkg/Ext4Dxe/Extents.c new file mode 100644 index 0000000000..f1964426d2 --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Extents.c @@ -0,0 +1,624 @@ +/** @file + Extent related routines + + Copyright (c) 2021 - 2022 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +/** + Checks if the checksum of the extent data block is correct. + @param[in] ExtHeader Pointer to the EXT4_EXTENT_HEADER. + @param[in] File Pointer to the file. + + @return TRUE if the checksum is correct, FALSE if there is corruption. +**/ +BOOLEAN +Ext4CheckExtentChecksum ( + IN CONST EXT4_EXTENT_HEADER *ExtHeader, + IN CONST EXT4_FILE *File + ); + +/** + Calculates the checksum of the extent data block. + @param[in] ExtHeader Pointer to the EXT4_EXTENT_HEADER. + @param[in] File Pointer to the file. + + @return The checksum. +**/ +UINT32 +Ext4CalculateExtentChecksum ( + IN CONST EXT4_EXTENT_HEADER *ExtHeader, + IN CONST EXT4_FILE *File + ); + +/** + Caches a range of extents, by allocating pool memory for each extent and adding it to the tree. + + @param[in] File Pointer to the open file. + @param[in] Extents Pointer to an array of extents. + @param[in] NumberExtents Length of the array. +**/ +VOID +Ext4CacheExtents ( + IN EXT4_FILE *File, + IN CONST EXT4_EXTENT *Extents, + IN UINT16 NumberExtents + ); + +/** + Gets an extent from the extents cache of the file. + + @param[in] File Pointer to the open file. + @param[in] Block Block we want to grab. + + @return Pointer to the extent, or NULL if it was not found. +**/ +EXT4_EXTENT * +Ext4GetExtentFromMap ( + IN EXT4_FILE *File, + IN UINT32 Block + ); + +/** + Retrieves the pointer to the top of the extent tree. + @param[in] Inode Pointer to the inode structure. + + @return Pointer to an EXT4_EXTENT_HEADER. This pointer is inside + the inode and must not be freed. +**/ +STATIC +EXT4_EXTENT_HEADER * +Ext4GetInoExtentHeader ( + IN EXT4_INODE *Inode + ) +{ + return (EXT4_EXTENT_HEADER *)Inode->i_data; +} + +/** + Checks if an extent header is valid. + @param[in] Header Pointer to the EXT4_EXTENT_HEADER structure. + + @return TRUE if valid, FALSE if not. +**/ +STATIC +BOOLEAN +Ext4ExtentHeaderValid ( + IN CONST EXT4_EXTENT_HEADER *Header + ) +{ + if (Header->eh_depth > EXT4_EXTENT_TREE_MAX_DEPTH) { + DEBUG ((DEBUG_ERROR, "[ext4] Invalid extent header depth %u\n", Header->eh_depth)); + return FALSE; + } + + if (Header->eh_magic != EXT4_EXTENT_HEADER_MAGIC) { + DEBUG ((DEBUG_ERROR, "[ext4] Invalid extent header magic %x\n", Header->eh_magic)); + return FALSE; + } + + if (Header->eh_max < Header->eh_entries) { + DEBUG (( + DEBUG_ERROR, + "[ext4] Invalid extent header num entries %u max entries %u\n", + Header->eh_entries, + Header->eh_max + )); + return FALSE; + } + + return TRUE; +} + +/** + Performs a binary search for a EXT4_EXTENT_INDEX that corresponds to a + logical block in a given extent tree node. + + @param[in] Header Pointer to the EXT4_EXTENT_HEADER structure. + @param[in] LogicalBlock Block that will be searched + + @return Pointer to the found EXT4_EXTENT_INDEX. +**/ +STATIC +EXT4_EXTENT_INDEX * +Ext4BinsearchExtentIndex ( + IN EXT4_EXTENT_HEADER *Header, + IN EXT4_BLOCK_NR LogicalBlock + ) +{ + EXT4_EXTENT_INDEX *l; + EXT4_EXTENT_INDEX *r; + EXT4_EXTENT_INDEX *m; + + l = ((EXT4_EXTENT_INDEX *)(Header + 1)) + 1; + r = ((EXT4_EXTENT_INDEX *)(Header + 1)) + Header->eh_entries - 1; + + // Perform a mostly-standard binary search on the array + // This works very nicely because the extents arrays are always sorted. + + while (l <= r) { + m = l + (r - l) / 2; + + if (LogicalBlock < m->ei_block) { + r = m - 1; + } else { + l = m + 1; + } + } + + return l - 1; +} + +/** + Performs a binary search for a EXT4_EXTENT that corresponds to a + logical block in a given extent tree node. + + @param[in] Header Pointer to the EXT4_EXTENT_HEADER structure. + @param[in] LogicalBlock Block that will be searched + + @return Pointer to the found EXT4_EXTENT_INDEX, else NULL if the array is empty. + Note: The caller must check if the logical block + is actually mapped under the given extent. +**/ +STATIC +EXT4_EXTENT * +Ext4BinsearchExtentExt ( + IN EXT4_EXTENT_HEADER *Header, + IN EXT4_BLOCK_NR LogicalBlock + ) +{ + EXT4_EXTENT *l; + EXT4_EXTENT *r; + EXT4_EXTENT *m; + + l = ((EXT4_EXTENT *)(Header + 1)) + 1; + r = ((EXT4_EXTENT *)(Header + 1)) + Header->eh_entries - 1; + // Perform a mostly-standard binary search on the array + // This works very nicely because the extents arrays are always sorted. + + // Empty array + if (Header->eh_entries == 0) { + return NULL; + } + + while (l <= r) { + m = l + (r - l) / 2; + + if (LogicalBlock < m->ee_block) { + r = m - 1; + } else { + l = m + 1; + } + } + + return l - 1; +} + +/** + Retrieves the leaf block from an EXT4_EXTENT_INDEX. + + @param[in] Index Pointer to the EXT4_EXTENT_INDEX structure. + + @return Block number of the leaf node. +**/ +STATIC +EXT4_BLOCK_NR +Ext4ExtentIdxLeafBlock ( + IN EXT4_EXTENT_INDEX *Index + ) +{ + return LShiftU64 (Index->ei_leaf_hi, 32) | Index->ei_leaf_lo; +} + +/** + Retrieves an extent from an EXT4 inode. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] File Pointer to the opened file. + @param[in] LogicalBlock Block number which the returned extent must cover. + @param[out] Extent Pointer to the output buffer, where the extent will be copied to. + + @retval EFI_SUCCESS Retrieval was succesful. + @retval EFI_NO_MAPPING Block has no mapping. +**/ +EFI_STATUS +Ext4GetExtent ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + IN EXT4_BLOCK_NR LogicalBlock, + OUT EXT4_EXTENT *Extent + ) +{ + EXT4_INODE *Inode; + VOID *Buffer; + EXT4_EXTENT *Ext; + UINT32 CurrentDepth; + EXT4_EXTENT_HEADER *ExtHeader; + EXT4_EXTENT_INDEX *Index; + EFI_STATUS Status; + + Inode = File->Inode; + Ext = NULL; + Buffer = NULL; + + DEBUG ((DEBUG_FS, "[ext4] Looking up extent for block %lu\n", LogicalBlock)); + + // ext4 does not have support for logical block numbers bigger than UINT32_MAX + if (LogicalBlock > (UINT32)-1) { + return EFI_NO_MAPPING; + } + + // Note: Right now, holes are the single biggest reason for cache misses + // We should find a way to get (or cache) holes + if ((Ext = Ext4GetExtentFromMap (File, (UINT32)LogicalBlock)) != NULL) { + *Extent = *Ext; + + return EFI_SUCCESS; + } + + if ((Inode->i_flags & EXT4_EXTENTS_FL) == 0) { + // If this is an older ext2/ext3 filesystem, emulate Ext4GetExtent using the block map + // By specification files using block maps are limited to 2^32 blocks, + // so we can safely cast LogicalBlock to uint32 + Status = Ext4GetBlocks (Partition, File, (UINT32)LogicalBlock, Extent); + + if (!EFI_ERROR (Status)) { + Ext4CacheExtents (File, Extent, 1); + } + + return Status; + } + + // Slow path, we'll need to read from disk and (try to) cache those extents. + + ExtHeader = Ext4GetInoExtentHeader (Inode); + + if (!Ext4ExtentHeaderValid (ExtHeader)) { + return EFI_VOLUME_CORRUPTED; + } + + CurrentDepth = ExtHeader->eh_depth; + + while (ExtHeader->eh_depth != 0) { + CurrentDepth--; + // While depth != 0, we're traversing the tree itself and not any leaves + // As such, every entry is an EXT4_EXTENT_INDEX entry + // Note: Entries after the extent header, either index or actual extent, are always sorted. + // Therefore, we can use binary search, and it's actually the standard for doing so + // (see FreeBSD). + + Index = Ext4BinsearchExtentIndex (ExtHeader, LogicalBlock); + + if (Buffer == NULL) { + Buffer = AllocatePool (Partition->BlockSize); + if (Buffer == NULL) { + return EFI_OUT_OF_RESOURCES; + } + } + + // Read the leaf block onto the previously-allocated buffer. + + Status = Ext4ReadBlocks (Partition, Buffer, 1, Ext4ExtentIdxLeafBlock (Index)); + if (EFI_ERROR (Status)) { + FreePool (Buffer); + return Status; + } + + ExtHeader = Buffer; + + if (!Ext4ExtentHeaderValid (ExtHeader)) { + FreePool (Buffer); + return EFI_VOLUME_CORRUPTED; + } + + if (!Ext4CheckExtentChecksum (ExtHeader, File)) { + DEBUG ((DEBUG_ERROR, "[ext4] Invalid extent checksum\n")); + FreePool (Buffer); + return EFI_VOLUME_CORRUPTED; + } + + if (ExtHeader->eh_depth != CurrentDepth) { + FreePool (Buffer); + return EFI_VOLUME_CORRUPTED; + } + } + + /* We try to cache every extent under a single leaf, since it's quite likely that we + * may need to access things sequentially. Furthermore, ext4 block allocation as done + * by linux (and possibly other systems) is quite fancy and usually it results in a small number of extents. + * Therefore, we shouldn't have any memory issues. + **/ + Ext4CacheExtents (File, (EXT4_EXTENT *)(ExtHeader + 1), ExtHeader->eh_entries); + + Ext = Ext4BinsearchExtentExt (ExtHeader, LogicalBlock); + + if (!Ext) { + if (Buffer != NULL) { + FreePool (Buffer); + } + + return EFI_NO_MAPPING; + } + + if (!((LogicalBlock >= Ext->ee_block) && (Ext->ee_block + Ext4GetExtentLength (Ext) > LogicalBlock))) { + // This extent does not cover the block + if (Buffer != NULL) { + FreePool (Buffer); + } + + return EFI_NO_MAPPING; + } + + *Extent = *Ext; + + if (Buffer != NULL) { + FreePool (Buffer); + } + + return EFI_SUCCESS; +} + +/** + Compare two EXT4_EXTENT structs. + Used in the extent map's ORDERED_COLLECTION. + + @param[in] UserStruct1 Pointer to the first user structure. + + @param[in] UserStruct2 Pointer to the second user structure. + + @retval <0 If UserStruct1 compares less than UserStruct2. + + @retval 0 If UserStruct1 compares equal to UserStruct2. + + @retval >0 If UserStruct1 compares greater than UserStruct2. +**/ +STATIC +INTN +EFIAPI +Ext4ExtentsMapStructCompare ( + IN CONST VOID *UserStruct1, + IN CONST VOID *UserStruct2 + ) +{ + CONST EXT4_EXTENT *Extent1; + CONST EXT4_EXTENT *Extent2; + + Extent1 = UserStruct1; + Extent2 = UserStruct2; + + return Extent1->ee_block < Extent2->ee_block ? -1 : + Extent1->ee_block > Extent2->ee_block ? 1 : 0; +} + +/** + Compare a standalone key against a EXT4_EXTENT containing an embedded key. + Used in the extent map's ORDERED_COLLECTION. + + @param[in] StandaloneKey Pointer to the bare key. + + @param[in] UserStruct Pointer to the user structure with the embedded + key. + + @retval <0 If StandaloneKey compares less than UserStruct's key. + + @retval 0 If StandaloneKey compares equal to UserStruct's key. + + @retval >0 If StandaloneKey compares greater than UserStruct's key. +**/ +STATIC +INTN +EFIAPI +Ext4ExtentsMapKeyCompare ( + IN CONST VOID *StandaloneKey, + IN CONST VOID *UserStruct + ) +{ + CONST EXT4_EXTENT *Extent; + UINT32 Block; + + // Note that logical blocks are 32-bits in size so no truncation can happen here + // with regards to 32-bit architectures. + Extent = UserStruct; + Block = (UINT32)(UINTN)StandaloneKey; + + if ((Block >= Extent->ee_block) && (Block - Extent->ee_block < Ext4GetExtentLength (Extent))) { + return 0; + } + + return Block < Extent->ee_block ? -1 : + Block > Extent->ee_block ? 1 : 0; +} + +/** + Initialises the (empty) extents map, that will work as a cache of extents. + + @param[in] File Pointer to the open file. + + @return Result of the operation. +**/ +EFI_STATUS +Ext4InitExtentsMap ( + IN EXT4_FILE *File + ) +{ + File->ExtentsMap = OrderedCollectionInit (Ext4ExtentsMapStructCompare, Ext4ExtentsMapKeyCompare); + if (!File->ExtentsMap) { + return EFI_OUT_OF_RESOURCES; + } + + return EFI_SUCCESS; +} + +/** + Frees the extents map, deleting every extent stored. + + @param[in] File Pointer to the open file. +**/ +VOID +Ext4FreeExtentsMap ( + IN EXT4_FILE *File + ) +{ + // Keep calling Min(), so we get an arbitrary node we can delete. + // If Min() returns NULL, it's empty. + + ORDERED_COLLECTION_ENTRY *MinEntry; + EXT4_EXTENT *Ext; + + MinEntry = NULL; + + while ((MinEntry = OrderedCollectionMin (File->ExtentsMap)) != NULL) { + OrderedCollectionDelete (File->ExtentsMap, MinEntry, (VOID **)&Ext); + FreePool (Ext); + } + + ASSERT (OrderedCollectionIsEmpty (File->ExtentsMap)); + + OrderedCollectionUninit (File->ExtentsMap); + File->ExtentsMap = NULL; +} + +/** + Caches a range of extents, by allocating pool memory for each extent and adding it to the tree. + + @param[in] File Pointer to the open file. + @param[in] Extents Pointer to an array of extents. + @param[in] NumberExtents Length of the array. +**/ +VOID +Ext4CacheExtents ( + IN EXT4_FILE *File, + IN CONST EXT4_EXTENT *Extents, + IN UINT16 NumberExtents + ) +{ + UINT16 Idx; + EXT4_EXTENT *Extent; + EFI_STATUS Status; + + /* Note that any out of memory condition might mean we don't get to cache a whole leaf of extents + * in which case, future insertions might fail. + */ + + for (Idx = 0; Idx < NumberExtents; Idx++, Extents++) { + Extent = AllocatePool (sizeof (EXT4_EXTENT)); + + if (Extent == NULL) { + return; + } + + CopyMem (Extent, Extents, sizeof (EXT4_EXTENT)); + Status = OrderedCollectionInsert (File->ExtentsMap, NULL, Extent); + + // EFI_ALREADY_STARTED = already exists in the tree. + if (EFI_ERROR (Status)) { + FreePool (Extent); + + if (Status == EFI_ALREADY_STARTED) { + continue; + } + + return; + } + } +} + +/** + Gets an extent from the extents cache of the file. + + @param[in] File Pointer to the open file. + @param[in] Block Block we want to grab. + + @return Pointer to the extent, or NULL if it was not found. +**/ +EXT4_EXTENT * +Ext4GetExtentFromMap ( + IN EXT4_FILE *File, + IN UINT32 Block + ) +{ + ORDERED_COLLECTION_ENTRY *Entry; + + Entry = OrderedCollectionFind (File->ExtentsMap, (CONST VOID *)(UINTN)Block); + + if (Entry == NULL) { + return NULL; + } + + return OrderedCollectionUserStruct (Entry); +} + +/** + Calculates the checksum of the extent data block. + @param[in] ExtHeader Pointer to the EXT4_EXTENT_HEADER. + @param[in] File Pointer to the file. + + @return The checksum. +**/ +UINT32 +Ext4CalculateExtentChecksum ( + IN CONST EXT4_EXTENT_HEADER *ExtHeader, + IN CONST EXT4_FILE *File + ) +{ + UINT32 Csum; + EXT4_PARTITION *Partition; + EXT4_INODE *Inode; + + Partition = File->Partition; + Inode = File->Inode; + + Csum = Ext4CalculateChecksum (Partition, &File->InodeNum, sizeof (EXT4_INO_NR), Partition->InitialSeed); + Csum = Ext4CalculateChecksum (Partition, &Inode->i_generation, sizeof (Inode->i_generation), Csum); + Csum = Ext4CalculateChecksum (Partition, ExtHeader, Partition->BlockSize - sizeof (EXT4_EXTENT_TAIL), Csum); + + return Csum; +} + +/** + Checks if the checksum of the extent data block is correct. + @param[in] ExtHeader Pointer to the EXT4_EXTENT_HEADER. + @param[in] File Pointer to the file. + + @return TRUE if the checksum is correct, FALSE if there is corruption. +**/ +BOOLEAN +Ext4CheckExtentChecksum ( + IN CONST EXT4_EXTENT_HEADER *ExtHeader, + IN CONST EXT4_FILE *File + ) +{ + EXT4_PARTITION *Partition; + EXT4_EXTENT_TAIL *Tail; + + Partition = File->Partition; + + if (!EXT4_HAS_METADATA_CSUM (Partition)) { + return TRUE; + } + + Tail = (EXT4_EXTENT_TAIL *)((CONST CHAR8 *)ExtHeader + (Partition->BlockSize - 4)); + + return Tail->eb_checksum == Ext4CalculateExtentChecksum (ExtHeader, File); +} + +/** + Retrieves the extent's length, dealing with uninitialized extents in the process. + + @param[in] Extent Pointer to the EXT4_EXTENT + + @returns Extent's length, in filesystem blocks. +**/ +EXT4_BLOCK_NR +Ext4GetExtentLength ( + IN CONST EXT4_EXTENT *Extent + ) +{ + // If it's an unintialized extent, the true length is ee_len - 2^15 + if (EXT4_EXTENT_IS_UNINITIALIZED (Extent)) { + return Extent->ee_len - EXT4_EXTENT_MAX_INITIALIZED; + } + + return Extent->ee_len; +} diff --git a/Ext4Pkg/Ext4Dxe/File.c b/Ext4Pkg/Ext4Dxe/File.c new file mode 100644 index 0000000000..04198a53bf --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/File.c @@ -0,0 +1,1001 @@ +/** @file + EFI_FILE_PROTOCOL implementation for EXT4 + + Copyright (c) 2021 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +#include + +/** + Reads a symlink file. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] File Pointer to the open symlink file. + @param[out] Symlink Pointer to the output unicode symlink string. + + @retval EFI_SUCCESS Symlink was read. + @retval EFI_ACCESS_DENIED Symlink is encrypted. + @retval EFI_OUT_OF_RESOURCES Memory allocation error. + @retval EFI_INVALID_PARAMETER Symlink path has incorrect length + @retval EFI_VOLUME_CORRUPTED Symlink read block size differ from inode value +**/ +EFI_STATUS +Ext4ReadSymlink ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT CHAR16 **Symlink + ); + +/** + Duplicates a file structure. + + @param[in] Original Pointer to the original file. + + @return Pointer to the new file structure. +**/ +STATIC +EXT4_FILE * +Ext4DuplicateFile ( + IN CONST EXT4_FILE *Original + ); + +/** + Gets the next path segment. + + @param[in] Path Pointer to the rest of the path. + @param[out] PathSegment Pointer to the buffer that will hold the path segment. + Note: It's necessarily EXT4_NAME_MAX +1 long. + @param[out] Length Pointer to the UINTN that will hold the length of the path segment. + + @retval !EFI_SUCCESS The path segment is too large(> EXT4_NAME_MAX). +**/ +STATIC +EFI_STATUS +GetPathSegment ( + IN CONST CHAR16 *Path, + OUT CHAR16 *PathSegment, + OUT UINTN *Length + ) +{ + CONST CHAR16 *Start; + CONST CHAR16 *End; + + Start = Path; + End = Path; + + // The path segment ends on a backslash or a null terminator + for ( ; *End != L'\0' && *End != L'\\'; End++) { + } + + *Length = End - Start; + + return StrnCpyS (PathSegment, EXT4_NAME_MAX, Start, End - Start); +} + +/** + Detects if we have more path segments on the path. + + @param[in] Path Pointer to the rest of the path. + @return True if we're on the last segment, false if there are more + segments. +**/ +STATIC +BOOLEAN +Ext4IsLastPathSegment ( + IN CONST CHAR16 *Path + ) +{ + while (Path[0] == L'\\') { + Path++; + } + + return Path[0] == '\0'; +} + +#define EXT4_INO_PERM_READ_OWNER 0400 +#define EXT4_INO_PERM_WRITE_OWNER 0200 +#define EXT4_INO_PERM_EXEC_OWNER 0100 + +/** + Detects if we have permissions to open the file on the desired mode. + + @param[in out] File Pointer to the file we're opening. + @param[in] OpenMode Mode in which to open the file. + + @return True if the open was succesful, false if we don't have + enough permissions. +**/ +STATIC +BOOLEAN +Ext4ApplyPermissions ( + IN OUT EXT4_FILE *File, + IN UINT64 OpenMode + ) +{ + UINT16 NeededPerms; + + NeededPerms = 0; + + if ((OpenMode & EFI_FILE_MODE_READ) != 0) { + NeededPerms |= EXT4_INO_PERM_READ_OWNER; + } + + if ((OpenMode & EFI_FILE_MODE_WRITE) != 0) { + NeededPerms |= EXT4_INO_PERM_WRITE_OWNER; + } + + if ((File->Inode->i_mode & NeededPerms) != NeededPerms) { + return FALSE; + } + + File->OpenMode = OpenMode; + + return TRUE; +} + +/** + Detects if we have permissions to search on the directory. + + @param[in out] File Pointer to the open directory. + + @return True if we have permission to search, else false. +**/ +STATIC +BOOLEAN +Ext4DirCanLookup ( + IN CONST EXT4_FILE *File + ) +{ + // In UNIX, executable permission on directories means that we have permission to look up + // files in a directory. + return (File->Inode->i_mode & EXT4_INO_PERM_EXEC_OWNER) == EXT4_INO_PERM_EXEC_OWNER; +} + +/** + Opens a new file relative to the source file's location. + + @param[out] FoundFile A pointer to the location to return the opened handle for the new + file. + @param[in] Source A pointer to the EXT4_FILE instance that is the file + handle to the source location. This would typically be an open + handle to a directory. + @param[in] FileName The Null-terminated string of the name of the file to be opened. + The file name may contain the following path modifiers: "\", ".", + and "..". + @param[in] OpenMode The mode to open the file. The only valid combinations that the + file may be opened with are: Read, Read/Write, or Create/Read/Write. + @param[in] Attributes Only valid for EFI_FILE_MODE_CREATE, in which case these are the + attribute bits for the newly created file. + + @retval EFI_SUCCESS The file was opened. + @retval EFI_NOT_FOUND The specified file could not be found on the device. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_MEDIA_CHANGED The device has a different medium in it or the medium is no + longer supported. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED An attempt was made to create a file, or open a file for write + when the media is write-protected. + @retval EFI_ACCESS_DENIED The service denied access to the file. + @retval EFI_OUT_OF_RESOURCES Not enough resources were available to open the file. + @retval EFI_VOLUME_FULL The volume is full. + +**/ +EFI_STATUS +Ext4OpenInternal ( + OUT EXT4_FILE **FoundFile, + IN EXT4_FILE *Source, + IN CHAR16 *FileName, + IN UINT64 OpenMode, + IN UINT64 Attributes + ) +{ + EXT4_FILE *Current; + EXT4_PARTITION *Partition; + UINTN Level; + CHAR16 PathSegment[EXT4_NAME_MAX + 1]; + UINTN Length; + EXT4_FILE *File; + CHAR16 *Symlink; + EFI_STATUS Status; + + Current = Source; + Partition = Current->Partition; + Level = 0; + + DEBUG ((DEBUG_FS, "[ext4] Ext4OpenInternal %s\n", FileName)); + // If the path starts with a backslash, we treat the root directory as the base directory + if (FileName[0] == L'\\') { + FileName++; + Current = Partition->Root; + } + + while (FileName[0] != L'\0') { + if (Partition->Root->SymLoops > SYMLOOP_MAX) { + DEBUG ((DEBUG_FS, "[ext4] Symloop limit is hit !\n")); + return EFI_ACCESS_DENIED; + } + + // Discard leading path separators + while (FileName[0] == L'\\') { + FileName++; + } + + if (GetPathSegment (FileName, PathSegment, &Length) != EFI_SUCCESS) { + return EFI_BUFFER_TOO_SMALL; + } + + // Reached the end of the path + if (Length == 0) { + break; + } + + FileName += Length; + + if (StrCmp (PathSegment, L".") == 0) { + // Opens of "." are a no-op + continue; + } + + DEBUG ((DEBUG_FS, "[ext4] Opening %s\n", PathSegment)); + + if (!Ext4FileIsDir (Current)) { + return EFI_INVALID_PARAMETER; + } + + if (!Ext4IsLastPathSegment (FileName)) { + if (!Ext4DirCanLookup (Current)) { + return EFI_ACCESS_DENIED; + } + } + + Status = Ext4OpenFile (Current, PathSegment, Partition, EFI_FILE_MODE_READ, &File); + + if (EFI_ERROR (Status) && (Status != EFI_NOT_FOUND)) { + return Status; + } else if (Status == EFI_NOT_FOUND) { + // We explicitly ignore the EFI_FILE_MODE_CREATE flag, since we don't have write support + /// If/ we add write support, this should be changed. + return Status; + } + + // Check if this is a valid file to open in EFI + if (!Ext4FileIsOpenable (File)) { + Ext4CloseInternal (File); + // This looks like an /okay/ status to return. + return EFI_ACCESS_DENIED; + } + + // + // Reading symlink and then trying to follow it + // + if (Ext4FileIsSymlink (File)) { + Partition->Root->SymLoops++; + DEBUG ((DEBUG_FS, "[ext4] File %s is symlink, trying to read it\n", PathSegment)); + Status = Ext4ReadSymlink (Partition, File, &Symlink); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_FS, "[ext4] Error reading %s symlink!\n", PathSegment)); + return Status; + } + + DEBUG ((DEBUG_FS, "[ext4] File %s is linked to %s\n", PathSegment, Symlink)); + // + // Close symlink file + // + Ext4CloseInternal (File); + // + // Open linked file by recursive call of Ext4OpenFile + // + Status = Ext4OpenInternal (FoundFile, Current, Symlink, OpenMode, Attributes); + FreePool (Symlink); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_FS, "[ext4] Error opening linked file %s\n", Symlink)); + return Status; + } + + // + // Set File to newly opened + // + File = *FoundFile; + } + + if (Level != 0) { + // Careful not to close the base directory + Ext4CloseInternal (Current); + } + + Level++; + + Current = File; + } + + if (Level == 0) { + // We opened the base directory again, so we need to duplicate the file structure + Current = Ext4DuplicateFile (Current); + if (Current == NULL) { + return EFI_OUT_OF_RESOURCES; + } + } + + if (!Ext4ApplyPermissions (Current, OpenMode)) { + Ext4CloseInternal (Current); + return EFI_ACCESS_DENIED; + } + + *FoundFile = Current; + + DEBUG ((DEBUG_FS, "[ext4] Opened filename %s\n", Current->Dentry->Name)); + return EFI_SUCCESS; +} + +/** + Opens a new file relative to the source file's location. + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the file + handle to the source location. This would typically be an open + handle to a directory. + @param[out] NewHandle A pointer to the location to return the opened handle for the new + file. + @param[in] FileName The Null-terminated string of the name of the file to be opened. + The file name may contain the following path modifiers: "\", ".", + and "..". + @param[in] OpenMode The mode to open the file. The only valid combinations that the + file may be opened with are: Read, Read/Write, or Create/Read/Write. + @param[in] Attributes Only valid for EFI_FILE_MODE_CREATE, in which case these are the + attribute bits for the newly created file. + @retval EFI_SUCCESS The file was opened. + @retval EFI_NOT_FOUND The specified file could not be found on the device. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_MEDIA_CHANGED The device has a different medium in it or the medium is no + longer supported. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED An attempt was made to create a file, or open a file for write + when the media is write-protected. + @retval EFI_ACCESS_DENIED The service denied access to the file. + @retval EFI_OUT_OF_RESOURCES Not enough resources were available to open the file. + @retval EFI_VOLUME_FULL The volume is full. +**/ +EFI_STATUS +EFIAPI +Ext4Open ( + IN EFI_FILE_PROTOCOL *This, + OUT EFI_FILE_PROTOCOL **NewHandle, + IN CHAR16 *FileName, + IN UINT64 OpenMode, + IN UINT64 Attributes + ) +{ + EFI_STATUS Status; + EXT4_FILE *FoundFile; + EXT4_FILE *Source; + + Source = EXT4_FILE_FROM_THIS (This); + + // + // Reset SymLoops counter + // + Source->Partition->Root->SymLoops = 0; + + Status = Ext4OpenInternal ( + &FoundFile, + Source, + FileName, + OpenMode, + Attributes + ); + + if (!EFI_ERROR (Status)) { + *NewHandle = &FoundFile->Protocol; + } + + return Status; +} + +/** + Closes a specified file handle. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the file + handle to close. + + @retval EFI_SUCCESS The file was closed. + +**/ +EFI_STATUS +EFIAPI +Ext4Close ( + IN EFI_FILE_PROTOCOL *This + ) +{ + return Ext4CloseInternal (EXT4_FILE_FROM_THIS (This)); +} + +/** + Closes a file. + + @param[in] File Pointer to the file. + + @return Status of the closing of the file. +**/ +EFI_STATUS +Ext4CloseInternal ( + IN EXT4_FILE *File + ) +{ + if ((File == File->Partition->Root) && !File->Partition->Unmounting) { + return EFI_SUCCESS; + } + + DEBUG ((DEBUG_FS, "[ext4] Closed file %p (inode %lu)\n", File, File->InodeNum)); + RemoveEntryList (&File->OpenFilesListNode); + FreePool (File->Inode); + Ext4FreeExtentsMap (File); + Ext4UnrefDentry (File->Dentry); + FreePool (File); + return EFI_SUCCESS; +} + +/** + Close and delete the file handle. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the + handle to the file to delete. + + @retval EFI_SUCCESS The file was closed and deleted, and the handle was closed. + @retval EFI_WARN_DELETE_FAILURE The handle was closed, but the file was not deleted. + +**/ +EFI_STATUS +EFIAPI +Ext4Delete ( + IN EFI_FILE_PROTOCOL *This + ) +{ + // We do a regular close here since we don't have write support. + Ext4Close (This); + return EFI_WARN_DELETE_FAILURE; +} + +/** + Reads data from a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the file + handle to read data from. + @param[in out] BufferSize On input, the size of the Buffer. On output, the amount of data + returned in Buffer. In both cases, the size is measured in bytes. + @param[out] Buffer The buffer into which the data is read. + + @retval EFI_SUCCESS Data was read. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_DEVICE_ERROR An attempt was made to read from a deleted file. + @retval EFI_DEVICE_ERROR On entry, the current file position is beyond the end of the file. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_BUFFER_TOO_SMALL The BufferSize is too small to read the current directory + entry. BufferSize has been updated with the size + needed to complete the request. + +**/ +EFI_STATUS +EFIAPI +Ext4ReadFile ( + IN EFI_FILE_PROTOCOL *This, + IN OUT UINTN *BufferSize, + OUT VOID *Buffer + ) +{ + EXT4_FILE *File; + EXT4_PARTITION *Partition; + EFI_STATUS Status; + + File = EXT4_FILE_FROM_THIS (This); + Partition = File->Partition; + + ASSERT (Ext4FileIsOpenable (File)); + + if (Ext4FileIsReg (File)) { + Status = Ext4Read (Partition, File, Buffer, File->Position, BufferSize); + if (Status == EFI_SUCCESS) { + File->Position += *BufferSize; + } + + return Status; + } else if (Ext4FileIsDir (File)) { + Status = Ext4ReadDir (Partition, File, Buffer, File->Position, BufferSize); + + return Status; + } + + return EFI_SUCCESS; +} + +/** + Writes data to a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the file + handle to write data to. + @param[in out] BufferSize On input, the size of the Buffer. On output, the amount of data + actually written. In both cases, the size is measured in bytes. + @param[in] Buffer The buffer of data to write. + + @retval EFI_SUCCESS Data was written. + @retval EFI_UNSUPPORTED Writes to open directory files are not supported. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_DEVICE_ERROR An attempt was made to write to a deleted file. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED The file or medium is write-protected. + @retval EFI_ACCESS_DENIED The file was opened read only. + @retval EFI_VOLUME_FULL The volume is full. + +**/ +EFI_STATUS +EFIAPI +Ext4WriteFile ( + IN EFI_FILE_PROTOCOL *This, + IN OUT UINTN *BufferSize, + IN VOID *Buffer + ) +{ + EXT4_FILE *File; + + File = EXT4_FILE_FROM_THIS (This); + + if (!(File->OpenMode & EFI_FILE_MODE_WRITE)) { + return EFI_ACCESS_DENIED; + } + + return EFI_WRITE_PROTECTED; +} + +/** + Returns a file's current position. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the file + handle to get the current position on. + @param[out] Position The address to return the file's current position value. + + @retval EFI_SUCCESS The position was returned. + @retval EFI_UNSUPPORTED The request is not valid on open directories. + @retval EFI_DEVICE_ERROR An attempt was made to get the position from a deleted file. + +**/ +EFI_STATUS +EFIAPI +Ext4GetPosition ( + IN EFI_FILE_PROTOCOL *This, + OUT UINT64 *Position + ) +{ + EXT4_FILE *File; + + File = EXT4_FILE_FROM_THIS (This); + + if (Ext4FileIsDir (File)) { + return EFI_UNSUPPORTED; + } + + *Position = File->Position; + + return EFI_SUCCESS; +} + +/** + Sets a file's current position. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the + file handle to set the requested position on. + @param[in] Position The byte position from the start of the file to set. + + @retval EFI_SUCCESS The position was set. + @retval EFI_UNSUPPORTED The seek request for nonzero is not valid on open + directories. + @retval EFI_DEVICE_ERROR An attempt was made to set the position of a deleted file. + +**/ +EFI_STATUS +EFIAPI +Ext4SetPosition ( + IN EFI_FILE_PROTOCOL *This, + IN UINT64 Position + ) +{ + EXT4_FILE *File; + + File = EXT4_FILE_FROM_THIS (This); + + // Only seeks to 0 (so it resets the ReadDir operation) are allowed + if (Ext4FileIsDir (File) && (Position != 0)) { + return EFI_UNSUPPORTED; + } + + // -1 (0xffffff.......) seeks to the end of the file + if (Position == (UINT64)-1) { + Position = EXT4_INODE_SIZE (File->Inode); + } + + File->Position = Position; + + return EFI_SUCCESS; +} + +/** + Retrieves information about the file and stores it in the EFI_FILE_INFO format. + + @param[in] File Pointer to an opened file. + @param[out] Info Pointer to a EFI_FILE_INFO. + @param[in out] BufferSize Pointer to the buffer size + + @return Status of the file information request. +**/ +EFI_STATUS +Ext4GetFileInfo ( + IN EXT4_FILE *File, + OUT EFI_FILE_INFO *Info, + IN OUT UINTN *BufferSize + ) +{ + UINTN FileNameLen; + UINTN FileNameSize; + UINTN NeededLength; + CONST CHAR16 *FileName; + + if (File->InodeNum == EXT4_ROOT_INODE_NR) { + // Root inode gets a filename of "", regardless of how it was opened. + FileName = L""; + } else { + FileName = File->Dentry->Name; + } + + FileNameLen = StrLen (FileName); + FileNameSize = StrSize (FileName); + + NeededLength = SIZE_OF_EFI_FILE_INFO + FileNameSize; + + if (*BufferSize < NeededLength) { + *BufferSize = NeededLength; + return EFI_BUFFER_TOO_SMALL; + } + + Info->FileSize = EXT4_INODE_SIZE (File->Inode); + Info->PhysicalSize = Ext4FilePhysicalSpace (File); + Ext4FileATime (File, &Info->LastAccessTime); + Ext4FileMTime (File, &Info->ModificationTime); + Ext4FileCreateTime (File, &Info->LastAccessTime); + Info->Attribute = 0; + Info->Size = NeededLength; + + if (Ext4FileIsDir (File)) { + Info->Attribute |= EFI_FILE_DIRECTORY; + } + + *BufferSize = NeededLength; + + return StrCpyS (Info->FileName, FileNameLen + 1, FileName); +} + +/** + Retrieves the volume name. + + @param[in] Part Pointer to the opened partition. + @param[out] Info Pointer to a CHAR16*. + @param[out] BufferSize Pointer to a UINTN, where the string length + of the name will be put. + + @return Status of the volume name request. +**/ +EFI_STATUS +Ext4GetVolumeName ( + IN EXT4_PARTITION *Partition, + OUT CHAR16 **OutVolName, + OUT UINTN *VolNameLen + ) +{ + CHAR8 TempVolName[16 + 1]; + CHAR16 *VolumeName; + UINTN VolNameLength; + EFI_STATUS Status; + + VolNameLength = 0; + VolumeName = NULL; + + // s_volume_name is only valid on dynamic revision; old filesystems don't support this + if (Partition->SuperBlock.s_rev_level == EXT4_DYNAMIC_REV) { + CopyMem (TempVolName, Partition->SuperBlock.s_volume_name, 16); + TempVolName[16] = '\0'; + + Status = UTF8StrToUCS2 (TempVolName, &VolumeName); + + if (EFI_ERROR (Status)) { + return Status; + } + + VolNameLength = StrLen (VolumeName); + } else { + VolumeName = AllocateZeroPool (sizeof (CHAR16)); + VolNameLength = 0; + } + + *OutVolName = VolumeName; + *VolNameLen = VolNameLength; + + return EFI_SUCCESS; +} + +/** + Retrieves information about the filesystem and stores it in the EFI_FILE_SYSTEM_INFO format. + + @param[in] Part Pointer to the opened partition. + @param[out] Info Pointer to a EFI_FILE_SYSTEM_INFO. + @param[in out] BufferSize Pointer to the buffer size + + @return Status of the file information request. +**/ +STATIC +EFI_STATUS +Ext4GetFilesystemInfo ( + IN EXT4_PARTITION *Part, + OUT EFI_FILE_SYSTEM_INFO *Info, + IN OUT UINTN *BufferSize + ) +{ + // Length of s_volume_name + null terminator + EFI_STATUS Status; + UINTN NeededLength; + EXT4_BLOCK_NR TotalBlocks; + EXT4_BLOCK_NR FreeBlocks; + CHAR16 *VolumeName; + UINTN VolNameLength; + + Status = Ext4GetVolumeName (Part, &VolumeName, &VolNameLength); + + if (EFI_ERROR (Status)) { + return Status; + } + + NeededLength = SIZE_OF_EFI_FILE_SYSTEM_INFO; + + NeededLength += StrSize (VolumeName); + + if (*BufferSize < NeededLength) { + *BufferSize = NeededLength; + + FreePool (VolumeName); + + return EFI_BUFFER_TOO_SMALL; + } + + TotalBlocks = Part->NumberBlocks; + + FreeBlocks = EXT4_BLOCK_NR_FROM_HALFS ( + Part, + Part->SuperBlock.s_free_blocks_count, + Part->SuperBlock.s_free_blocks_count_hi + ); + + Info->BlockSize = Part->BlockSize; + Info->Size = NeededLength; + Info->ReadOnly = Part->ReadOnly; + Info->VolumeSize = MultU64x32 (TotalBlocks, Part->BlockSize); + Info->FreeSpace = MultU64x32 (FreeBlocks, Part->BlockSize); + + StrCpyS (Info->VolumeLabel, VolNameLength + 1, VolumeName); + + FreePool (VolumeName); + + *BufferSize = NeededLength; + + return EFI_SUCCESS; +} + +/** + Retrieves the volume label and stores it in the EFI_FILE_SYSTEM_VOLUME_LABEL format. + + @param[in] Part Pointer to the opened partition. + @param[out] Info Pointer to a EFI_FILE_SYSTEM_VOLUME_LABEL. + @param[in out] BufferSize Pointer to the buffer size + + @return Status of the file information request. +**/ +STATIC +EFI_STATUS +Ext4GetVolumeLabelInfo ( + IN EXT4_PARTITION *Part, + OUT EFI_FILE_SYSTEM_VOLUME_LABEL *Info, + IN OUT UINTN *BufferSize + ) +{ + // Length of s_volume_name + null terminator + CHAR16 *VolumeName; + UINTN VolNameLength; + EFI_STATUS Status; + UINTN NeededLength; + + Status = Ext4GetVolumeName (Part, &VolumeName, &VolNameLength); + + if (EFI_ERROR (Status)) { + return Status; + } + + NeededLength = (VolNameLength + 1) * sizeof (CHAR16); + + if (NeededLength > *BufferSize) { + *BufferSize = NeededLength; + + FreePool (VolumeName); + + return EFI_BUFFER_TOO_SMALL; + } + + Status = StrCpyS (Info->VolumeLabel, VolNameLength + 1, VolumeName); + + ASSERT_EFI_ERROR (Status); + + FreePool (VolumeName); + + *BufferSize = NeededLength; + + return EFI_SUCCESS; +} + +/** + Returns information about a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the file + handle the requested information is for. + @param[in] InformationType The type identifier for the information being requested. + @param[in out] BufferSize On input, the size of Buffer. On output, the amount of data + returned in Buffer. In both cases, the size is measured in bytes. + @param[out] Buffer A pointer to the data buffer to return. The buffer's type is + indicated by InformationType. + + @retval EFI_SUCCESS The information was returned. + @retval EFI_UNSUPPORTED The InformationType is not known. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_BUFFER_TOO_SMALL The BufferSize is too small to read the current directory entry. + BufferSize has been updated with the size needed to complete + the request. +**/ +EFI_STATUS +EFIAPI +Ext4GetInfo ( + IN EFI_FILE_PROTOCOL *This, + IN EFI_GUID *InformationType, + IN OUT UINTN *BufferSize, + OUT VOID *Buffer + ) +{ + EXT4_FILE *File; + EXT4_PARTITION *Partition; + + File = EXT4_FILE_FROM_THIS (This); + Partition = File->Partition; + + if (CompareGuid (InformationType, &gEfiFileInfoGuid)) { + return Ext4GetFileInfo (File, Buffer, BufferSize); + } + + if (CompareGuid (InformationType, &gEfiFileSystemInfoGuid)) { + return Ext4GetFilesystemInfo (Partition, Buffer, BufferSize); + } + + if (CompareGuid (InformationType, &gEfiFileSystemVolumeLabelInfoIdGuid)) { + return Ext4GetVolumeLabelInfo (Partition, Buffer, BufferSize); + } + + return EFI_UNSUPPORTED; +} + +/** + Duplicates a file structure. + + @param[in] Original Pointer to the original file. + + @return Pointer to the new file structure. +**/ +STATIC +EXT4_FILE * +Ext4DuplicateFile ( + IN CONST EXT4_FILE *Original + ) +{ + EXT4_PARTITION *Partition; + EXT4_FILE *File; + EFI_STATUS Status; + + Partition = Original->Partition; + File = AllocateZeroPool (sizeof (EXT4_FILE)); + + if (File == NULL) { + return NULL; + } + + File->Inode = Ext4AllocateInode (Partition); + if (File->Inode == NULL) { + FreePool (File); + return NULL; + } + + CopyMem (File->Inode, Original->Inode, Partition->InodeSize); + + File->Position = 0; + Ext4SetupFile (File, Partition); + File->InodeNum = Original->InodeNum; + File->OpenMode = 0; // Will be filled by other code + + Status = Ext4InitExtentsMap (File); + if (EFI_ERROR (Status)) { + FreePool (File->Inode); + FreePool (File); + return NULL; + } + + File->Dentry = Original->Dentry; + + Ext4RefDentry (File->Dentry); + + InsertTailList (&Partition->OpenFiles, &File->OpenFilesListNode); + + return File; +} + +/** + Sets information about a file. + + @param[in] This A pointer to the EFI_FILE_PROTOCOL instance that is the file + handle the information is for. + @param[in] InformationType The type identifier for the information being set. + @param[in] BufferSize The size, in bytes, of Buffer. + @param[in] Buffer A pointer to the data buffer to write. The buffer's type is + indicated by InformationType. + + @retval EFI_SUCCESS The information was set. + @retval EFI_UNSUPPORTED The InformationType is not known. + @retval EFI_NO_MEDIA The device has no medium. + @retval EFI_DEVICE_ERROR The device reported an error. + @retval EFI_VOLUME_CORRUPTED The file system structures are corrupted. + @retval EFI_WRITE_PROTECTED InformationType is EFI_FILE_INFO_ID and the media is + read-only. + @retval EFI_WRITE_PROTECTED InformationType is EFI_FILE_PROTOCOL_SYSTEM_INFO_ID + and the media is read only. + @retval EFI_WRITE_PROTECTED InformationType is EFI_FILE_SYSTEM_VOLUME_LABEL_ID + and the media is read-only. + @retval EFI_ACCESS_DENIED An attempt is made to change the name of a file to a + file that is already present. + @retval EFI_ACCESS_DENIED An attempt is being made to change the EFI_FILE_DIRECTORY + Attribute. + @retval EFI_ACCESS_DENIED An attempt is being made to change the size of a directory. + @retval EFI_ACCESS_DENIED InformationType is EFI_FILE_INFO_ID and the file was opened + read-only and an attempt is being made to modify a field + other than Attribute. + @retval EFI_VOLUME_FULL The volume is full. + @retval EFI_BAD_BUFFER_SIZE BufferSize is smaller than the size of the type indicated + by InformationType. + +**/ +EFI_STATUS +EFIAPI +Ext4SetInfo ( + IN EFI_FILE_PROTOCOL *This, + IN EFI_GUID *InformationType, + IN UINTN BufferSize, + IN VOID *Buffer + ) +{ + EXT4_FILE *File; + EXT4_PARTITION *Partition; + + File = EXT4_FILE_FROM_THIS (This); + Partition = File->Partition; + + if (Partition->ReadOnly) { + return EFI_WRITE_PROTECTED; + } + + // There's no write support just yet. + return EFI_UNSUPPORTED; +} diff --git a/Ext4Pkg/Ext4Dxe/Inode.c b/Ext4Pkg/Ext4Dxe/Inode.c new file mode 100644 index 0000000000..5ccb4d2bfc --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Inode.c @@ -0,0 +1,490 @@ +/** @file + Inode related routines + + Copyright (c) 2021 - 2022 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent + + EpochToEfiTime copied from EmbeddedPkg/Library/TimeBaseLib.c + Copyright (c) 2016, Hisilicon Limited. All rights reserved. + Copyright (c) 2016-2019, Linaro Limited. All rights reserved. + Copyright (c) 2021, Ampere Computing LLC. All rights reserved. +**/ + +#include "Ext4Dxe.h" + +/** + Calculates the checksum of the given inode. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] Inode Pointer to the inode. + @param[in] InodeNum Inode number. + + @return The checksum. +**/ +UINT32 +Ext4CalculateInodeChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_INODE *Inode, + IN EXT4_INO_NR InodeNum + ) +{ + UINT32 Crc; + UINT16 Dummy; + BOOLEAN HasSecondChecksumField; + CONST VOID *RestOfInode; + UINTN RestOfInodeLength; + UINTN Length; + + HasSecondChecksumField = EXT4_INODE_HAS_FIELD (Inode, i_checksum_hi); + + Dummy = 0; + + Crc = Ext4CalculateChecksum (Partition, &InodeNum, sizeof (InodeNum), Partition->InitialSeed); + Crc = Ext4CalculateChecksum (Partition, &Inode->i_generation, sizeof (Inode->i_generation), Crc); + + Crc = Ext4CalculateChecksum ( + Partition, + Inode, + OFFSET_OF (EXT4_INODE, i_osd2.data_linux.l_i_checksum_lo), + Crc + ); + + Crc = Ext4CalculateChecksum (Partition, &Dummy, sizeof (Dummy), Crc); + + RestOfInode = &Inode->i_osd2.data_linux.l_i_reserved; + RestOfInodeLength = Partition->InodeSize - OFFSET_OF (EXT4_INODE, i_osd2.data_linux.l_i_reserved); + + if (HasSecondChecksumField) { + Length = OFFSET_OF (EXT4_INODE, i_checksum_hi) - OFFSET_OF (EXT4_INODE, i_osd2.data_linux.l_i_reserved); + + Crc = Ext4CalculateChecksum (Partition, &Inode->i_osd2.data_linux.l_i_reserved, Length, Crc); + Crc = Ext4CalculateChecksum (Partition, &Dummy, sizeof (Dummy), Crc); + + // 4 is the size of the i_extra_size field + the size of i_checksum_hi + RestOfInodeLength = Partition->InodeSize - EXT4_GOOD_OLD_INODE_SIZE - 4; + RestOfInode = &Inode->i_ctime_extra; + } + + Crc = Ext4CalculateChecksum (Partition, RestOfInode, RestOfInodeLength, Crc); + + return Crc; +} + +/** + Reads from an EXT4 inode. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] File Pointer to the opened file. + @param[out] Buffer Pointer to the buffer. + @param[in] Offset Offset of the read. + @param[in out] Length Pointer to the length of the buffer, in bytes. + After a succesful read, it's updated to the number of read bytes. + + @return Status of the read operation. +**/ +EFI_STATUS +Ext4Read ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT VOID *Buffer, + IN UINT64 Offset, + IN OUT UINTN *Length + ) +{ + EXT4_INODE *Inode; + UINT64 InodeSize; + UINT64 CurrentSeek; + UINTN RemainingRead; + UINTN BeenRead; + UINTN WasRead; + EXT4_EXTENT Extent; + UINT32 BlockOff; + EFI_STATUS Status; + BOOLEAN HasBackingExtent; + UINT32 HoleOff; + UINT64 HoleLen; + UINT64 ExtentStartBytes; + UINT64 ExtentLengthBytes; + UINT64 ExtentLogicalBytes; + + // Our extent offset is the difference between CurrentSeek and ExtentLogicalBytes + UINT64 ExtentOffset; + UINTN ExtentMayRead; + + Inode = File->Inode; + InodeSize = EXT4_INODE_SIZE (Inode); + CurrentSeek = Offset; + RemainingRead = *Length; + BeenRead = 0; + + DEBUG ((DEBUG_FS, "[ext4] Ext4Read(%s, Offset %lu, Length %lu)\n", File->Dentry->Name, Offset, *Length)); + + if (Offset > InodeSize) { + return EFI_DEVICE_ERROR; + } + + if (RemainingRead > InodeSize - Offset) { + RemainingRead = (UINTN)(InodeSize - Offset); + } + + while (RemainingRead != 0) { + WasRead = 0; + + // The algorithm here is to get the extent corresponding to the current block + // and then read as much as we can from the current extent. + + Status = Ext4GetExtent ( + Partition, + File, + DivU64x32Remainder (CurrentSeek, Partition->BlockSize, &BlockOff), + &Extent + ); + + if ((Status != EFI_SUCCESS) && (Status != EFI_NO_MAPPING)) { + return Status; + } + + HasBackingExtent = Status != EFI_NO_MAPPING; + + if (!HasBackingExtent || EXT4_EXTENT_IS_UNINITIALIZED (&Extent)) { + HoleOff = BlockOff; + + if (!HasBackingExtent) { + HoleLen = Partition->BlockSize - HoleOff; + } else { + // Uninitialized extents behave exactly the same as file holes, except they have + // blocks already allocated to them. + HoleLen = (Ext4GetExtentLength (&Extent) * Partition->BlockSize) - HoleOff; + } + + WasRead = HoleLen > RemainingRead ? RemainingRead : (UINTN)HoleLen; + // Potential improvement: In the future, we could get the file hole's total + // size and memset all that + ZeroMem (Buffer, WasRead); + } else { + ExtentStartBytes = MultU64x32 ( + LShiftU64 (Extent.ee_start_hi, 32) | + Extent.ee_start_lo, + Partition->BlockSize + ); + ExtentLengthBytes = Extent.ee_len * Partition->BlockSize; + ExtentLogicalBytes = (UINT64)Extent.ee_block * Partition->BlockSize; + ExtentOffset = CurrentSeek - ExtentLogicalBytes; + ExtentMayRead = (UINTN)(ExtentLengthBytes - ExtentOffset); + + WasRead = ExtentMayRead > RemainingRead ? RemainingRead : ExtentMayRead; + + Status = Ext4ReadDiskIo (Partition, Buffer, WasRead, ExtentStartBytes + ExtentOffset); + + if (EFI_ERROR (Status)) { + DEBUG (( + DEBUG_ERROR, + "[ext4] Error %r reading [%lu, %lu]\n", + Status, + ExtentStartBytes + ExtentOffset, + ExtentStartBytes + ExtentOffset + WasRead - 1 + )); + return Status; + } + } + + RemainingRead -= WasRead; + Buffer = (VOID *)((CHAR8 *)Buffer + WasRead); + BeenRead += WasRead; + CurrentSeek += WasRead; + } + + *Length = BeenRead; + + return EFI_SUCCESS; +} + +/** + Allocates a zeroed inode structure. + @param[in] Partition Pointer to the opened EXT4 partition. + + @return Pointer to the allocated structure, from the pool, + with size Partition->InodeSize. +**/ +EXT4_INODE * +Ext4AllocateInode ( + IN EXT4_PARTITION *Partition + ) +{ + BOOLEAN NeedsToZeroRest; + UINT32 InodeSize; + EXT4_INODE *Inode; + + NeedsToZeroRest = FALSE; + InodeSize = Partition->InodeSize; + + // We allocate a structure of at least sizeof(EXT4_INODE), but in the future, when + // write support is added and we need to flush inodes to disk, we could have a bit better + // distinction between the on-disk inode and a separate, nicer to work with inode struct. + // It's important to note that EXT4_INODE includes fields that may not exist in an actual + // filesystem (the minimum inode size is 128 byte and at the moment the size of EXT4_INODE + // is 160 bytes). + + if (InodeSize < sizeof (EXT4_INODE)) { + InodeSize = sizeof (EXT4_INODE); + NeedsToZeroRest = TRUE; + } + + Inode = AllocateZeroPool (InodeSize); + + if (!Inode) { + return NULL; + } + + if (NeedsToZeroRest) { + Inode->i_extra_isize = 0; + } + + return Inode; +} + +/** + Checks if a file is a directory. + @param[in] File Pointer to the opened file. + + @return TRUE if file is a directory. +**/ +BOOLEAN +Ext4FileIsDir ( + IN CONST EXT4_FILE *File + ) +{ + return (File->Inode->i_mode & EXT4_INO_TYPE_DIR) == EXT4_INO_TYPE_DIR; +} + +/** + Checks if a file is a symlink. + + @param[in] File Pointer to the opened file. + + @return BOOLEAN Whether file is a symlink +**/ +BOOLEAN +Ext4FileIsSymlink ( + IN CONST EXT4_FILE *File + ) +{ + return (File->Inode->i_mode & EXT4_INO_TYPE_SYMLINK) == EXT4_INO_TYPE_SYMLINK; +} + +/** + Checks if a file is a regular file. + @param[in] File Pointer to the opened file. + + @return BOOLEAN TRUE if file is a regular file. +**/ +BOOLEAN +Ext4FileIsReg ( + IN CONST EXT4_FILE *File + ) +{ + return (File->Inode->i_mode & EXT4_INO_TYPE_REGFILE) == EXT4_INO_TYPE_REGFILE; +} + +/** + Calculates the physical space used by a file. + @param[in] File Pointer to the opened file. + + @return Physical space used by a file, in bytes. +**/ +UINT64 +Ext4FilePhysicalSpace ( + IN EXT4_FILE *File + ) +{ + BOOLEAN HugeFile; + UINT64 Blocks; + + HugeFile = EXT4_HAS_RO_COMPAT (File->Partition, EXT4_FEATURE_RO_COMPAT_HUGE_FILE); + Blocks = File->Inode->i_blocks; + + if (HugeFile) { + Blocks |= LShiftU64 (File->Inode->i_osd2.data_linux.l_i_blocks_high, 32); + + // If HUGE_FILE is enabled and EXT4_HUGE_FILE_FL is set in the inode's flags, each unit + // in i_blocks corresponds to an actual filesystem block + if ((File->Inode->i_flags & EXT4_HUGE_FILE_FL) != 0) { + return MultU64x32 (Blocks, File->Partition->BlockSize); + } + } + + // Else, each i_blocks unit corresponds to 512 bytes + return MultU64x32 (Blocks, 512); +} + +// Copied from EmbeddedPkg at my mentor's request. +// The lack of comments and good variable names is frightening... + +/** + Converts Epoch seconds (elapsed since 1970 JANUARY 01, 00:00:00 UTC) to EFI_TIME. + + @param[in] EpochSeconds Epoch seconds. + @param[out] Time The time converted to UEFI format. + +**/ +STATIC +VOID +EFIAPI +EpochToEfiTime ( + IN UINTN EpochSeconds, + OUT EFI_TIME *Time + ) +{ + UINTN a; + UINTN b; + UINTN c; + UINTN d; + UINTN g; + UINTN j; + UINTN m; + UINTN y; + UINTN da; + UINTN db; + UINTN dc; + UINTN dg; + UINTN hh; + UINTN mm; + UINTN ss; + UINTN J; + + J = (EpochSeconds / 86400) + 2440588; + j = J + 32044; + g = j / 146097; + dg = j % 146097; + c = (((dg / 36524) + 1) * 3) / 4; + dc = dg - (c * 36524); + b = dc / 1461; + db = dc % 1461; + a = (((db / 365) + 1) * 3) / 4; + da = db - (a * 365); + y = (g * 400) + (c * 100) + (b * 4) + a; + m = (((da * 5) + 308) / 153) - 2; + d = da - (((m + 4) * 153) / 5) + 122; + + Time->Year = (UINT16)(y - 4800 + ((m + 2) / 12)); + Time->Month = ((m + 2) % 12) + 1; + Time->Day = (UINT8)(d + 1); + + ss = EpochSeconds % 60; + a = (EpochSeconds - ss) / 60; + mm = a % 60; + b = (a - mm) / 60; + hh = b % 24; + + Time->Hour = (UINT8)hh; + Time->Minute = (UINT8)mm; + Time->Second = (UINT8)ss; + Time->Nanosecond = 0; +} + +// The time format used to (de/en)code timestamp and timestamp_extra is documented on +// the ext4 docs page in kernel.org +#define EXT4_EXTRA_TIMESTAMP_MASK ((1 << 2) - 1) + +#define EXT4_FILE_GET_TIME_GENERIC(Name, Field) \ + VOID \ + Ext4File ## Name (IN EXT4_FILE *File, OUT EFI_TIME *Time) \ + { \ + EXT4_INODE *Inode = File->Inode; \ + UINT64 SecondsEpoch = Inode->Field; \ + UINT32 Nanoseconds = 0; \ + \ + if (EXT4_INODE_HAS_FIELD (Inode, Field ## _extra)) { \ + SecondsEpoch |= LShiftU64 ((UINT64)(Inode->Field ## _extra & EXT4_EXTRA_TIMESTAMP_MASK), 32); \ + Nanoseconds = Inode->Field ## _extra >> 2; \ + } \ + EpochToEfiTime ((UINTN)SecondsEpoch, Time); \ + Time->Nanosecond = Nanoseconds; \ + } + +// Note: EpochToEfiTime should be adjusted to take in a UINT64 instead of a UINTN, in order to avoid Y2038 +// on 32-bit systems. + +/** + Gets the file's last access time. + @param[in] File Pointer to the opened file. + @param[out] Time Pointer to an EFI_TIME structure. +**/ +EXT4_FILE_GET_TIME_GENERIC (ATime, i_atime); + +/** + Gets the file's last (data) modification time. + @param[in] File Pointer to the opened file. + @param[out] Time Pointer to an EFI_TIME structure. +**/ +EXT4_FILE_GET_TIME_GENERIC (MTime, i_mtime); + +/** + Gets the file's creation time. + @param[in] File Pointer to the opened file. + @param[out] Time Pointer to an EFI_TIME structure. +**/ +STATIC +EXT4_FILE_GET_TIME_GENERIC ( + CrTime, + i_crtime + ); + +/** + Gets the file's creation time, if possible. + @param[in] File Pointer to the opened file. + @param[out] Time Pointer to an EFI_TIME structure. + In the case where the the creation time isn't recorded, + Time is zeroed. +**/ +VOID +Ext4FileCreateTime ( + IN EXT4_FILE *File, + OUT EFI_TIME *Time + ) +{ + EXT4_INODE *Inode; + + Inode = File->Inode; + + if (!EXT4_INODE_HAS_FIELD (Inode, i_crtime)) { + ZeroMem (Time, sizeof (EFI_TIME)); + return; + } + + Ext4FileCrTime (File, Time); +} + +/** + Checks if the checksum of the inode is correct. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] Inode Pointer to the inode. + @param[in] InodeNum Inode number. + + @return TRUE if checksum is correct, FALSE if there is corruption. +**/ +BOOLEAN +Ext4CheckInodeChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST EXT4_INODE *Inode, + IN EXT4_INO_NR InodeNum + ) +{ + UINT32 Csum; + UINT32 DiskCsum; + + if (!EXT4_HAS_METADATA_CSUM (Partition)) { + return TRUE; + } + + Csum = Ext4CalculateInodeChecksum (Partition, Inode, InodeNum); + + DiskCsum = Inode->i_osd2.data_linux.l_i_checksum_lo; + + if (EXT4_INODE_HAS_FIELD (Inode, i_checksum_hi)) { + DiskCsum |= ((UINT32)Inode->i_checksum_hi) << 16; + } else { + // Only keep the lower bits for the comparison if the checksum is 16 bits. + Csum &= 0xffff; + } + + return Csum == DiskCsum; +} diff --git a/Ext4Pkg/Ext4Dxe/Partition.c b/Ext4Pkg/Ext4Dxe/Partition.c new file mode 100644 index 0000000000..316807497d --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Partition.c @@ -0,0 +1,132 @@ +/** @file + Driver entry point + + Copyright (c) 2021 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +/** + Opens an ext4 partition and installs the Simple File System protocol. + + @param[in] DeviceHandle Handle to the block device. + @param[in] DiskIo Pointer to an EFI_DISK_IO_PROTOCOL. + @param[in opt] DiskIo2 Pointer to an EFI_DISK_IO2_PROTOCOL, if supported. + @param[in] BlockIo Pointer to an EFI_BLOCK_IO_PROTOCOL. + + @retval EFI_SUCCESS The opening was successful. + !EFI_SUCCESS Opening failed. +**/ +EFI_STATUS +Ext4OpenPartition ( + IN EFI_HANDLE DeviceHandle, + IN EFI_DISK_IO_PROTOCOL *DiskIo, + IN OPTIONAL EFI_DISK_IO2_PROTOCOL *DiskIo2, + IN EFI_BLOCK_IO_PROTOCOL *BlockIo + ) +{ + EXT4_PARTITION *Part; + EFI_STATUS Status; + + Part = AllocateZeroPool (sizeof (*Part)); + + if (Part == NULL) { + return EFI_OUT_OF_RESOURCES; + } + + InitializeListHead (&Part->OpenFiles); + + Part->BlockIo = BlockIo; + Part->DiskIo = DiskIo; + Part->DiskIo2 = DiskIo2; + + Status = Ext4OpenSuperblock (Part); + + if (EFI_ERROR (Status)) { + FreePool (Part); + return Status; + } + + Part->Interface.Revision = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_REVISION; + Part->Interface.OpenVolume = Ext4OpenVolume; + Status = gBS->InstallMultipleProtocolInterfaces ( + &DeviceHandle, + &gEfiSimpleFileSystemProtocolGuid, + &Part->Interface, + NULL + ); + + if (EFI_ERROR (Status)) { + FreePool (Part); + return Status; + } + + return EFI_SUCCESS; +} + +/** + Sets up the protocol and metadata of a file that is being opened. + + @param[in out] File Pointer to the file. + @param[in] Partition Pointer to the opened partition. +**/ +VOID +Ext4SetupFile ( + IN OUT EXT4_FILE *File, + IN EXT4_PARTITION *Partition + ) +{ + // Note: We don't yet support revision 2 of the file protocol + // (needs DISK_IO2 + asynchronous IO) + File->Protocol.Revision = EFI_FILE_PROTOCOL_REVISION; + File->Protocol.Open = Ext4Open; + File->Protocol.Close = Ext4Close; + File->Protocol.Delete = Ext4Delete; + File->Protocol.Read = Ext4ReadFile; + File->Protocol.Write = Ext4WriteFile; + File->Protocol.SetPosition = Ext4SetPosition; + File->Protocol.GetPosition = Ext4GetPosition; + File->Protocol.GetInfo = Ext4GetInfo; + File->Protocol.SetInfo = Ext4SetInfo; + + File->Partition = Partition; +} + +/** + Unmounts and frees an ext4 partition. + + @param[in] Partition Pointer to the opened partition. + + @retval Status of the unmount. +**/ +EFI_STATUS +Ext4UnmountAndFreePartition ( + IN EXT4_PARTITION *Partition + ) +{ + LIST_ENTRY *Entry; + LIST_ENTRY *NextEntry; + EXT4_FILE *File; + BOOLEAN DeletedRootDentry; + + Partition->Unmounting = TRUE; + Ext4CloseInternal (Partition->Root); + + BASE_LIST_FOR_EACH_SAFE (Entry, NextEntry, &Partition->OpenFiles) { + File = EXT4_FILE_FROM_OPEN_FILES_NODE (Entry); + + Ext4CloseInternal (File); + } + + DeletedRootDentry = Ext4UnrefDentry (Partition->RootDentry); + + if (!DeletedRootDentry) { + DEBUG ((DEBUG_ERROR, "[ext4] Failed to delete root dentry - resource leak present.\n")); + } + + FreePool (Partition->BlockGroups); + FreePool (Partition); + + return EFI_SUCCESS; +} diff --git a/Ext4Pkg/Ext4Dxe/Superblock.c b/Ext4Pkg/Ext4Dxe/Superblock.c new file mode 100644 index 0000000000..edee051c41 --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Superblock.c @@ -0,0 +1,355 @@ +/** @file + Superblock managing routines + + Copyright (c) 2021 - 2022 Pedro Falcato All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +STATIC CONST UINT32 gSupportedCompatFeat = EXT4_FEATURE_COMPAT_EXT_ATTR; + +STATIC CONST UINT32 gSupportedRoCompatFeat = + EXT4_FEATURE_RO_COMPAT_DIR_NLINK | EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE | + EXT4_FEATURE_RO_COMPAT_HUGE_FILE | EXT4_FEATURE_RO_COMPAT_LARGE_FILE | + EXT4_FEATURE_RO_COMPAT_GDT_CSUM | EXT4_FEATURE_RO_COMPAT_METADATA_CSUM | EXT4_FEATURE_RO_COMPAT_SPARSE_SUPER; + +STATIC CONST UINT32 gSupportedIncompatFeat = + EXT4_FEATURE_INCOMPAT_64BIT | EXT4_FEATURE_INCOMPAT_DIRDATA | + EXT4_FEATURE_INCOMPAT_FLEX_BG | EXT4_FEATURE_INCOMPAT_FILETYPE | + EXT4_FEATURE_INCOMPAT_EXTENTS | EXT4_FEATURE_INCOMPAT_LARGEDIR | + EXT4_FEATURE_INCOMPAT_MMP | EXT4_FEATURE_INCOMPAT_RECOVER; + +// Future features that may be nice additions in the future: +// 1) Btree support: Required for write support and would speed up lookups in large directories. +// 2) meta_bg: Required to mount meta_bg-enabled partitions. + +// Note: We ignore MMP because it's impossible that it's mapped elsewhere, +// I think (unless there's some sort of network setup where we're accessing a remote partition). + +// Note on corruption signaling: +// We (Ext4Dxe) could signal corruption by setting s_state to |= EXT4_FS_STATE_ERRORS_DETECTED. +// I've decided against that, because right now the driver is read-only, and +// that would mean we would need to writeback the superblock. If something like +// this is desired, it's fairly trivial to look for EFI_VOLUME_CORRUPTED +// references and add some Ext4SignalCorruption function + function call. + +/** + Checks the superblock's magic value. + + @param[in] DiskIo Pointer to the DiskIo. + @param[in] BlockIo Pointer to the BlockIo. + + @returns Whether the partition has a valid EXT4 superblock magic value. +**/ +BOOLEAN +Ext4SuperblockCheckMagic ( + IN EFI_DISK_IO_PROTOCOL *DiskIo, + IN EFI_BLOCK_IO_PROTOCOL *BlockIo + ) +{ + UINT16 Magic; + EFI_STATUS Status; + + Status = DiskIo->ReadDisk ( + DiskIo, + BlockIo->Media->MediaId, + EXT4_SUPERBLOCK_OFFSET + OFFSET_OF (EXT4_SUPERBLOCK, s_magic), + sizeof (Magic), + &Magic + ); + if (EFI_ERROR (Status)) { + return FALSE; + } + + if (Magic != EXT4_SIGNATURE) { + return FALSE; + } + + return TRUE; +} + +/** + Does brief validation of the ext4 superblock. + + @param[in] Sb Pointer to the read superblock. + + @return TRUE if a valid ext4 superblock, else FALSE. +**/ +BOOLEAN +Ext4SuperblockValidate ( + CONST EXT4_SUPERBLOCK *Sb + ) +{ + if (Sb->s_magic != EXT4_SIGNATURE) { + return FALSE; + } + + if ((Sb->s_rev_level != EXT4_DYNAMIC_REV) && (Sb->s_rev_level != EXT4_GOOD_OLD_REV)) { + return FALSE; + } + + if ((Sb->s_state & EXT4_FS_STATE_UNMOUNTED) == 0) { + DEBUG ((DEBUG_WARN, "[ext4] Filesystem was not unmounted cleanly\n")); + } + + return TRUE; +} + +/** + Calculates the superblock's checksum. + + @param[in] Partition Pointer to the opened partition. + @param[in] Sb Pointer to the superblock. + + @return The superblock's checksum. +**/ +STATIC +UINT32 +Ext4CalculateSuperblockChecksum ( + EXT4_PARTITION *Partition, + CONST EXT4_SUPERBLOCK *Sb + ) +{ + // Most checksums require us to go through a dummy 0 as part of the requirement + // that the checksum is done over a structure with its checksum field = 0. + UINT32 Checksum; + + Checksum = Ext4CalculateChecksum ( + Partition, + Sb, + OFFSET_OF (EXT4_SUPERBLOCK, s_checksum), + ~0U + ); + + return Checksum; +} + +/** + Verifies that the superblock's checksum is valid. + + @param[in] Partition Pointer to the opened partition. + @param[in] Sb Pointer to the superblock. + + @return The superblock's checksum. +**/ +STATIC +BOOLEAN +Ext4VerifySuperblockChecksum ( + EXT4_PARTITION *Partition, + CONST EXT4_SUPERBLOCK *Sb + ) +{ + if (!EXT4_HAS_METADATA_CSUM (Partition)) { + return TRUE; + } + + return Sb->s_checksum == Ext4CalculateSuperblockChecksum (Partition, Sb); +} + +/** + Opens and parses the superblock. + + @param[out] Partition Partition structure to fill with filesystem details. + @retval EFI_SUCCESS Parsing was succesful and the partition is a + valid ext4 partition. +**/ +EFI_STATUS +Ext4OpenSuperblock ( + OUT EXT4_PARTITION *Partition + ) +{ + UINT32 Index; + EFI_STATUS Status; + EXT4_SUPERBLOCK *Sb; + UINT32 NrBlocksRem; + UINTN NrBlocks; + UINT32 UnsupportedRoCompat; + EXT4_BLOCK_GROUP_DESC *Desc; + + Status = Ext4ReadDiskIo ( + Partition, + &Partition->SuperBlock, + sizeof (EXT4_SUPERBLOCK), + EXT4_SUPERBLOCK_OFFSET + ); + + if (EFI_ERROR (Status)) { + return Status; + } + + Sb = &Partition->SuperBlock; + + if (!Ext4SuperblockValidate (Sb)) { + return EFI_VOLUME_CORRUPTED; + } + + if (Sb->s_rev_level == EXT4_DYNAMIC_REV) { + Partition->FeaturesCompat = Sb->s_feature_compat; + Partition->FeaturesIncompat = Sb->s_feature_incompat; + Partition->FeaturesRoCompat = Sb->s_feature_ro_compat; + Partition->InodeSize = Sb->s_inode_size; + + // Check for proper alignment of InodeSize and that InodeSize is indeed larger than + // the minimum size, 128 bytes. + if (((Partition->InodeSize % 4) != 0) || (Partition->InodeSize < EXT4_GOOD_OLD_INODE_SIZE)) { + return EFI_VOLUME_CORRUPTED; + } + } else { + // GOOD_OLD_REV + Partition->FeaturesCompat = Partition->FeaturesIncompat = Partition->FeaturesRoCompat = 0; + Partition->InodeSize = EXT4_GOOD_OLD_INODE_SIZE; + } + + // Now, check for the feature set of the filesystem + // It's essential to check for this to avoid filesystem corruption and to avoid + // accidentally opening an ext2/3/4 filesystem we don't understand, which would be disasterous. + + if (Partition->FeaturesIncompat & ~gSupportedIncompatFeat) { + DEBUG (( + DEBUG_ERROR, + "[ext4] Unsupported features %lx\n", + Partition->FeaturesIncompat & ~gSupportedIncompatFeat + )); + return EFI_UNSUPPORTED; + } + + if (EXT4_HAS_INCOMPAT (Partition, EXT4_FEATURE_INCOMPAT_RECOVER)) { + DEBUG ((DEBUG_WARN, "[ext4] Needs journal recovery, mounting read-only\n")); + Partition->ReadOnly = TRUE; + } + + // At the time of writing, it's the only supported checksum. + if (Partition->FeaturesCompat & EXT4_FEATURE_RO_COMPAT_METADATA_CSUM && + (Sb->s_checksum_type != EXT4_CHECKSUM_CRC32C)) + { + return EFI_UNSUPPORTED; + } + + if ((Partition->FeaturesIncompat & EXT4_FEATURE_INCOMPAT_CSUM_SEED) != 0) { + Partition->InitialSeed = Sb->s_checksum_seed; + } else { + Partition->InitialSeed = Ext4CalculateChecksum (Partition, Sb->s_uuid, 16, ~0U); + } + + UnsupportedRoCompat = Partition->FeaturesRoCompat & ~gSupportedRoCompatFeat; + + if (UnsupportedRoCompat != 0) { + DEBUG ((DEBUG_WARN, "[ext4] Unsupported ro compat %x\n", UnsupportedRoCompat)); + Partition->ReadOnly = TRUE; + } + + // gSupportedCompatFeat is documentation-only since we never need to access it. + // The line below avoids unused variable warnings. + (VOID)gSupportedCompatFeat; + + DEBUG ((DEBUG_FS, "Read only = %u\n", Partition->ReadOnly)); + + Partition->BlockSize = (UINT32)LShiftU64 (1024, Sb->s_log_block_size); + + // The size of a block group can also be calculated as 8 * Partition->BlockSize + if (Sb->s_blocks_per_group != 8 * Partition->BlockSize) { + return EFI_UNSUPPORTED; + } + + Partition->NumberBlocks = EXT4_BLOCK_NR_FROM_HALFS (Partition, Sb->s_blocks_count, Sb->s_blocks_count_hi); + Partition->NumberBlockGroups = DivU64x32 (Partition->NumberBlocks, Sb->s_blocks_per_group); + + DEBUG (( + DEBUG_FS, + "[ext4] Number of blocks = %lu\n[ext4] Number of block groups: %lu\n", + Partition->NumberBlocks, + Partition->NumberBlockGroups + )); + + if (EXT4_IS_64_BIT (Partition)) { + // s_desc_size should be 4 byte aligned and + // 64 bit filesystems need DescSize to be 64 bytes + if (((Sb->s_desc_size % 4) != 0) || (Sb->s_desc_size < EXT4_64BIT_BLOCK_DESC_SIZE)) { + return EFI_VOLUME_CORRUPTED; + } + + Partition->DescSize = Sb->s_desc_size; + } else { + Partition->DescSize = EXT4_OLD_BLOCK_DESC_SIZE; + } + + if (!Ext4VerifySuperblockChecksum (Partition, Sb)) { + DEBUG ((DEBUG_ERROR, "[ext4] Bad superblock checksum %lx\n", Ext4CalculateSuperblockChecksum (Partition, Sb))); + return EFI_VOLUME_CORRUPTED; + } + + NrBlocks = (UINTN)DivU64x32Remainder ( + MultU64x32 (Partition->NumberBlockGroups, Partition->DescSize), + Partition->BlockSize, + &NrBlocksRem + ); + + if (NrBlocksRem != 0) { + NrBlocks++; + } + + Partition->BlockGroups = Ext4AllocAndReadBlocks (Partition, NrBlocks, Partition->BlockSize == 1024 ? 2 : 1); + + if (Partition->BlockGroups == NULL) { + return EFI_OUT_OF_RESOURCES; + } + + for (Index = 0; Index < Partition->NumberBlockGroups; Index++) { + Desc = Ext4GetBlockGroupDesc (Partition, Index); + if (!Ext4VerifyBlockGroupDescChecksum (Partition, Desc, Index)) { + DEBUG ((DEBUG_ERROR, "[ext4] Block group descriptor %u has an invalid checksum\n", Index)); + FreePool (Partition->BlockGroups); + return EFI_VOLUME_CORRUPTED; + } + } + + // RootDentry will serve as the basis of our directory entry tree. + Partition->RootDentry = Ext4CreateDentry (L"\\", NULL); + + if (Partition->RootDentry == NULL) { + FreePool (Partition->BlockGroups); + return EFI_OUT_OF_RESOURCES; + } + + // Note that the cast below is completely safe, because EXT4_FILE is a specialisation of EFI_FILE_PROTOCOL + Status = Ext4OpenVolume (&Partition->Interface, (EFI_FILE_PROTOCOL **)&Partition->Root); + + if (EFI_ERROR (Status)) { + Ext4UnrefDentry (Partition->RootDentry); + FreePool (Partition->BlockGroups); + } + + return Status; +} + +/** + Calculates the checksum of the given buffer. + @param[in] Partition Pointer to the opened EXT4 partition. + @param[in] Buffer Pointer to the buffer. + @param[in] Length Length of the buffer, in bytes. + @param[in] InitialValue Initial value of the CRC. + + @return The checksum. +**/ +UINT32 +Ext4CalculateChecksum ( + IN CONST EXT4_PARTITION *Partition, + IN CONST VOID *Buffer, + IN UINTN Length, + IN UINT32 InitialValue + ) +{ + if (!EXT4_HAS_METADATA_CSUM (Partition)) { + return 0; + } + + switch (Partition->SuperBlock.s_checksum_type) { + case EXT4_CHECKSUM_CRC32C: + // For some reason, EXT4 really likes non-inverted CRC32C checksums, so we stick to that here. + return ~CalculateCrc32c(Buffer, Length, ~InitialValue); + default: + ASSERT (FALSE); + return 0; + } +} diff --git a/Ext4Pkg/Ext4Dxe/Symlink.c b/Ext4Pkg/Ext4Dxe/Symlink.c new file mode 100644 index 0000000000..0905417ffb --- /dev/null +++ b/Ext4Pkg/Ext4Dxe/Symlink.c @@ -0,0 +1,261 @@ +/** @file + Symbolic links routines + + Copyright (c) 2022 Savva Mitrofanov All rights reserved. + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include "Ext4Dxe.h" + +/** + Detects if a symlink is a fast symlink. + + @param[in] File Pointer to the opened file. + + @return BOOLEAN Whether symlink is a fast symlink +**/ +STATIC +BOOLEAN +Ext4SymlinkIsFastSymlink ( + IN CONST EXT4_FILE *File + ) +{ + // + // Detection logic of the fast-symlink splits into two behaviors - old and new. + // The old behavior is based on comparing the extended attribute blocks + // with the inode's i_blocks, and if it's zero we know the inode isn't storing + // the link in filesystem blocks, so we look to the inode->i_data. + // The new behavior is apparently needed only with the large EA inode feature. + // In this case we check that inode size less than maximum fast symlink size. + // So, we revert to the old behavior if the large EA inode feature is not set. + // + UINT32 FileAcl; + UINT32 ExtAttrBlocks; + + if ((File->Inode->i_flags & EXT4_EA_INODE_FL) == 0) { + FileAcl = File->Inode->i_file_acl; + if (EXT4_IS_64_BIT (File->Partition)) { + // + // We don't care about final value, we are just checking for any bit is set + // so, thats why we neglect LShiftU64(.., 32) + // + FileAcl |= File->Inode->i_osd2.data_linux.l_i_file_acl_high; + } + + ExtAttrBlocks = FileAcl != 0 ? (File->Partition->BlockSize >> 9) : 0; + + return File->Inode->i_blocks == ExtAttrBlocks; + } + + return EXT4_INODE_SIZE (File->Inode) <= EXT4_FAST_SYMLINK_MAX_SIZE; +} + +/** + Reads a fast symlink file. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] File Pointer to the open symlink file. + @param[out] AsciiSymlink Pointer to the output ascii symlink string. + @param[out] AsciiSymlinkSize Pointer to the output ascii symlink string length. + + @retval EFI_SUCCESS Fast symlink was read. + @retval EFI_OUT_OF_RESOURCES Memory allocation error. +**/ +STATIC +EFI_STATUS +Ext4ReadFastSymlink ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT CHAR8 **AsciiSymlink, + OUT UINT32 *AsciiSymlinkSize + ) +{ + UINT32 SymlinkSize; + CHAR8 *AsciiSymlinkTmp; + + // + // Fast-symlink's EXT4_INODE_SIZE is not necessarily validated when we checked it in + // Ext4SymlinkIsFastSymlink(), so truncate if necessary. + // + SymlinkSize = (UINT32)MIN (EXT4_INODE_SIZE (File->Inode), EXT4_FAST_SYMLINK_MAX_SIZE); + + AsciiSymlinkTmp = AllocatePool (SymlinkSize + 1); + if (AsciiSymlinkTmp == NULL) { + DEBUG ((DEBUG_ERROR, "[ext4] Failed to allocate symlink ascii string buffer\n")); + return EFI_OUT_OF_RESOURCES; + } + + CopyMem (AsciiSymlinkTmp, File->Inode->i_data, SymlinkSize); + + // + // Add null-terminator + // + AsciiSymlinkTmp[SymlinkSize] = '\0'; + + *AsciiSymlink = AsciiSymlinkTmp; + *AsciiSymlinkSize = SymlinkSize + 1; + + return EFI_SUCCESS; +} + +/** + Reads a slow symlink file. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] File Pointer to the open symlink file. + @param[out] AsciiSymlink Pointer to the output ascii symlink string. + @param[out] AsciiSymlinkSize Pointer to the output ascii symlink string length. + + @retval EFI_SUCCESS Slow symlink was read. + @retval EFI_OUT_OF_RESOURCES Memory allocation error. + @retval EFI_INVALID_PARAMETER Slow symlink path has incorrect length + @retval EFI_VOLUME_CORRUPTED Symlink read block size differ from inode value +**/ +STATIC +EFI_STATUS +Ext4ReadSlowSymlink ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT CHAR8 **AsciiSymlink, + OUT UINT32 *AsciiSymlinkSize + ) +{ + EFI_STATUS Status; + CHAR8 *SymlinkTmp; + UINT64 SymlinkSizeTmp; + UINT32 SymlinkAllocateSize; + UINTN ReadSize; + + SymlinkSizeTmp = EXT4_INODE_SIZE (File->Inode); + + // + // Allocate EXT4_INODE_SIZE + 1 + // + if (SymlinkSizeTmp >= EXT4_EFI_PATH_MAX) { + DEBUG (( + DEBUG_WARN, + "[ext4] Warn: symlink path maximum length was hit!\n" + )); + return EFI_INVALID_PARAMETER; + } + + SymlinkAllocateSize = (UINT32)SymlinkSizeTmp + 1; + + SymlinkTmp = AllocatePool (SymlinkAllocateSize); + if (SymlinkTmp == NULL) { + DEBUG ((DEBUG_FS, "[ext4] Failed to allocate symlink ascii string buffer\n")); + return EFI_OUT_OF_RESOURCES; + } + + ReadSize = (UINTN)SymlinkSizeTmp; + Status = Ext4Read (Partition, File, SymlinkTmp, File->Position, &ReadSize); + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_FS, "[ext4] Failed to read symlink from blocks with status %r\n", Status)); + FreePool (SymlinkTmp); + return Status; + } + + // + // Add null-terminator + // + SymlinkTmp[SymlinkSizeTmp] = '\0'; + + if (SymlinkSizeTmp != ReadSize) { + DEBUG (( + DEBUG_FS, + "[ext4] Error! The size of the read block doesn't match the value from the inode!\n" + )); + return EFI_VOLUME_CORRUPTED; + } + + *AsciiSymlinkSize = SymlinkAllocateSize; + *AsciiSymlink = SymlinkTmp; + + return EFI_SUCCESS; +} + +/** + Reads a symlink file. + + @param[in] Partition Pointer to the ext4 partition. + @param[in] File Pointer to the open symlink file. + @param[out] Symlink Pointer to the output unicode symlink string. + + @retval EFI_SUCCESS Symlink was read. + @retval EFI_ACCESS_DENIED Symlink is encrypted. + @retval EFI_OUT_OF_RESOURCES Memory allocation error. + @retval EFI_INVALID_PARAMETER Symlink path has incorrect length + @retval EFI_VOLUME_CORRUPTED Symlink read block size differ from inode value +**/ +EFI_STATUS +Ext4ReadSymlink ( + IN EXT4_PARTITION *Partition, + IN EXT4_FILE *File, + OUT CHAR16 **Symlink + ) +{ + EFI_STATUS Status; + CHAR8 *SymlinkTmp; + UINT32 SymlinkSize; + CHAR16 *Symlink16Tmp; + CHAR16 *Needle; + + // + // Assume that we alread read Inode via Ext4ReadInode + // Skip reading, just check encryption flag + // + if ((File->Inode->i_flags & EXT4_ENCRYPT_FL) != 0) { + DEBUG ((DEBUG_WARN, "[ext4] Warn: symlink is encrypted\n")); + return EFI_ACCESS_DENIED; + } + + if (Ext4SymlinkIsFastSymlink (File)) { + Status = Ext4ReadFastSymlink (Partition, File, &SymlinkTmp, &SymlinkSize); + } else { + Status = Ext4ReadSlowSymlink (Partition, File, &SymlinkTmp, &SymlinkSize); + } + + if (EFI_ERROR (Status)) { + DEBUG ((DEBUG_FS, "[ext4] Symlink read error with Status %r\n", Status)); + return Status; + } + + Symlink16Tmp = AllocatePool (SymlinkSize * sizeof (CHAR16)); + if (Symlink16Tmp == NULL) { + DEBUG ((DEBUG_FS, "[ext4] Failed to allocate symlink unicode string buffer\n")); + FreePool (SymlinkTmp); + return EFI_OUT_OF_RESOURCES; + } + + Status = AsciiStrToUnicodeStrS ( + SymlinkTmp, + Symlink16Tmp, + SymlinkSize + ); + + FreePool (SymlinkTmp); + + if (EFI_ERROR (Status)) { + DEBUG (( + DEBUG_FS, + "[ext4] Failed to convert ascii symlink to unicode with Status %r\n", + Status + )); + FreePool (Symlink16Tmp); + FreePool (SymlinkTmp); + return Status; + } + + // + // Convert to UEFI slashes + // + for (Needle = Symlink16Tmp; *Needle != L'\0'; Needle++) { + if (*Needle == L'/') { + *Needle = L'\\'; + } + } + + *Symlink = Symlink16Tmp; + + return Status; +} diff --git a/Ext4Pkg/Ext4Libs.dsc.inc b/Ext4Pkg/Ext4Libs.dsc.inc new file mode 100644 index 0000000000..078183e0cc --- /dev/null +++ b/Ext4Pkg/Ext4Libs.dsc.inc @@ -0,0 +1,11 @@ +## @file +# Ext4 DSC include file for [LibraryClasses] section of all Architectures. +# +# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +!if $(EXT4_ENABLE) == TRUE + BaseUcs2Utf8Lib|RedfishPkg/Library/BaseUcs2Utf8Lib/BaseUcs2Utf8Lib.inf +!endif diff --git a/Ext4Pkg/Ext4Pkg.dec b/Ext4Pkg/Ext4Pkg.dec new file mode 100644 index 0000000000..f1f8b39c3c --- /dev/null +++ b/Ext4Pkg/Ext4Pkg.dec @@ -0,0 +1,17 @@ +## @file +# Ext4 Package +# +# This package provides libraries and drivers related to the ext4 filesystem implementation. +# More details are available at: https://www.kernel.org/doc/html/v5.4/filesystems/ext4/index.html +# +# Copyright (c) 2021 Pedro Falcato +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +[Defines] + DEC_SPECIFICATION = 0x00010005 + PACKAGE_NAME = Ext4Pkg + PACKAGE_UNI_FILE = Ext4Pkg.uni + PACKAGE_GUID = 6B4BF998-668B-46D3-BCFA-971F99F8708C + PACKAGE_VERSION = 0.1 diff --git a/Ext4Pkg/Ext4Pkg.dsc b/Ext4Pkg/Ext4Pkg.dsc new file mode 100644 index 0000000000..18cfc102b8 --- /dev/null +++ b/Ext4Pkg/Ext4Pkg.dsc @@ -0,0 +1,75 @@ +## @file +# Ext4 Package +# +# This package provides libraries and drivers related to the ext4 filesystem implementation. +# More details are available at: https://www.kernel.org/doc/html/v5.4/filesystems/ext4/index.html +# +# Copyright (c) 2021 Pedro Falcato +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + + +[Defines] + PLATFORM_NAME = Ext4 + PLATFORM_GUID = 6B4BF998-668B-46D3-BCFA-971F99F8708C + PLATFORM_VERSION = 0.1 + DSC_SPECIFICATION = 0x00010005 + SUPPORTED_ARCHITECTURES = IA32|X64|EBC|ARM|AARCH64|RISCV64 + OUTPUT_DIRECTORY = Build/Ext4Pkg + BUILD_TARGETS = DEBUG|RELEASE|NOOPT + SKUID_IDENTIFIER = DEFAULT + +!include MdePkg/MdeLibs.dsc.inc + +[BuildOptions] + *_*_*_CC_FLAGS = -D DISABLE_NEW_DEPRECATED_INTERFACES + +[LibraryClasses] + # + # Entry Point Libraries + # + UefiDriverEntryPoint|MdePkg/Library/UefiDriverEntryPoint/UefiDriverEntryPoint.inf + # + # Common Libraries + # + BaseLib|MdePkg/Library/BaseLib/BaseLib.inf + BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf + UefiLib|MdePkg/Library/UefiLib/UefiLib.inf + PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf + PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf + MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf + UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf + UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf + DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf + DebugPrintErrorLevelLib|MdePkg/Library/BaseDebugPrintErrorLevelLib/BaseDebugPrintErrorLevelLib.inf + DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf + OrderedCollectionLib|MdePkg/Library/BaseOrderedCollectionRedBlackTreeLib/BaseOrderedCollectionRedBlackTreeLib.inf + BaseUcs2Utf8Lib|RedfishPkg/Library/BaseUcs2Utf8Lib/BaseUcs2Utf8Lib.inf + + # + # Required for stack protector support + # + NULL|MdePkg/Library/BaseStackCheckLib/BaseStackCheckLib.inf + +################################################################################################### +# +# Components Section - list of the modules and components that will be processed by compilation +# tools and the EDK II tools to generate PE32/PE32+/Coff image files. +# +# Note: The EDK II DSC file is not used to specify how compiled binary images get placed +# into firmware volume images. This section is just a list of modules to compile from +# source into UEFI-compliant binaries. +# It is the FDF file that contains information on combining binary files into firmware +# volume images, whose concept is beyond UEFI and is described in PI specification. +# Binary modules do not need to be listed in this section, as they should be +# specified in the FDF file. For example: Shell binary (Shell_Full.efi), FAT binary (Fat.efi), +# Logo (Logo.bmp), and etc. +# There may also be modules listed in this section that are not required in the FDF file, +# When a module listed here is excluded from FDF file, then UEFI-compliant binary will be +# generated for it, but the binary will not be put into any firmware volume. +# +################################################################################################### + +[Components] + Ext4Pkg/Ext4Dxe/Ext4Dxe.inf diff --git a/Ext4Pkg/Ext4Pkg.uni b/Ext4Pkg/Ext4Pkg.uni new file mode 100644 index 0000000000..abeadd8fd9 --- /dev/null +++ b/Ext4Pkg/Ext4Pkg.uni @@ -0,0 +1,14 @@ +## @file +# Ext4 Package +# +# This package provides libraries and drivers related to the ext4 filesystem implementation. +# More details are available at: https://www.kernel.org/doc/html/v5.4/filesystems/ext4/index.html +# +# Copyright (c) 2021 Pedro Falcato +# SPDX-License-Identifier: BSD-2-Clause-Patent +# +## + +#string STR_PACKAGE_ABSTRACT #language en-US "Module implementations for the EXT4 file system" + +#string STR_PACKAGE_DESCRIPTION #language en-US "This package contains UEFI drivers and libraries for the EXT4 file system."