]> git.proxmox.com Git - mirror_edk2.git/commitdiff
OvmfPkg/VirtioFsDxe: add helper for appending and sanitizing paths
authorLaszlo Ersek <lersek@redhat.com>
Wed, 16 Dec 2020 21:10:53 +0000 (22:10 +0100)
committermergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Mon, 21 Dec 2020 17:16:23 +0000 (17:16 +0000)
EFI_FILE_PROTOCOL.Open() -- for opening files -- and
EFI_FILE_PROTOCOL.SetInfo() --  for renaming files -- will require us to
append a relative UEFI pathname to an absolute base pathname. In turn,
components of the resultant pathnames will have to be sent to virtiofsd,
which does not consume UEFI-style pathnames.

We're going to maintain the base pathnames in canonical POSIX format:
- absolute (starts with "/"),
- dot (.) and dot-dot (..) components resolved/removed,
- uses forward slashes,
- sequences of slashes collapsed,
- printable ASCII character set,
- CHAR8 encoding,
- no trailing slash except for the root directory itself,
- length at most VIRTIO_FS_MAX_PATHNAME_LENGTH.

Add a helper function that can append a UEFI pathname to such a base
pathname, and produce the result in conformance with the same invariants.

Cc: Ard Biesheuvel <ard.biesheuvel@arm.com>
Cc: Jordan Justen <jordan.l.justen@intel.com>
Cc: Philippe Mathieu-Daudé <philmd@redhat.com>
Ref: https://bugzilla.tianocore.org/show_bug.cgi?id=3097
Signed-off-by: Laszlo Ersek <lersek@redhat.com>
Message-Id: <20201216211125.19496-17-lersek@redhat.com>
Acked-by: Ard Biesheuvel <ard.biesheuvel@arm.com>
OvmfPkg/VirtioFsDxe/Helpers.c
OvmfPkg/VirtioFsDxe/VirtioFsDxe.h
OvmfPkg/VirtioFsDxe/VirtioFsDxe.inf

index 739b0c63cfaa4d4991093ad65c1e6a1bef3e3f8f..8ffdabfd40f35ecb057c941097613d01c62bce7b 100644 (file)
@@ -6,6 +6,9 @@
   SPDX-License-Identifier: BSD-2-Clause-Patent\r
 **/\r
 \r
+#include <Library/BaseLib.h>             // StrLen()\r
+#include <Library/BaseMemoryLib.h>       // CopyMem()\r
+#include <Library/MemoryAllocationLib.h> // AllocatePool()\r
 #include <Library/VirtioLib.h>           // Virtio10WriteFeatures()\r
 \r
 #include "VirtioFsDxe.h"\r
@@ -1115,3 +1118,474 @@ VirtioFsErrnoToEfiStatus (
 \r
   return EFI_DEVICE_ERROR;\r
 }\r
