/** @file | |
EFI_FILE_PROTOCOL.Read() member function for the Virtio Filesystem driver. | |
Copyright (C) 2020, Red Hat, Inc. | |
SPDX-License-Identifier: BSD-2-Clause-Patent | |
**/ | |
#include <Library/BaseMemoryLib.h> // CopyMem() | |
#include <Library/MemoryAllocationLib.h> // 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. | |
**/ | |
STATIC | |
EFI_STATUS | |
ReadRegularFile ( | |
IN OUT VIRTIO_FS_FILE *VirtioFsFile, | |
IN OUT UINTN *BufferSize, | |
OUT VOID *Buffer | |
) | |
{ | |
VIRTIO_FS *VirtioFs; | |
EFI_STATUS Status; | |
VIRTIO_FS_FUSE_ATTRIBUTES_RESPONSE FuseAttr; | |
UINTN Transferred; | |
UINTN Left; | |
VirtioFs = VirtioFsFile->OwnerFs; | |
// | |
// The UEFI spec forbids reads that start beyond the end of the file. | |
// | |
Status = VirtioFsFuseGetAttr (VirtioFs, VirtioFsFile->NodeId, &FuseAttr); | |
if (EFI_ERROR (Status) || (VirtioFsFile->FilePosition > FuseAttr.Size)) { | |
return EFI_DEVICE_ERROR; | |
} | |
Status = EFI_SUCCESS; | |
Transferred = 0; | |
Left = *BufferSize; | |
while (Left > 0) { | |
UINT32 ReadSize; | |
// | |
// FUSE_READ cannot express a >=4GB buffer size. | |
// | |
ReadSize = (UINT32)MIN ((UINTN)MAX_UINT32, Left); | |
Status = VirtioFsFuseReadFileOrDir ( | |
VirtioFs, | |
VirtioFsFile->NodeId, | |
VirtioFsFile->FuseHandle, | |
FALSE, // IsDir | |
VirtioFsFile->FilePosition + Transferred, | |
&ReadSize, | |
(UINT8 *)Buffer + Transferred | |
); | |
if (EFI_ERROR (Status) || (ReadSize == 0)) { | |
break; | |
} | |
Transferred += ReadSize; | |
Left -= ReadSize; | |
} | |
*BufferSize = Transferred; | |
VirtioFsFile->FilePosition += Transferred; | |
// | |
// If we managed to read some data, return success. If zero bytes were | |
// transferred due to zero-sized buffer on input or due to EOF on first read, | |
// return SUCCESS. Otherwise, return the error due to which zero bytes were | |
// transferred. | |
// | |
return (Transferred > 0) ? EFI_SUCCESS : Status; | |
} | |
EFI_STATUS | |
EFIAPI | |
VirtioFsSimpleFileRead ( | |
IN EFI_FILE_PROTOCOL *This, | |
IN OUT UINTN *BufferSize, | |
OUT VOID *Buffer | |
) | |
{ | |
VIRTIO_FS_FILE *VirtioFsFile; | |
EFI_STATUS Status; | |
VirtioFsFile = VIRTIO_FS_FILE_FROM_SIMPLE_FILE (This); | |
if (VirtioFsFile->IsDirectory) { | |
Status = ReadFileInfoCache (VirtioFsFile, BufferSize, Buffer); | |
} else { | |
Status = ReadRegularFile (VirtioFsFile, BufferSize, Buffer); | |
} | |
return Status; | |
} |