From eb0b79586feaa448136f023575ea3c8916381136 Mon Sep 17 00:00:00 2001 From: Laszlo Ersek Date: Wed, 16 Dec 2020 22:11:13 +0100 Subject: OvmfPkg/VirtioFsDxe: implement EFI_FILE_PROTOCOL.Read() for directories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using the functions introduced previously, we can now implement VirtioFsSimpleFileRead() for directories as well. This patch completes the read-only support for virtio-fs. Commands like "TYPE" and "DIR" work in the UEFI shell. Cc: Ard Biesheuvel Cc: Jordan Justen Cc: Philippe Mathieu-Daudé Ref: https://bugzilla.tianocore.org/show_bug.cgi?id=3097 Signed-off-by: Laszlo Ersek Message-Id: <20201216211125.19496-37-lersek@redhat.com> Acked-by: Ard Biesheuvel --- OvmfPkg/VirtioFsDxe/SimpleFsRead.c | 343 ++++++++++++++++++++++++++++++++++++- 1 file changed, 342 insertions(+), 1 deletion(-) diff --git a/OvmfPkg/VirtioFsDxe/SimpleFsRead.c b/OvmfPkg/VirtioFsDxe/SimpleFsRead.c index c4ad07c9ae..d6368342a1 100644 --- a/OvmfPkg/VirtioFsDxe/SimpleFsRead.c +++ b/OvmfPkg/VirtioFsDxe/SimpleFsRead.c @@ -6,8 +6,349 @@ SPDX-License-Identifier: BSD-2-Clause-Patent **/ +#include // CopyMem() +#include // AllocatePool() + #include "VirtioFsDxe.h" +/** + Populate a caller-allocated EFI_FILE_INFO object from + VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE. + + @param[in] Dirent The entry read from the directory stream. The + caller is responsible for ensuring that + Dirent->Namelen describe valid storage. + + @param[in] SingleFileInfoSize The allocated size of FileInfo. + + @param[out] FileInfo The EFI_FILE_INFO object to populate. On + success, all fields in FileInfo will be + updated, setting FileInfo->Size to the + actually used size (which will not exceed + SingleFileInfoSize). + + @retval EFI_SUCCESS FileInfo has been filled in. + + @return Error codes propagated from + VirtioFsFuseDirentPlusToEfiFileInfo() and + VirtioFsFuseAttrToEfiFileInfo(). The contents of + FileInfo are indeterminate. +**/ +STATIC +EFI_STATUS +PopulateFileInfo ( + IN VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE *Dirent, + IN UINTN SingleFileInfoSize, + OUT EFI_FILE_INFO *FileInfo + ) +{ + EFI_STATUS Status; + + // + // Convert the name, set the actual size. + // + FileInfo->Size = SingleFileInfoSize; + Status = VirtioFsFuseDirentPlusToEfiFileInfo (Dirent, FileInfo); + if (EFI_ERROR (Status)) { + return Status; + } + // + // Populate the scalar fields. + // + Status = VirtioFsFuseAttrToEfiFileInfo (&Dirent->AttrResp, FileInfo); + return Status; +} + +/** + Refill the EFI_FILE_INFO cache from the directory stream. +**/ +STATIC +EFI_STATUS +RefillFileInfoCache ( + IN OUT VIRTIO_FS_FILE *VirtioFsFile + ) +{ + VIRTIO_FS *VirtioFs; + EFI_STATUS Status; + VIRTIO_FS_FUSE_STATFS_RESPONSE FilesysAttr; + UINT32 DirentBufSize; + UINT8 *DirentBuf; + UINTN SingleFileInfoSize; + UINT8 *FileInfoArray; + UINT64 DirStreamCookie; + UINT64 CacheEndsAtCookie; + UINTN NumFileInfo; + + // + // Allocate a DirentBuf that can receive at least + // VIRTIO_FS_FILE_MAX_FILE_INFO directory entries, based on the maximum + // filename length supported by the filesystem. Note that the multiplication + // is safe from overflow due to the VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE_SIZE() + // check. + // + VirtioFs = VirtioFsFile->OwnerFs; + Status = VirtioFsFuseStatFs (VirtioFs, VirtioFsFile->NodeId, &FilesysAttr); + if (EFI_ERROR (Status)) { + return Status; + } + DirentBufSize = (UINT32)VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE_SIZE ( + FilesysAttr.Namelen); + if (DirentBufSize == 0) { + return EFI_UNSUPPORTED; + } + DirentBufSize *= VIRTIO_FS_FILE_MAX_FILE_INFO; + DirentBuf = AllocatePool (DirentBufSize); + if (DirentBuf == NULL) { + return EFI_OUT_OF_RESOURCES; + } + + // + // Allocate the EFI_FILE_INFO cache. A single EFI_FILE_INFO element is sized + // accordingly to the maximum filename length supported by the filesystem. + // + // Note that the calculation below cannot overflow, due to the filename limit + // imposed by the VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE_SIZE() check above. The + // calculation takes the L'\0' character that we'll need to append into + // account. + // + SingleFileInfoSize = (OFFSET_OF (EFI_FILE_INFO, FileName) + + ((UINTN)FilesysAttr.Namelen + 1) * sizeof (CHAR16)); + FileInfoArray = AllocatePool ( + VIRTIO_FS_FILE_MAX_FILE_INFO * SingleFileInfoSize + ); + if (FileInfoArray == NULL) { + Status = EFI_OUT_OF_RESOURCES; + goto FreeDirentBuf; + } + + // + // Pick up reading the directory stream where the previous cache ended. + // + DirStreamCookie = VirtioFsFile->FilePosition; + CacheEndsAtCookie = VirtioFsFile->FilePosition; + NumFileInfo = 0; + do { + UINT32 Remaining; + UINT32 Consumed; + + // + // Fetch a chunk of the directory stream. The chunk may hold more entries + // than what we can fit in the cache. The chunk may also not entirely fill + // the cache, especially after filtering out entries that cannot be + // supported under UEFI (sockets, FIFOs, filenames with backslashes, etc). + // + Remaining = DirentBufSize; + Status = VirtioFsFuseReadFileOrDir ( + VirtioFs, + VirtioFsFile->NodeId, + VirtioFsFile->FuseHandle, + TRUE, // IsDir + DirStreamCookie, // Offset + &Remaining, // Size + DirentBuf // Data + ); + if (EFI_ERROR (Status)) { + goto FreeFileInfoArray; + } + + if (Remaining == 0) { + // + // The directory stream ends. + // + break; + } + + // + // Iterate over all records in DirentBuf. Primarily, forget them all. + // Secondarily, if a record proves transformable to EFI_FILE_INFO, add it + // to the EFI_FILE_INFO cache (unless the cache is full). + // + Consumed = 0; + while (Remaining >= sizeof (VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE)) { + VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE *Dirent; + UINT32 DirentSize; + + Dirent = (VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE *)(DirentBuf + Consumed); + DirentSize = (UINT32)VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE_SIZE ( + Dirent->Namelen); + if (DirentSize == 0) { + // + // This means one of two things: (a) Dirent->Namelen is zero, or (b) + // (b) Dirent->Namelen is unsupportably large. (a) is just invalid for + // the Virtio Filesystem device to send, while (b) shouldn't happen + // because "FilesysAttr.Namelen" -- the maximum filename length + // supported by the filesystem -- proved acceptable above. + // + Status = EFI_PROTOCOL_ERROR; + goto FreeFileInfoArray; + } + if (DirentSize > Remaining) { + // + // Dirent->Namelen suggests that the filename byte array (plus any + // padding) are truncated. This should never happen; the Virtio + // Filesystem device is supposed to send complete entries only. + // + Status = EFI_PROTOCOL_ERROR; + goto FreeFileInfoArray; + } + if (Dirent->Namelen > FilesysAttr.Namelen) { + // + // This is possible without tripping the truncation check above, due to + // how entries are padded. The condition means that Dirent->Namelen is + // reportedly larger than the filesystem limit, without spilling into + // the next alignment bucket. Should never happen. + // + Status = EFI_PROTOCOL_ERROR; + goto FreeFileInfoArray; + } + + // + // If we haven't filled the EFI_FILE_INFO cache yet, attempt transforming + // Dirent to EFI_FILE_INFO. + // + if (NumFileInfo < VIRTIO_FS_FILE_MAX_FILE_INFO) { + EFI_FILE_INFO *FileInfo; + + FileInfo = (EFI_FILE_INFO *)(FileInfoArray + + (NumFileInfo * SingleFileInfoSize)); + Status = PopulateFileInfo (Dirent, SingleFileInfoSize, FileInfo); + if (!EFI_ERROR (Status)) { + // + // Dirent has been transformed and cached successfully. + // + NumFileInfo++; + // + // The next time we refill the cache, restart reading the directory + // stream right after the entry that we've just transformed and + // cached. + // + CacheEndsAtCookie = Dirent->CookieForNextEntry; + } + // + // If Dirent wasn't transformable to an EFI_FILE_INFO, we'll just skip + // it. + // + } + + // + // Make the Virtio Filesystem device forget the NodeId in this directory + // entry, as we'll need it no more. (The "." and ".." entries need no + // FUSE_FORGET requests, when returned by FUSE_READDIRPLUS -- and so the + // Virtio Filesystem device reports their NodeId fields as zero.) + // + if (Dirent->NodeResp.NodeId != 0) { + VirtioFsFuseForget (VirtioFs, Dirent->NodeResp.NodeId); + } + + // + // Advance to the next entry in DirentBuf. + // + DirStreamCookie = Dirent->CookieForNextEntry; + Consumed += DirentSize; + Remaining -= DirentSize; + } + + if (Remaining > 0) { + // + // This suggests that a VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE header was + // truncated. This should never happen; the Virtio Filesystem device is + // supposed to send complete entries only. + // + Status = EFI_PROTOCOL_ERROR; + goto FreeFileInfoArray; + } + // + // Fetch another DirentBuf from the directory stream, unless we've filled + // the EFI_FILE_INFO cache. + // + } while (NumFileInfo < VIRTIO_FS_FILE_MAX_FILE_INFO); + + // + // Commit the results. (Note that the result may be an empty cache.) + // + if (VirtioFsFile->FileInfoArray != NULL) { + FreePool (VirtioFsFile->FileInfoArray); + } + VirtioFsFile->FileInfoArray = FileInfoArray; + VirtioFsFile->SingleFileInfoSize = SingleFileInfoSize; + VirtioFsFile->NumFileInfo = NumFileInfo; + VirtioFsFile->NextFileInfo = 0; + VirtioFsFile->FilePosition = CacheEndsAtCookie; + + FreePool (DirentBuf); + return EFI_SUCCESS; + +FreeFileInfoArray: + FreePool (FileInfoArray); + +FreeDirentBuf: + FreePool (DirentBuf); + + return Status; +} + +/** + Read an entry from the EFI_FILE_INFO cache. +**/ +STATIC +EFI_STATUS +ReadFileInfoCache ( + IN OUT VIRTIO_FS_FILE *VirtioFsFile, + IN OUT UINTN *BufferSize, + OUT VOID *Buffer + ) +{ + EFI_FILE_INFO *FileInfo; + UINTN CallerAllocated; + + // + // Refill the cache if needed. If the refill doesn't produce any new + // EFI_FILE_INFO, report End of Directory, by setting (*BufferSize) to 0. + // + if (VirtioFsFile->NextFileInfo == VirtioFsFile->NumFileInfo) { + EFI_STATUS Status; + + Status = RefillFileInfoCache (VirtioFsFile); + if (EFI_ERROR (Status)) { + return (Status == EFI_BUFFER_TOO_SMALL) ? EFI_DEVICE_ERROR : Status; + } + if (VirtioFsFile->NumFileInfo == 0) { + *BufferSize = 0; + return EFI_SUCCESS; + } + } + FileInfo = (EFI_FILE_INFO *)(VirtioFsFile->FileInfoArray + + (VirtioFsFile->NextFileInfo * + VirtioFsFile->SingleFileInfoSize)); + + // + // Check if the caller is ready to accept FileInfo. If not, we'll just + // present the required size for now. + // + // (The (UINTN) cast below is safe because FileInfo->Size has been reduced + // from VirtioFsFile->SingleFileInfoSize, in + // + // RefillFileInfoCache() + // PopulateFileInfo() + // VirtioFsFuseDirentPlusToEfiFileInfo() + // + // and VirtioFsFile->SingleFileInfoSize was computed from + // FilesysAttr.Namelen, which had been accepted by + // VIRTIO_FS_FUSE_DIRENTPLUS_RESPONSE_SIZE().) + // + CallerAllocated = *BufferSize; + *BufferSize = (UINTN)FileInfo->Size; + if (CallerAllocated < *BufferSize) { + return EFI_BUFFER_TOO_SMALL; + } + // + // Output FileInfo, and remove it from the cache. + // + CopyMem (Buffer, FileInfo, *BufferSize); + VirtioFsFile->NextFileInfo++; + return EFI_SUCCESS; +} + /** Read from a regular file. **/ @@ -85,7 +426,7 @@ VirtioFsSimpleFileRead ( VirtioFsFile = VIRTIO_FS_FILE_FROM_SIMPLE_FILE (This); if (VirtioFsFile->IsDirectory) { - Status = EFI_NO_MEDIA; + Status = ReadFileInfoCache (VirtioFsFile, BufferSize, Buffer); } else { Status = ReadRegularFile (VirtioFsFile, BufferSize, Buffer); } -- cgit v1.2.3