gateway/uplink: bound writes against the block-pool constant - #313
Open
fderepas wants to merge 1 commit into
Open
gateway/uplink: bound writes against the block-pool constant#313fderepas wants to merge 1 commit into
fderepas wants to merge 1 commit into
Conversation
… raw `CONFIG_POUCH_BLOCK_SIZE`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
pouch_gateway_uplink_write()(src/gateway/uplink.c) bounds itsbuf_writeinto a block-pool element against
GW_BLOCK_MAX_BYTES, which is defined as theraw
CONFIG_POUCH_BLOCK_SIZE. The pool element itself is dimensioned fromMAX_PLAINTEXT_BLOCK_SIZE, which usesCONFIG_POUCH_BLOCK_SIZErounded down toa power of two. For any non-power-of-two
CONFIG_POUCH_BLOCK_SIZE, the writebound exceeds the element capacity and peer-supplied bytes overflow the pool
element. This PR derives the bound from the same constant as the allocation and
adds a build-time power-of-two assertion.
Severity: Medium — peer-controlled pool overflow, config-conditional (requires a
non-power-of-two
CONFIG_POUCH_BLOCK_SIZE; not reachable at the default 512).The defect
src/gateway/uplink.c:This has the correct
MIN-and-loop shape — it just bounds against the wrongconstant. The pool element is sized by
MAX_PLAINTEXT_BLOCK_SIZE(
port/zephyr/blockbuf.c:K_MEM_SLAB_DEFINE(blockbuf, WB_UP(POUCH_BUF_OVERHEAD + MAX_PLAINTEXT_BLOCK_SIZE), …)), and that constant is rounded down (src/block.h):with
LOG2a floor (port/include/pouch/port.h):So the write bound is
CONFIG_POUCH_BLOCK_SIZE, the capacity is3 + 2^floor(log2(CONFIG_POUCH_BLOCK_SIZE)), and the comment's claim that the slot"holds at least
CONFIG_POUCH_BLOCK_SIZEbytes" is false for every non-power-of-twovalue.
Arithmetic
CONFIG_POUCH_BLOCK_SIZEMAX_PLAINTEXT_BLOCK_SIZEGW_BLOCK_MAX_BYTESAt powers of two the bound is exactly 3 bytes under capacity (the block header), so
the code is safe by coincidence of the header size, not by construction.
The overflow needs no large single write:
uplink->wblockpersists across calls andis only submitted at
buf_size_get >= GW_BLOCK_MAX_BYTES, so ordinary small bearerchunks accumulate. At
CONFIG_POUCH_BLOCK_SIZE = 1000, five 128-byte BLE-sizedrecv()chunks put 640 bytes into a 515-byte element.Trust source / reachability
payload/lenare the raw bytes a connected device pushed at the gateway; thegateway forwards node uplinks to the cloud without decrypting them, so there is
no authentication gate on this path — the trust source is the peer device. Content
is fully peer-controlled; the overrun length is set by the configuration, not the
peer. On Zephyr the overrun lands in the next
k_mem_slabelement, whose first wordis the free-list
nextpointer (a write-what-where primitive). Reachability isgated only on a non-power-of-two
CONFIG_POUCH_BLOCK_SIZE, which the Kconfig acceptswith no
rangeand no warning (see below).Root-cause note
Because of the same rounding, a user who sets
CONFIG_POUCH_BLOCK_SIZE = 1000silently gets 512-byte blocks everywhere else (
block_space_get,MAX_CIPHERTEXT_BLOCK_SIZE, the slab element). One Kconfig option is consumed rawin this one place and rounded in every other — that inconsistency, not the
MINitself, is the bug.
CONFIG_POUCH_BLOCK_SIZE(src/Kconfig) carries norangeandno power-of-two wording, and no
BUILD_ASSERTin the tree constrains it (the onepower-of-two assert,
port/zephyr/transport/coap/blockwise.c, constrains thedifferent
CONFIG_POUCH_COAP_BLOCK_SIZE).src/gateway/uplink.cis the only write bound in the tree spelled as a raw Kconfigvalue rather than a
block.hconstant; the siblings get it right(
src/downlink.cbounds againstMAX_CIPHERTEXT_BLOCK_SIZE,src/entry.candsrc/stream.cagainstblock_space_get).Reproduction (AddressSanitizer)
W6c-asan-test.ctranscribesGW_BLOCK_MAX_BYTES,pouch_gateway_uplink_write,buf_claim/buf_write,LOG2, theblock.hmacros, and the slab geometryverbatim, and feeds 128-byte bearer chunks. The discriminator is the compile-time
CONFIG_POUCH_BLOCK_SIZE:At 512 (power of two) it survives; at 1000 (non-power-of-two) ASan faults on the
fifth accumulated chunk:
Rebuilding with
-DFIX(bound =MAX_BLOCK_PAYLOAD_SIZE) atCONFIG_POUCH_BLOCK_SIZE = 1000survives cleanly — the same discriminating test theraw witness describes.
Why the existing tests miss it
Two independent reasons: (1)
tests/pouch/gateway/src/stub_blockbuf.creplaces theslab with a 4096-byte
malloc(8× the real element), the same double that maskedthe gateway-downlink overflow; and (2) the tests only build at the default
power-of-two
CONFIG_POUCH_BLOCK_SIZE, where the bug is unreachable by construction.tests/pouch/gateway/src/uplink.cdoes push 20×200 = 4000 bytes — enough to overflowa real 515-byte element several times — and passes only because of the stub.
The fix
Primary — derive the bound from the same constant as the allocation:
MAX_BLOCK_PAYLOAD_SIZE(=MAX_PLAINTEXT_BLOCK_SIZE − BLOCK_HEADER_SIZE) is therounded payload the pool element is built for, so the bound now follows the
allocation by construction and equals the previous value at every power of two.
Secondary — fail the build on the surprising rounding (independently worth
having, since a user asking for 1000-byte blocks silently getting 512 is itself a
defect):
The secondary fix alone is not sufficient: asserting a side condition makes
this miscalculation impossible but leaves the invariant "write bound == allocation
size" resting on a coincidence between two unrelated files. Only the primary fix
makes the bound follow the allocation by construction. Recommend both.
How this was found
This was found by creating a model of Pouch using a theorem prover, then proving using
Weakest Preconditions the expected properties on the source code. WP cannot discharge
the
buf_writevalid_destobligation for a non-power-of-twoCONFIG_POUCH_BLOCK_SIZEbecause the write bound (
GW_BLOCK_MAX_BYTES) exceeds the proven element capacity(
MAX_PLAINTEXT_BLOCK_SIZE); the contrast with the sibling bounds that useblock.hconstants pinpointed the raw-Kconfig bound. Confirmed independently under
AddressSanitizer (above), config-conditionally, with a faithful transcription of the
shipped
uplink/block/slab geometry.