Skip to content

Harden untrusted input rpmsg virtio - #702

Open
bentheredonethat wants to merge 7 commits into
OpenAMP:mainfrom
bentheredonethat:Harden-untrusted-input-rpmsg-virtio
Open

bentheredonethat wants to merge 7 commits into
OpenAMP:mainfrom
bentheredonethat:Harden-untrusted-input-rpmsg-virtio

Conversation

@bentheredonethat

Copy link
Copy Markdown
Contributor

No description provided.

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 arnopo 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.

one extra comment else LGTM

Comment thread lib/rpmsg/rpmsg_virtio.c Outdated
}

rp_hdr->reserved = idx;
ept = NULL;

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.

You can directly initialize it when you declare the local variale

struct rpmsg_endpoint *ept = NULL;

@bentheredonethat bentheredonethat Sep 15, 2026

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.

@arnopo

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
@bentheredonethat
bentheredonethat force-pushed the Harden-untrusted-input-rpmsg-virtio branch from 95f52bd to be4d485 Compare September 15, 2026 14:02
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
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.

2 participants