/** @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 // 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. **/ 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; }