Skip to content

[Swift] Verify size-prefixed roots from reader position - #9196

Open
carrerasdarren-cell wants to merge 1 commit into
google:masterfrom
carrerasdarren-cell:security/swift-size-prefixed-verifier-root
Open

[Swift] Verify size-prefixed roots from reader position#9196
carrerasdarren-cell wants to merge 1 commit into
google:masterfrom
carrerasdarren-cell:security/swift-size-prefixed-verifier-root

Conversation

@carrerasdarren-cell

Copy link
Copy Markdown

Summary

  • verify Swift roots from the active ByteBuffer.reader position
  • verify optional file identifiers from that same position
  • add regression coverage for malformed decoy roots, truncated identifiers, and a valid prefixed identifier

Problem

Both size-prefixed checked-root APIs call skipPrefix() before delegating to getCheckedRoot. The returned table is constructed from the post-prefix reader position, but verification previously followed a root offset from byte zero and checked a file identifier at byte four.

A malformed size-prefixed buffer could therefore place a valid decoy root where the verifier looked while returning a different malformed table from the post-prefix root. The same malformed table is rejected when checked directly, but the prefixed wrappers accepted it. Generated accessors or mutators could then operate outside the buffer bounds despite the checked API succeeding.

Fix

Capture the active reader position once and use it consistently for file-identifier verification, root verification, and table construction. The file-identifier bounds check now accounts for that position without wrapping on short buffers.

Validation

  • added a Swift regression covering both size-prefixed checked-root APIs
  • added short file-identifier bounds coverage and a valid prefixed control
  • parsed all modified Swift source and test files with swiftc
  • compiled the production Swift sources with a deterministic harness; direct malformed verification rejected, both patched prefixed paths rejected, a truncated identifier rejected, and a valid prefixed identifier was accepted
  • git diff --check passes

@mustiikhalil

Copy link
Copy Markdown
Collaborator

@carrerasdarren-cell Thanks for opening the PR. I was looking at the cpp approach. And it would be better if the swift implementation also follows a similar convention. Where we only skip after we have verified the prefixed value too. It's fine if we duplicate the logic for the verification here getPrefixedSizeCheckedRoot.

bool VerifySizePrefixedBuffer(const char* const identifier) {

TEST_EQ(VerifySizePrefixedMonsterBuffer(verifier), true);

Validate the size prefix before advancing ByteBuffer.reader, then use the active reader position consistently for root, file identifier, and returned-object verification. Cover malformed decoy roots, oversized prefixes, short identifiers, and valid prefixed identifiers.
@carrerasdarren-cell
carrerasdarren-cell force-pushed the security/swift-size-prefixed-verifier-root branch from b4a79a7 to 83b8c03 Compare August 7, 2026 07:22
@carrerasdarren-cell

Copy link
Copy Markdown
Author

Thanks, updated in 83b8c03. Both checked size-prefixed entry points now read and validate the UOffset prefix before calling skipPrefix(). getPrefixedSizeCheckedRoot applies the C++-style prefix <= available bytes check, while getCheckedPrefixedSizeRoot preserves its exact-size requirement. I also added an oversized-prefix regression that verifies rejection occurs without advancing ByteBuffer.reader. The production Swift modules compile with Swift 6.3.3, the modified sources/tests parse, git diff --check passes, and the deterministic malformed/valid harness still passes.

@mustiikhalil mustiikhalil left a comment

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.

LGTM! amazing work! one of the comments can be disregarded if not needed

guard
position >= 0,
position <= storage.capacity,
storage.capacity - position >= size * 2

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.

nit: &* instead of normal *

throw FlatbuffersErrors.bufferDoesntContainID
}
let str = _buffer.readString(at: size, count: size)
let str = _buffer.readString(at: position + size, count: size)

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.

nit: &+

Comment on lines +26 to +40
let prefixPosition = byteBuffer.reader
let prefix: UOffset = try verifier.getValue(at: prefixPosition)
let availableSize = byteBuffer.size &- UOffset(MemoryLayout<UOffset>.size)

if requireExactSize {
guard prefix == availableSize else {
throw FlatbuffersErrors.prefixedSizeNotEqualToBufferSize
}
} else if prefix > availableSize {
throw FlatbuffersErrors.outOfBounds(
position: UInt(prefixPosition)
&+ UInt(MemoryLayout<UOffset>.size)
&+ UInt(prefix),
end: byteBuffer.capacity)
}

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.

Not needed; but if you see fit, and the logic is movable. Would it make sense for this to live in the verifier instead?

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants