Skip to content

Container: Improve container component loading with bounds validation#2687

Open
vtcao297 wants to merge 2 commits into
slimbootloader:masterfrom
vtcao297:vtcao/harden_libcontainer
Open

Container: Improve container component loading with bounds validation#2687
vtcao297 wants to merge 2 commits into
slimbootloader:masterfrom
vtcao297:vtcao/harden_libcontainer

Conversation

@vtcao297
Copy link
Copy Markdown
Contributor

Improvements to the container library component loading and decompression pathways:

  • Add bounded traversal functions for container header entry parsing with explicit checks

  • Implement explicit container bounds validation at registration time to reject malformed or incomplete container regions before processing

  • Add runtime input validation in decompressor metadata parsing to enforce minimum buffer sizes and NULL pointer checks in decompression GetInfo APIs

  • Improve flow robustness by validating component metadata before decompressor interaction

  • Refactor some extremely nested blocks for better readability

These changes strengthen boot-time security by enforcing defense-in-depth precondition validation across container parsing, component verification, and decompression initialization stages.

@vtcao297 vtcao297 requested a review from a team as a code owner April 27, 2026 20:43
@vtcao297 vtcao297 force-pushed the vtcao/harden_libcontainer branch 3 times, most recently from 4cff6b2 to 147d053 Compare April 28, 2026 00:56

// Check for overflow before addition
if ((CurrEntry->HashSize > (MAX_UINT64 - sizeof(COMPONENT_ENTRY) - CurrOffset)) ||
((CurrOffset + sizeof(COMPONENT_ENTRY) + CurrEntry->HashSize) > HeaderLimit)) {
Copy link
Copy Markdown
Collaborator

@tsaikevin tsaikevin Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vtcao297 line 164 and 165 are duplicated, the MAX value of HeaderLimit is MAX_UINT64 too. so line 165 is sufficient. fyi
btw, line 170 checking is equivalent(potentially dead code)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right, the first guard condition is kind of impossible since the worst-case sum is only ~ 2^32 + 267 which is much less 2^64 so it can never be true, over-defensive coding :). And yes 170 would then also be redundant as 165, so i'll just combine them. Thanks!

@vtcao297 vtcao297 force-pushed the vtcao/harden_libcontainer branch from 147d053 to 531652a Compare April 29, 2026 04:40

HeaderLimit = ContainerHdr->DataOffset;
CurrOffset = (UINT64)((UINT8 *)CurrEntry - (UINT8 *)ContainerHdr);
if (CurrOffset > HeaderLimit) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vtcao297 do we consider checking the "==" condition where no space left at CurrOffset?thx

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the == case is handled safely by the next block.

  • if (CurrOffset > HeaderLimit) rejects true out-of-range.

  • if ((HeaderLimit - CurrOffset) < sizeof (COMPONENT_ENTRY)) rejects “no room left” and partial room.

So when CurrOffset == HeaderLimit, remaining space is 0, which is < sizeof(COMPONENT_ENTRY), and it returns FALSE. Valid containers shouldn't be affected because a valid component entry must have at least sizeof(COMPONENT_ENTRY) bytes remaining.

ScrLen = 0;
}