+\r
+//\r
+// Parser states for canonicalizing a POSIX pathname.\r
+//\r
+typedef enum {\r
+  ParserInit,   // just starting\r
+  ParserEnd,    // finished\r
+  ParserSlash,  // slash(es) seen\r
+  ParserDot,    // one dot seen since last slash\r
+  ParserDotDot, // two dots seen since last slash\r
+  ParserNormal, // a different sequence seen\r
+} PARSER_STATE;\r
+\r
+/**\r
+  Strip the trailing slash from the parser's output buffer, unless the trailing\r
+  slash stands for the root directory.\r
+\r
+  @param[in] Buffer        The parser's output buffer. Only used for\r
+                           sanity-checking.\r
+\r
+  @param[in,out] Position  On entry, points at the next character to produce\r
+                           (i.e., right past the end of the output written by\r
+                           the parser thus far). The last character in the\r
+                           parser's output buffer is a slash. On return, the\r
+                           slash is stripped, by decrementing Position by one.\r
+                           If this action would remove the slash character\r
+                           standing for the root directory, then the function\r
+                           has no effect.\r
+**/\r
+STATIC\r
+VOID\r
+ParserStripSlash (\r
+  IN     CHAR8 *Buffer,\r
+  IN OUT UINTN *Position\r
+  )\r
+{\r
+  ASSERT (*Position >= 1);\r
+  ASSERT (Buffer[*Position - 1] == '/');\r
+  if (*Position == 1) {\r
+    return;\r
+  }\r
+  (*Position)--;\r
+}\r
+\r
+/**\r
+  Produce one character in the parser's output buffer.\r
+\r
+  @param[out] Buffer       The parser's output buffer. On return, Char8 will\r
+                           have been written.\r
+\r
+  @param[in,out] Position  On entry, points at the next character to produce\r
+                           (i.e., right past the end of the output written by\r
+                           the parser thus far). On return, Position is\r
+                           incremented by one.\r
+\r
+  @param[in] Size          Total allocated size of the parser's output buffer.\r
+                           Used for sanity-checking.\r
+\r
+  @param[in] Char8         The character to place in the output buffer.\r
+**/\r
+STATIC\r
+VOID\r
+ParserCopy (\r
+     OUT CHAR8 *Buffer,\r
+  IN OUT UINTN *Position,\r
+  IN     UINTN Size,\r
+  IN     CHAR8 Char8\r
+  )\r
+{\r
+  ASSERT (*Position < Size);\r
+  Buffer[(*Position)++] = Char8;\r
+}\r
+\r
+/**\r
+  Rewind the last single-dot in the parser's output buffer.\r
+\r
+  @param[in] Buffer        The parser's output buffer. Only used for\r
+                           sanity-checking.\r
+\r
+  @param[in,out] Position  On entry, points at the next character to produce\r
+                           (i.e., right past the end of the output written by\r
+                           the parser thus far); the parser's output buffer\r
+                           ends with the characters ('/', '.'). On return, the\r
+                           dot is rewound by decrementing Position by one; a\r
+                           slash character will reside at the new end of the\r
+                           parser's output buffer.\r
+**/\r
+STATIC\r
+VOID\r
+ParserRewindDot (\r
+  IN     CHAR8 *Buffer,\r
+  IN OUT UINTN *Position\r
+  )\r
+{\r
+  ASSERT (*Position >= 2);\r
+  ASSERT (Buffer[*Position - 1] == '.');\r
+  ASSERT (Buffer[*Position - 2] == '/');\r
+  (*Position)--;\r
+}\r
+\r
+/**\r
+  Rewind the last dot-dot in the parser's output buffer.\r
+\r
+  @param[in] Buffer        The parser's output buffer. Only used for\r
+                           sanity-checking.\r
+\r
+  @param[in,out] Position  On entry, points at the next character to produce\r
+                           (i.e., right past the end of the output written by\r
+                           the parser thus far); the parser's output buffer\r
+                           ends with the characters ('/', '.', '.'). On return,\r
+                           the ('.', '.') pair is rewound unconditionally, by\r
+                           decrementing Position by two; a slash character\r
+                           resides at the new end of the parser's output\r
+                           buffer.\r
+\r
+                           If this slash character stands for the root\r
+                           directory, then RootEscape is set to TRUE.\r
+\r
+                           Otherwise (i.e., if this slash character is not the\r
+                           one standing for the root directory), then the slash\r
+                           character, and the pathname component preceding it,\r
+                           are removed by decrementing Position further. In\r
+                           this case, the slash character preceding the removed\r
+                           pathname component will reside at the new end of the\r
+                           parser's output buffer.\r
+\r
+  @param[out] RootEscape   Set to TRUE on output if the dot-dot component tries\r
+                           to escape the root directory, as described above.\r
+                           Otherwise, RootEscape is not modified.\r
+**/\r
+STATIC\r
+VOID\r
+ParserRewindDotDot (\r
+  IN     CHAR8   *Buffer,\r
+  IN OUT UINTN   *Position,\r
+     OUT BOOLEAN *RootEscape\r
+\r
+  )\r
+{\r
+  ASSERT (*Position >= 3);\r
+  ASSERT (Buffer[*Position - 1] == '.');\r
+  ASSERT (Buffer[*Position - 2] == '.');\r
+  ASSERT (Buffer[*Position - 3] == '/');\r
+  (*Position) -= 2;\r
+\r
+  if (*Position == 1) {\r
+    //\r
+    // Root directory slash reached; don't try to climb higher.\r
+    //\r
+    *RootEscape = TRUE;\r
+    return;\r
+  }\r
+\r
+  //\r
+  // Skip slash.\r
+  //\r
+  (*Position)--;\r
+  //\r
+  // Scan until next slash to the left.\r
+  //\r
+  do {\r
+    ASSERT (*Position > 0);\r
+    (*Position)--;\r
+  } while (Buffer[*Position] != '/');\r
+  (*Position)++;\r
+}\r
+\r
+/**\r
+  Append the UEFI-style RhsPath16 to the POSIX-style, canonical format\r
+  LhsPath8. Output the POSIX-style, canonical format result in ResultPath, as a\r
+  dynamically allocated string.\r
+\r
+  Canonicalization (aka sanitization) establishes the following properties:\r
+  - ResultPath is absolute (starts with "/"),\r
+  - dot (.) and dot-dot (..) components are resolved/eliminated in ResultPath,\r
+    with the usual semantics,\r
+  - ResultPath uses forward slashes,\r
+  - sequences of slashes are squashed in ResultPath,\r
+  - the printable ASCII character set covers ResultPath,\r
+  - CHAR8 encoding is used in ResultPath,\r
+  - no trailing slash present in ResultPath except for the standalone root\r
+    directory,\r
+  - the length of ResultPath is at most VIRTIO_FS_MAX_PATHNAME_LENGTH.\r
+\r
+  Any dot-dot in RhsPath16 that would remove the root directory is dropped, and\r
+  reported through RootEscape, without failing the function call.\r
+\r
+  @param[in] LhsPath8      Identifies the base directory. The caller is\r
+                           responsible for ensuring that LhsPath8 conform to\r
+                           the above canonical pathname format on entry.\r
+\r
+  @param[in] RhsPath16     Identifies the desired file with a UEFI-style CHAR16\r
+                           pathname. If RhsPath16 starts with a backslash, then\r
+                           RhsPath16 is considered absolute, and LhsPath8 is\r
+                           ignored; RhsPath16 is sanitized in isolation, for\r
+                           producing ResultPath8. Otherwise (i.e., if RhsPath16\r
+                           is relative), RhsPath16 is transliterated to CHAR8,\r
+                           and naively appended to LhsPath8. The resultant\r
+                           fused pathname is then sanitized, to produce\r
+                           ResultPath8.\r
+\r
+  @param[out] ResultPath8  The POSIX-style, canonical format pathname that\r
+                           leads to the file desired by the caller. After use,\r
+                           the caller is responsible for freeing ResultPath8.\r
+\r
+  @param[out] RootEscape   Set to TRUE if at least one dot-dot component in\r
+                           RhsPath16 attempted to escape the root directory;\r
+                           set to FALSE otherwise.\r
+\r
+  @retval EFI_SUCCESS            ResultPath8 has been produced. RootEscape has\r
+                                 been output.\r
+\r
+  @retval EFI_INVALID_PARAMETER  RhsPath16 is zero-length.\r
+\r
+  @retval EFI_INVALID_PARAMETER  RhsPath16 failed the\r
+                                 VIRTIO_FS_MAX_PATHNAME_LENGTH check.\r
+\r
+  @retval EFI_OUT_OF_RESOURCES   Memory allocation failed.\r
+\r
+  @retval EFI_OUT_OF_RESOURCES   ResultPath8 would have failed the\r
+                                 VIRTIO_FS_MAX_PATHNAME_LENGTH check.\r
+\r
+  @retval EFI_UNSUPPORTED        RhsPath16 contains a character that either\r
+                                 falls outside of the printable ASCII set, or\r
+                                 is a forward slash.\r
+**/\r
+EFI_STATUS\r
+VirtioFsAppendPath (\r
+  IN     CHAR8   *LhsPath8,\r
+  IN     CHAR16  *RhsPath16,\r
+     OUT CHAR8   **ResultPath8,\r
+     OUT BOOLEAN *RootEscape\r
+  )\r
+{\r
+  UINTN        RhsLen;\r
+  CHAR8        *RhsPath8;\r
+  UINTN        Idx;\r
+  EFI_STATUS   Status;\r
+  UINTN        SizeToSanitize;\r
+  CHAR8        *BufferToSanitize;\r
+  CHAR8        *SanitizedBuffer;\r
+  PARSER_STATE State;\r
+  UINTN        SanitizedPosition;\r
+\r
+  //\r
+  // Appending an empty pathname is not allowed.\r
+  //\r
+  RhsLen = StrLen (RhsPath16);\r
+  if (RhsLen == 0) {\r
+    return EFI_INVALID_PARAMETER;\r
+  }\r
+  //\r
+  // Enforce length restriction on RhsPath16.\r
+  //\r
+  if (RhsLen > VIRTIO_FS_MAX_PATHNAME_LENGTH) {\r
+    return EFI_INVALID_PARAMETER;\r
+  }\r
+\r
+  //\r
+  // Transliterate RhsPath16 to RhsPath8 by:\r
+  // - rejecting RhsPath16 if a character outside of printable ASCII is seen,\r
+  // - rejecting RhsPath16 if a forward slash is seen,\r
+  // - replacing backslashes with forward slashes,\r
+  // - casting the characters from CHAR16 to CHAR8.\r
+  //\r
+  RhsPath8 = AllocatePool (RhsLen + 1);\r
+  if (RhsPath8 == NULL) {\r
+    return EFI_OUT_OF_RESOURCES;\r
+  }\r
+  for (Idx = 0; RhsPath16[Idx] != L'\0'; Idx++) {\r
+    if (RhsPath16[Idx] < 0x20 || RhsPath16[Idx] > 0x7E ||\r
+        RhsPath16[Idx] == L'/') {\r
+      Status = EFI_UNSUPPORTED;\r
+      goto FreeRhsPath8;\r
+    }\r
+    RhsPath8[Idx] = (CHAR8)((RhsPath16[Idx] == L'\\') ? L'/' : RhsPath16[Idx]);\r
+  }\r
+  RhsPath8[Idx++] = '\0';\r
+\r
+  //\r
+  // Now prepare the input for the canonicalization (squashing of sequences of\r
+  // forward slashes, and eliminating . (dot) and .. (dot-dot) pathname\r
+  // components).\r
+  //\r
+  // The sanitized path can never be longer than the naive concatenation of the\r
+  // left hand side and right hand side paths, so we'll use the catenated size\r
+  // for allocating the sanitized output too.\r
+  //\r
+  if (RhsPath8[0] == '/') {\r
+    //\r
+    // If the right hand side path is absolute, then it is not appended to the\r
+    // left hand side path -- it *replaces* the left hand side path.\r
+    //\r
+    SizeToSanitize = RhsLen + 1;\r
+    BufferToSanitize = RhsPath8;\r
+  } else {\r
+    //\r
+    // If the right hand side path is relative, then it is appended (naively)\r
+    // to the left hand side.\r
+    //\r
+    UINTN LhsLen;\r
+\r
+    LhsLen = AsciiStrLen (LhsPath8);\r
+    SizeToSanitize = LhsLen + 1 + RhsLen + 1;\r
+    BufferToSanitize = AllocatePool (SizeToSanitize);\r
+    if (BufferToSanitize == NULL) {\r
+      Status = EFI_OUT_OF_RESOURCES;\r
+      goto FreeRhsPath8;\r
+    }\r
+    CopyMem (BufferToSanitize, LhsPath8, LhsLen);\r
+    BufferToSanitize[LhsLen] = '/';\r
+    CopyMem (BufferToSanitize + LhsLen + 1, RhsPath8, RhsLen + 1);\r
+  }\r
+\r
+  //\r
+  // Allocate the output buffer.\r
+  //\r
+  SanitizedBuffer = AllocatePool (SizeToSanitize);\r
+  if (SanitizedBuffer == NULL) {\r
+    Status = EFI_OUT_OF_RESOURCES;\r
+    goto FreeBufferToSanitize;\r
+  }\r
+\r
+  //\r
+  // State machine for parsing the input and producing the canonical output\r
+  // follows.\r
+  //\r
+  *RootEscape       = FALSE;\r
+  Idx               = 0;\r
+  State             = ParserInit;\r
+  SanitizedPosition = 0;\r
+  do {\r
+    CHAR8 Chr8;\r
+\r
+    ASSERT (Idx < SizeToSanitize);\r
+    Chr8 = BufferToSanitize[Idx++];\r
+\r
+    switch (State) {\r
+    case ParserInit: // just starting\r
+      ASSERT (Chr8 == '/');\r
+      ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+      State = ParserSlash;\r
+      break;\r
+\r
+    case ParserSlash: // slash(es) seen\r
+      switch (Chr8) {\r
+      case '\0':\r
+        ParserStripSlash (SanitizedBuffer, &SanitizedPosition);\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserEnd;\r
+        break;\r
+      case '/':\r
+        //\r
+        // skip & stay in same state\r
+        //\r
+        break;\r
+      case '.':\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserDot;\r
+        break;\r
+      default:\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserNormal;\r
+        break;\r
+      }\r
+      break;\r
+\r
+    case ParserDot: // one dot seen since last slash\r
+      switch (Chr8) {\r
+      case '\0':\r
+        ParserRewindDot (SanitizedBuffer, &SanitizedPosition);\r
+        ParserStripSlash (SanitizedBuffer, &SanitizedPosition);\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserEnd;\r
+        break;\r
+      case '/':\r
+        ParserRewindDot (SanitizedBuffer, &SanitizedPosition);\r
+        State = ParserSlash;\r
+        break;\r
+      case '.':\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserDotDot;\r
+        break;\r
+      default:\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserNormal;\r
+        break;\r
+      }\r
+      break;\r
+\r
+    case ParserDotDot: // two dots seen since last slash\r
+      switch (Chr8) {\r
+      case '\0':\r
+        ParserRewindDotDot (SanitizedBuffer, &SanitizedPosition, RootEscape);\r
+        ParserStripSlash (SanitizedBuffer, &SanitizedPosition);\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserEnd;\r
+        break;\r
+      case '/':\r
+        ParserRewindDotDot (SanitizedBuffer, &SanitizedPosition, RootEscape);\r
+        State = ParserSlash;\r
+        break;\r
+      case '.':\r
+        //\r
+        // fall through\r
+        //\r
+      default:\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserNormal;\r
+        break;\r
+      }\r
+      break;\r
+\r
+    case ParserNormal: // a different sequence seen\r
+      switch (Chr8) {\r
+      case '\0':\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserEnd;\r
+        break;\r
+      case '/':\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        State = ParserSlash;\r
+        break;\r
+      case '.':\r
+        //\r
+        // fall through\r
+        //\r
+      default:\r
+        //\r
+        // copy and stay in same state\r
+        //\r
+        ParserCopy (SanitizedBuffer, &SanitizedPosition, SizeToSanitize, Chr8);\r
+        break;\r
+      }\r
+      break;\r
+\r
+    default:\r
+      ASSERT (FALSE);\r
+      break;\r
+    }\r
+  } while (State != ParserEnd);\r
+\r
+  //\r
+  // Ensure length invariant on ResultPath8.\r
+  //\r
+  ASSERT (SanitizedPosition >= 2);\r
+  if (SanitizedPosition - 1 > VIRTIO_FS_MAX_PATHNAME_LENGTH) {\r
+    Status = EFI_OUT_OF_RESOURCES;\r
+    goto FreeSanitizedBuffer;\r
+  }\r
+\r
+  *ResultPath8    = SanitizedBuffer;\r
+  SanitizedBuffer = NULL;\r
+  Status          = EFI_SUCCESS;\r
+  //\r
+  // Fall through.\r
+  //\r
+FreeSanitizedBuffer:\r
+  if (SanitizedBuffer != NULL) {\r
+    FreePool (SanitizedBuffer);\r
+  }\r
+\r
+FreeBufferToSanitize:\r
+  if (RhsPath8[0] != '/') {\r
+    FreePool (BufferToSanitize);\r
+  }\r
+\r
+FreeRhsPath8:\r
+  FreePool (RhsPath8);\r
+  return Status;\r
+}\r
index 1cbd27d8fb524e694464252ac9bb44645ec6a8e1..f4fed64c7217baf8f052e956e415afc60626b47a 100644 (file)
 #define VIRTIO_FS_FILE_SIG \\r
   SIGNATURE_64 ('V', 'I', 'O', 'F', 'S', 'F', 'I', 'L')\r
 \r
