Harden untrusted input rpmsg virtio - #702
bentheredonethat wants to merge 7 commits into
Conversation
The remote peer controls every byte of a name service message name. If the name fills the wire field without a NUL terminator, endpoint lookup and application callbacks read past the copied name when they treat it as a C string. Terminate the copied name inside the name field before it is used, as upstream Linux RPMsg does. Signed-off-by: Ben Levinsky <ben.levinsky@amd.com> Assisted-by: Codex:GPT-5
When the source is smaller than the destination and contains a NUL, safe_strcpy() includes the copied terminator in its zero-fill length. The resulting memset() writes one byte beyond the destination buffer. Calculate the fill length from the advanced destination pointer. This ensures only bytes remaining inside the destination are cleared. Signed-off-by: Ben Levinsky <ben.levinsky@amd.com> Assisted-by: Codex:GPT-5
virtqueue_create() takes the vring alignment from its caller without checking it. vring_init() rounds the used ring address with a ~(align - 1) mask, so a zero alignment places the used ring at NULL. Reject a zero alignment in the virtqueue constructor and document the requirement. Check the ring pointer unconditionally as well, since VQ_PARAM_CHK() compiles out unless VQ_DEBUG is enabled. Signed-off-by: Ben Levinsky <ben.levinsky@amd.com> Assisted-by: Codex:GPT-5
arnopo
left a comment
There was a problem hiding this comment.
one extra comment else LGTM
| } | ||
|
|
||
| rp_hdr->reserved = idx; | ||
| ept = NULL; |
There was a problem hiding this comment.
You can directly initialize it when you declare the local variale
struct rpmsg_endpoint *ept = NULL;
There was a problem hiding this comment.
Good catch, fixed — but not quite as written, and I want to flag why.
ept was declared at function scope, outside the while (1) loop, and nothing resets it at the bottom of the loop body. Adding the initializer to that declaration would only run once, on entry.
On any later iteration where the new length check rejects the buffer, the lookup is skipped and ept would still hold the endpoint from the previous iteration — so we'd dispatch the callback to
the wrong endpoint and call rpmsg_ept_decref() on it a second time.
So I moved the declaration into the loop body instead, which gives you the direct initialization you asked for and keeps the per-iteration reset:
while (1) {
/*
* Scoped to the loop body so that every iteration starts
* without an endpoint. The lookup below is skipped for
* malformed buffers, so a function-scope variable would keep
* the endpoint found by an earlier iteration and that stale
* pointer would be dispatched to and dereferenced again.
*/
struct rpmsg_endpoint *ept = NULL;
I've added that comment in the code and a paragraph to the commit message covering the scope change, since it's not obvious from the diff alone. Folded into "rpmsg: virtio: Validate received
message lengths" for v3.
The remote peer controls both the used-ring length and the payload length in the rpmsg header. Trusting either value can make cache invalidation and endpoint callbacks access beyond the receive buffer. Cap the driver-side length to the locally allocated buffer size before cache invalidation. Deliver a buffer to an endpoint only when the header and the announced payload fit in the reported length, and release the buffer through the existing path otherwise. Move the endpoint pointer from function scope into the receive loop body and initialize it at its declaration. The endpoint lookup is now conditional, so a function-scope variable would retain the endpoint resolved by a previous iteration whenever the length checks reject a buffer, and that already unreferenced pointer would be dispatched to and unreferenced a second time. Signed-off-by: Ben Levinsky <ben.levinsky@amd.com> Assisted-by: Codex:GPT-5
Name service messages are allocated on the stack and copied in full to the remote peer. safe_strcpy() does not clear bytes after a short name when source and destination sizes match. This can disclose stack contents. Zero-initialize the message before populating its fields. Unused name bytes then never cross the shared-memory trust boundary. Signed-off-by: Ben Levinsky <ben.levinsky@amd.com> Assisted-by: Codex:GPT-5
95f52bd to
be4d485
Compare
virtqueue_create() rejects a zero vring alignment but accepts any other value. vring_init() rounds the used ring address with a ~(align - 1) mask, which only clears low order bits when the alignment is a power of two. A value such as 0x80000001 makes the mask clear high order bits of the computed address instead, placing the used ring outside the vring memory while the local core keeps dereferencing it. Require the alignment to be a power of two in the virtqueue constructor and document the requirement. Signed-off-by: Ben Levinsky <ben.levinsky@amd.com> Assisted-by: Codex:GPT-5
virtqueue_get_buffer() takes the descriptor index from the used ring, which the remote processor writes, and uses it to index vq_descx[] without any bounds check. A remote that reports an index above vq_nentries makes the driver read a cookie from past the end of the array and then write NULL over that same location. vq_ring_free_chain() is reached first and only guards the index with VQ_RING_ASSERT_VALID_IDX(), which expands to nothing unless VQ_DEBUG is enabled, so it does not prevent the access in normal builds. Drop buffers whose descriptor index is out of range, and bound the descriptor chain walk so it stays inside the descriptor table when the assert is compiled out. Signed-off-by: Ben Levinsky <ben.levinsky@amd.com> Assisted-by: Codex:GPT-5
No description provided.