On 12/7/23 10:44, Gerd Hoffmann wrote: > Extend the ValidateFvHeader function, additionally to the header checks > walk over the list of variables and sanity check them. > > In case we find inconsistencies indicating variable store corruption > return EFI_NOT_FOUND so the variable store will be re-initialized. > > Signed-off-by: Gerd Hoffmann <kra...@redhat.com> > --- > OvmfPkg/VirtNorFlashDxe/VirtNorFlashFvb.c | 82 +++++++++++++++++++++-- > 1 file changed, 77 insertions(+), 5 deletions(-) > > diff --git a/OvmfPkg/VirtNorFlashDxe/VirtNorFlashFvb.c > b/OvmfPkg/VirtNorFlashDxe/VirtNorFlashFvb.c > index 5ee98e9b595a..1bfb14495abd 100644 > --- a/OvmfPkg/VirtNorFlashDxe/VirtNorFlashFvb.c > +++ b/OvmfPkg/VirtNorFlashDxe/VirtNorFlashFvb.c > @@ -185,11 +185,16 @@ ValidateFvHeader ( > IN NOR_FLASH_INSTANCE *Instance > ) > { > - UINT16 Checksum; > - EFI_FIRMWARE_VOLUME_HEADER *FwVolHeader; > - VARIABLE_STORE_HEADER *VariableStoreHeader; > - UINTN VariableStoreLength; > - UINTN FvLength; > + UINT16 Checksum; > + EFI_FIRMWARE_VOLUME_HEADER *FwVolHeader; > + VARIABLE_STORE_HEADER *VariableStoreHeader; > + UINTN VarOffset; > + AUTHENTICATED_VARIABLE_HEADER *VarHeader; > + UINTN VarSize; > + CHAR16 *VarName; > + CHAR8 *VarState; > + UINTN VariableStoreLength; > + UINTN FvLength; > > FwVolHeader = (EFI_FIRMWARE_VOLUME_HEADER *)Instance->RegionBaseAddress; > > @@ -260,6 +265,73 @@ ValidateFvHeader ( > return EFI_NOT_FOUND; > } > > + // check variables
(1) Comment style -- should be preceded and succeeded by otherwise empty // lines > + DEBUG ((DEBUG_INFO, "%a: checking variables\n", __func__)); > + VarOffset = sizeof (*VariableStoreHeader); > + while (VarOffset + sizeof (*VarHeader) < VariableStoreHeader->Size) { (2) The addition is not checked for overflow. When it is evaluated for the first time, it cannot overflow, but I can't easily find a proof that it cannot overflow in further iterations on 32-bit platforms. (The strict "<" relop in itself seems justified; we always expect *some* data after the variable header.) I suggest rewriting as follows: for (;;) { RETURN_STATUS Status; UINTN VarHeaderEnd; Status = SafeUintnAdd (VarOffset, sizeof (*VarHeader), &VarHeaderEnd); if (RETURN_ERROR (Status)) { return EFI_NOT_FOUND; } if (VarHeaderEnd >= VariableStoreHeader->Size) { break; } Perhaps even the second check should return EFI_NOT_FOUND, assuming we require (StartId != 0x55aa) as the only successful termination condition. > + VarHeader = (VOID *)((UINTN)VariableStoreHeader + VarOffset); > + if (VarHeader->StartId != 0x55aa) { > + DEBUG ((DEBUG_INFO, "%a: end of var list\n", __func__)); > + break; > + } > + > + VarSize = sizeof (*VarHeader) + VarHeader->NameSize + > VarHeader->DataSize; (3) Given that we expect that the varstore may be corrupt, this addition can definitely overflow on 32-bit platforms (where the additions are performed in UINT32). I suggest using SafeUintnAdd() from SafeIntLib again. > + if (VarOffset + VarSize > VariableStoreHeader->Size) { (4) This addition should be checked for overflow too. Alternatively -- given that, from previous checks, here we know for sure that VarOffset is strictly smaller than VariableStoreHeader->Size --, the expression can be rearranged as follows (by subtracting VarOffset from both sides): if (VarSize > VariableStoreHeader->Size - VarOffset) { Mathematically this means the same thing, but the subtraction on the right hand side cannot underflow. > + DEBUG (( > + DEBUG_ERROR, > + "%a: invalid variable size: 0x%x + 0x%x + 0x%x + 0x%x > 0x%x\n", > + __func__, > + VarOffset, (5) VarOffset is of type UINTN, which may be UINT32 or UINT64. %x is not a proper format specifier for printing UINT64. PrintLib offers no format specifier for UINTN, only for UINT32 (%x, %u) and UINT64 (%lx / %Lx, %lu / %Lu). Therefore, the best way to format a UINTN value is: - convert it to UINT64 explicitly, - print it with one of the format specifiers matching UINT64. For example, DEBUG ((DEBUG_ERROR, "%Lx\n", (UINT64)VarOffset)); > + sizeof (*VarHeader), (6) same issue, sizeof evals to a UINTN. > + VarHeader->NameSize, > + VarHeader->DataSize, > + VariableStoreHeader->Size these are good (all three are UINT32) > + )); > + return EFI_NOT_FOUND; > + } > + > + VarName = (VOID *)((UINTN)VariableStoreHeader + VarOffset > + + sizeof (*VarHeader)); (7) This seems premature. We should first check that NameSize is (a) nonzero and (b) divisible by 2. > + switch (VarHeader->State) { > + case VAR_HEADER_VALID_ONLY: > + VarState = "header-ok"; (8) Then better make VarState point to a CONST CHAR8, not just a CHAR8. > + VarName = L"<unknown>"; (9) Ditto. (10) If VarName is invalid here when State is VAR_HEADER_VALID_ONLY, then we should calculate VarName only when the State says it's going to be valid. Creating and invalid pointer value first and then overwriting it with something else is "cart before horse". > + break; > + case VAR_ADDED: > + VarState = "ok"; > + break; > + case VAR_ADDED &VAR_IN_DELETED_TRANSITION: > + VarState = "del-in-transition"; > + break; > + case VAR_ADDED &VAR_DELETED: > + case VAR_ADDED &VAR_DELETED &VAR_IN_DELETED_TRANSITION: > + VarState = "deleted"; > + break; > + default: > + DEBUG (( > + DEBUG_ERROR, > + "%a: invalid variable state: 0x%x\n", > + __func__, > + VarHeader->State > + )); This is fine. The State field is UINT8, so it's getting promoted to "int" (INT32). "%x" is fine for printing UINT32 ("unsigned int"), and it's fine to reinterpret nonnegative "int"s as "unsigned int"s, per C std. > + return EFI_NOT_FOUND; > + } > + > + DEBUG (( > + DEBUG_VERBOSE, > + "%a: +0x%04x: name=0x%x data=0x%x '%s' (%a)\n", > + __func__, > + VarOffset, (10) same comment as (5) > + VarHeader->NameSize, > + VarHeader->DataSize, > + VarName, (11) regarding VarName, we have not checked whether it was NUL-terminated; %s relies on that. Alternatively, you can provide the precision as a separate UINTN argument: DEBUG ((DEBUG_VERBOSE, "%.*s\n", (UINTN)(VarHeader->NameSize / 2), VarName)); > + VarState > + )); > + > + VarSize = (VarSize + 3) & ~3; // align Two problems: (12) Unchecked addition. (13) The bitwise negation operator applied to a signed integer is exceeding ugly: (a) It inverts the sign bit, which *happens* to produce a negative value. (b) That negative value is then converted to UINTN (for the bitwise AND), by adding -- mathematically speaking -- (MAX_UINTN+1) to it. This only produces the expected result because the original negative value uses two's complement representation in the first place. It's (almost) better to use the ALIGN_VALUE() macro from "MdePkg/Include/Base.h"; but that too would not check for overflow. Suggestion: UINTN LowBits; LowBits = VarSize & (BIT0 | BIT1); if (LowBits > 0) { Status = SafeUintnAdd (VarSize, BIT2 - LowBits, &VarSize); if (RETURN_ERROR (Status)) { return EFI_NOT_FOUND; } } (14) However, even *this* should be performed at the top, where we check (VarOffset + VarSize > VariableStoreHeader->Size). More precisely, that check should consider the aligned-up variable size. That's because: > + VarOffset += VarSize; this addition here uses the aligned-up VarSize as increment, which may be larger than the previously checked VarSize value -- and so it can land us outside of VariableStoreHeader->Size at once. > + } > + > return EFI_SUCCESS; > } > The general idea is, once we don't trust the varstore, there cannot be a *single* unchecked addition in the code. (Unless we can *prove* that overflow is impossible.) Laszlo -=-=-=-=-=-=-=-=-=-=-=- Groups.io Links: You receive all messages sent to this group. View/Reply Online (#112340): https://edk2.groups.io/g/devel/message/112340 Mute This Topic: https://groups.io/mt/103031342/21656 Group Owner: devel+ow...@edk2.groups.io Unsubscribe: https://edk2.groups.io/g/devel/leave/9847357/21656/1706620634/xyzzy [arch...@mail-archive.com] -=-=-=-=-=-=-=-=-=-=-=-