+//\r
+// The following limit applies to two kinds of pathnames.\r
+//\r
+// - The length of a POSIX-style, canonical pathname *at rest* never exceeds\r
+//   VIRTIO_FS_MAX_PATHNAME_LENGTH. (Length is defined as the number of CHAR8\r
+//   elements in the canonical pathname, excluding the terminating '\0'.) This\r
+//   is an invariant that is ensured for canonical pathnames created, and that\r
+//   is assumed about canonical pathname inputs (which all originate\r
+//   internally).\r
+//\r
+// - If the length of a UEFI-style pathname *argument*, originating directly or\r
+//   indirectly from the EFI_FILE_PROTOCOL caller, exceeds\r
+//   VIRTIO_FS_MAX_PATHNAME_LENGTH, then the argument is rejected. (Length is\r
+//   defined as the number of CHAR16 elements in the UEFI-style pathname,\r
+//   excluding the terminating L'\0'.) This is a restriction that's checked on\r
+//   external UEFI-style pathname inputs.\r
+//\r
+// The limit is not expected to be a practical limitation; it's only supposed\r
+// to prevent attempts at overflowing size calculations. For both kinds of\r
+// pathnames, separate limits could be used; a common limit is used purely for\r
+// simplicity.\r
+//\r
+#define VIRTIO_FS_MAX_PATHNAME_LENGTH ((UINTN)65535)\r
+\r
 //\r
 // Filesystem label encoded in UCS-2, transformed from the UTF-8 representation\r
 // in "VIRTIO_FS_CONFIG.Tag", and NUL-terminated. Only the printable ASCII code\r
@@ -192,6 +216,14 @@ VirtioFsErrnoToEfiStatus (
   IN INT32 Errno\r
   );\r
 \r
+EFI_STATUS\r
+VirtioFsAppendPath (\r
+  IN     CHAR8   *LhsPath8,\r
+  IN     CHAR16  *RhsPath16,\r
+     OUT CHAR8   **ResultPath8,\r
+     OUT BOOLEAN *RootEscape\r
+  );\r
+\r
 //\r
 // Wrapper functions for FUSE commands (primitives).\r
 //\r
index 15e21772c8ac32368b60ae00663182c872057d63..0c92bccdac869c57320b5000cf969b59d70062d7 100644 (file)
 \r
 [LibraryClasses]\r
   BaseLib\r
+  BaseMemoryLib\r
   DebugLib\r
   MemoryAllocationLib\r
   UefiBootServicesTableLib\r