Status = DecompressGetInfo (CompressHdr->Signature, CompressHdr->Data,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vtcao297 when line 1254 is true, DstLen and ScrLen are set 0, is it still necessary to call DecompressGetInfo in line 1259? thx

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I guess it doesn't, I was trying to flatten the previous codeblock with its nested if..elses. Thanks for catching this, it didn't manifest in my testing because the worse case scenario was just over allocation of additional 64K in the scratch buffer AllocBuf if the compression algo was LZMA. Will fix this soon

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in the latest revision.

@tsaikevin
Copy link
Copy Markdown
Collaborator

@vtcao297 since many changes merged in container library, it actually introduces some chance to break(Copilot suggests some but I'm unable to verify:( ) Suggest to run static analysis again after code merged. fyi

}
for (Index = 0; Index < ContainerHdr->Count; Index++) {
CompEntry = (COMPONENT_ENTRY *)((UINT8 *)(CompEntry + 1) + CompEntry->HashSize);
if (!GetNextComponentEntryBounded (ContainerHdr, CompEntry, &CompEntry)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks CompEntry is repeated param. Its not passing next param.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the old logic at ContainerLib.c, the code effectively did this:

-Read CompEntry->HashSize from the current entry.
-Compute the next pointer with raw pointer arithmetic.
-Only after that, compare the resulting offset against ContainerHdr->DataOffset.

That means it could still read fields from a partially out-of-bounds current entry before discovering the next pointer was invalid.

The replacement via GetNextComponentEntryBounded keeps the same core traversal rule:

next entry = current entry + sizeof(COMPONENT_ENTRY) + HashSize

but adds two security properties before returning the next pointer:

  • IsComponentEntryReadable ensures the fixed-size current entry header is fully readable inside the container header bounds before reading HashSize.
  • GetNextComponentEntryBounded ensures the computed next entry does not exceed DataOffset

Improvements to the container library component loading and decompression pathways:

- Add bounded traversal functions for container header entry parsing with explicit checks

- Implement explicit container bounds validation at registration time to reject
  malformed or incomplete container regions before processing

- Add runtime input validation in decompressor metadata parsing to enforce
  minimum buffer sizes and NULL pointer checks in decompression GetInfo APIs

- Improve flow robustness by validating component metadata before decompressor interaction

- Refactor some extremely nested blocks for better readability

These changes strengthen boot-time security by enforcing defense-in-depth
precondition validation across container parsing, component verification, and
decompression initialization stages.

Signed-off-by: Vincent Cao <[email protected]>
A guard for FS->Ext2FsNumCylinder from commit 193129e was inadvertently removed due to commit 3e853aa.
This addresses that regression but also improve on the original fix by decreasing caps the descriptor
table allocation at 256 KB — comfortably within the ~3.6 (lowest) -> 22 (highest) configured MB pool budget
for supported platforms.  The original fix, the threshold is the wrong bound MAX_UINTN/sizeof(EXT2GD) = 288
trillion groups on 64-bit, and 67 million groups on 32-bit. That's not a meaningful filesystem
limit — it only guards against a UINTN multiplication overflow, but not against a value that would
potentially corrupt the pool allocator's UINT32 arithmetic.

Signed-off-by: Vincent Cao <[email protected]>
@vtcao297 vtcao297 force-pushed the vtcao/harden_libcontainer branch from 531652a to 5c4ee07 Compare April 30, 2026 03:14
@vtcao297
Copy link
Copy Markdown
Contributor Author

@vtcao297 since many changes merged in container library, it actually introduces some chance to break(Copilot suggests some but I'm unable to verify:( ) Suggest to run static analysis again after code merged. fyi

Kevin - I didn't realize static analysis is being run against PRs, can you point me to the link result so I can review if the items being caught are real and need to be addressed before merge.

@bejeanmo
Copy link
Copy Markdown
Collaborator

bejeanmo commented May 4, 2026

@vtcao297 since many changes merged in container library, it actually introduces some chance to break(Copilot suggests some but I'm unable to verify:( ) Suggest to run static analysis again after code merged. fyi

Kevin - I didn't realize static analysis is being run against PRs, can you point me to the link result so I can review if the items being caught are real and need to be addressed before merge.

It isn't. I think kevin is suggesting to check after its merged.

Copy link
Copy Markdown
Collaborator

@gdong1 gdong1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @vtcao297, thanks for the security hardening work here.

I think this change works perfectly fine, but the per-access bounded checks modify a significant amount of existing code and increase binary size.

How about an alternative approach: validate-once at registration time? Since SBL runs single-threaded and container memory can't be mutated between registration and use, a single structural validation pass at RegisterContainer() should be sufficient. This keeps all existing traversal code untouched.

I put together a quick implementation for reference:
[https://github.com/slimbootloader/slimbootloader/compare/master...gdong1:slimbootloader:validate-container-bounds]

Feel free to take a look and let me know if you'd like to adopt this approach or incorporate parts of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants