[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [PATCH v6 06/17] nbd/server: Support a request payload
From: |
Eric Blake |
Subject: |
Re: [PATCH v6 06/17] nbd/server: Support a request payload |
Date: |
Fri, 8 Sep 2023 12:43:41 -0500 |
User-agent: |
NeoMutt/20230517 |
On Wed, Sep 06, 2023 at 12:52:22PM -0500, Eric Blake wrote:
> On Tue, Sep 05, 2023 at 05:36:15PM +0300, Vladimir Sementsov-Ogievskiy wrote:
> > On 29.08.23 20:58, Eric Blake wrote:
> > > Upcoming additions to support NBD 64-bit effect lengths allow for the
> > > possibility to distinguish between payload length (capped at 32M) and
> > > effect length (64 bits, although we generally assume 63 bits because
> > > of off_t limitations). Without that extension, only the NBD_CMD_WRITE
> > > request has a payload; but with the extension, it makes sense to allow
> > > at least NBD_CMD_BLOCK_STATUS to have both a payload and effect length
> > > in a future patch (where the payload is a limited-size struct that in
> > > turn gives the real effect length as well as a subset of known ids for
> > > which status is requested). Other future NBD commands may also have a
> > > request payload, so the 64-bit extension introduces a new
> > > NBD_CMD_FLAG_PAYLOAD_LEN that distinguishes between whether the header
> > > length is a payload length or an effect length, rather than
> > > hard-coding the decision based on the command; although a client
> > > should never send a command with a payload without the negotiation
> > > phase proving such extension is available, we are now able to
> > > gracefully fail unexpected client payloads while keeping the
> > > connection alive. Note that we do not support the payload version of
> > > BLOCK_STATUS yet.
> > >
> > > Signed-off-by: Eric Blake <eblake@redhat.com>
> > > ---
> > >
> > > v5: retitled from v4 13/24, rewrite on top of previous patch's switch
> > > statement [Vladimir]
> > >
> > > v4: less indentation on several 'if's [Vladimir]
> > > ---
> > > nbd/server.c | 33 ++++++++++++++++++++++++++++-----
> > > nbd/trace-events | 1 +
> > > 2 files changed, 29 insertions(+), 5 deletions(-)
> > >
> > > diff --git a/nbd/server.c b/nbd/server.c
> > > index dd3ab59224c..adcfcdeacb7 100644
> > > --- a/nbd/server.c
> > > +++ b/nbd/server.c
> > > @@ -2334,7 +2334,8 @@ static int coroutine_fn
> > > nbd_co_receive_request(NBDRequestData *req,
> > > Error **errp)
> > > {
> > > NBDClient *client = req->client;
> > > - bool check_length = false;
> > > + bool extended_with_payload;
> > > + bool check_length;
> > > bool check_rofs = false;
> > > bool allocate_buffer = false;
> > > unsigned payload_len = 0;
> > > @@ -2350,6 +2351,9 @@ static int coroutine_fn
> > > nbd_co_receive_request(NBDRequestData *req,
> > >
> > > trace_nbd_co_receive_request_decode_type(request->cookie,
> > > request->type,
> > >
> > > nbd_cmd_lookup(request->type));
> > > + check_length = extended_with_payload = client->mode >=
> > > NBD_MODE_EXTENDED &&
> > > + request->flags & NBD_CMD_FLAG_PAYLOAD_LEN;
> > > +
> > > switch (request->type) {
> > > case NBD_CMD_DISC:
> > > /* Special case: we're going to disconnect without a reply,
> > > @@ -2366,6 +2370,14 @@ static int coroutine_fn
> > > nbd_co_receive_request(NBDRequestData *req,
> > > break;
> > >
> > > case NBD_CMD_WRITE:
> > > + if (client->mode >= NBD_MODE_EXTENDED) {
> > > + if (!extended_with_payload) {
> > > + /* The client is noncompliant. Trace it, but proceed. */
> > > +
> > > trace_nbd_co_receive_ext_payload_compliance(request->from,
> > > +
> > > request->len);
> > > + }
> > > + valid_flags |= NBD_CMD_FLAG_PAYLOAD_LEN;
> > > + }
> > > payload_len = request->len;
> > > check_length = true;
> > > allocate_buffer = true;
> > > @@ -2407,6 +2419,15 @@ static int coroutine_fn
> > > nbd_co_receive_request(NBDRequestData *req,
> >
> > more context:
> >
> > /* Payload and buffer handling. */
> > if (!payload_len) {
> > req->complete = true;
>
> At this point, payload_len is equal to 0 for all but NBD_CMD_WRITE. [1]
>
> > }
> > if (check_length && request->len > NBD_MAX_BUFFER_SIZE) {
> > /* READ, WRITE, CACHE */
> > error_setg(errp, "len (%" PRIu64 ") is larger than max len (%u)",
> > request->len, NBD_MAX_BUFFER_SIZE);
> > return -EINVAL;
> > }
> >
> >
> > > + if (extended_with_payload && !allocate_buffer) {
> >
> > it's correct but strange, as allocate_buffer is (READ or WRITE), and READ
> > is totally unrelated here.
>
> Oh, you do have a point. If a client mistakenly passes the
> extended_with_payload flag on NBD_CMD_READ, we end up skipping this
> code which tries to parse off that payload, meaning we could be out of
> sync for reacting to the next command; if the client is particularly
> malicious, they could send payload that resembles another valid
> command. Checking specifically for !WRITE rather than for
> !allocate_buffer is more accurate, although I was trying to avoid
> duplicating checks that were covered in the switch statement above.
>
> I'll have to think about this a bit.
>
> >
> > > + /*
> > > + * For now, we don't support payloads on other commands; but
> > > + * we can keep the connection alive by ignoring the payload.
> >
> > Was it in specification, that we can safely ignore unknown payload for
> > known commands?
>
> Well, at this point, the client is already non-compliant, so anything
> we do is best effort only (the client may have set the bit but not
> sent any payload, at which case we block until the client does send
> another command but we consume that command as payload; or it sent
> more payload than the size it advertises and so we see a magic number
> mismatch when trying to interpret those extra bytes as the next
> command).
>
> But yes, the current spec wording mentions consuming payload bytes and
> then failing with NBD_EINVAL. Is there a potential that we need to
> change the spec wording, if we think a malicious client can abuse the
> flag to force the server into a deadlock scenario which can be abused
> as a denial-of-service attack against other clients, compared to the
> more conservative approach of just disconnecting because the client
> sent a bad flag? Generally, I look at disconnecting in response to
> client behavior as the least favorable response to take; attempting to
> return an error message is nicer. And being blocked on I/O is not
> necessarily a denial of service (it isn't actively consuming CPU
> cycles, and we don't stop servicing parallel clients).
>
> >
> > > + */
> > > + assert(request->type != NBD_CMD_WRITE);
> > > + payload_len = request->len;
> >
> > what I don't like here, is that we already set req->complete to true for
> > this request, when payload_len was zero.
> >
> > Probably, for extended_with_payload requests we should initialize
> > payload_len at top of the function?
>
> Well, we DO initialize it to 0, and then assign it under NBD_CMD_READ.
> But the code that sets req->complete should probably NOT be setting it
> if we are about to attempt reading payload bytes; that is, back at
> [1], the code there probably needs to be conditional on
> extended_with_payload.
>
> >
> > > + request->len = 0;
> > > + }
> > > if (allocate_buffer) {> /* READ, WRITE */
> > > req->data = blk_try_blockalign(client->exp->common.blk,
> > > @@ -2417,10 +2438,12 @@ static int coroutine_fn
> > > nbd_co_receive_request(NBDRequestData *req,
> > > }
> > > }
> > > if (payload_len) {
> > > - /* WRITE */
> > > - assert(req->data);
> > > - ret = nbd_read(client->ioc, req->data, payload_len,
> > > - "CMD_WRITE data", errp);
> > > + if (req->data) {
> >
> > and req->data is actually (READ or WRITE) ( == allocated_buffer) as well.
> >
> > I'd prefer here "if (request->type == NBD_CMD_WRITE)" here
>
> Looks like I'll be respinning this patch slightly.
Here's what I'm planning to squash in. Your idea of setting
payload_len up front makes sense. I will also have to squash in some
followups to 17/17, when I start accepting payload for
NBD_CMD_BLOCK_STATUS.
diff --git i/nbd/server.c w/nbd/server.c
index d54dd0604bf..35d3f8989f2 100644
--- i/nbd/server.c
+++ w/nbd/server.c
@@ -2317,36 +2317,41 @@ static int coroutine_fn nbd_co_send_bitmap(NBDClient
*client,
* to the client (although the caller may still need to disconnect after
* reporting the error).
*/
static int coroutine_fn nbd_co_receive_request(NBDRequestData *req,
NBDRequest *request,
Error **errp)
{
NBDClient *client = req->client;
bool extended_with_payload;
- bool check_length;
+ bool check_length = false;
bool check_rofs = false;
bool allocate_buffer = false;
+ bool payload_okay = false;
unsigned payload_len = 0;
int valid_flags = NBD_CMD_FLAG_FUA;
int ret;
g_assert(qemu_in_coroutine());
assert(client->recv_coroutine == qemu_coroutine_self());
ret = nbd_receive_request(client, request, errp);
if (ret < 0) {
return ret;
}
trace_nbd_co_receive_request_decode_type(request->cookie, request->type,
nbd_cmd_lookup(request->type));
- check_length = extended_with_payload = client->mode >= NBD_MODE_EXTENDED &&
+ extended_with_payload = client->mode >= NBD_MODE_EXTENDED &&
request->flags & NBD_CMD_FLAG_PAYLOAD_LEN;
+ if (extended_with_payload) {
+ payload_len = request->len;
+ check_length = true;
+ }
switch (request->type) {
case NBD_CMD_DISC:
/* Special case: we're going to disconnect without a reply,
* whether or not flags, from, or len are bogus */
req->complete = true;
return -EIO;
case NBD_CMD_READ:
@@ -2360,18 +2365,19 @@ static int coroutine_fn
nbd_co_receive_request(NBDRequestData *req,
case NBD_CMD_WRITE:
if (client->mode >= NBD_MODE_EXTENDED) {
if (!extended_with_payload) {
/* The client is noncompliant. Trace it, but proceed. */
trace_nbd_co_receive_ext_payload_compliance(request->from,
request->len);
}
valid_flags |= NBD_CMD_FLAG_PAYLOAD_LEN;
}
+ payload_okay = true;
payload_len = request->len;
check_length = true;
allocate_buffer = true;
check_rofs = true;
break;
case NBD_CMD_FLUSH:
break;
@@ -2401,38 +2407,38 @@ static int coroutine_fn
nbd_co_receive_request(NBDRequestData *req,
if (!payload_len) {
req->complete = true;
}
if (check_length && request->len > NBD_MAX_BUFFER_SIZE) {
/* READ, WRITE, CACHE */
error_setg(errp, "len (%" PRIu64 ") is larger than max len (%u)",
request->len, NBD_MAX_BUFFER_SIZE);
return -EINVAL;
}
- if (extended_with_payload && !allocate_buffer) {
+ if (payload_len && !payload_okay) {
/*
* For now, we don't support payloads on other commands; but
* we can keep the connection alive by ignoring the payload.
*/
assert(request->type != NBD_CMD_WRITE);
- payload_len = request->len;
request->len = 0;
}
if (allocate_buffer) {
/* READ, WRITE */
req->data = blk_try_blockalign(client->exp->common.blk,
request->len);
if (req->data == NULL) {
error_setg(errp, "No memory");
return -ENOMEM;
}
}
if (payload_len) {
- if (req->data) {
+ if (payload_okay) {
+ assert(req->data);
ret = nbd_read(client->ioc, req->data, payload_len,
"CMD_WRITE data", errp);
} else {
ret = nbd_drop(client->ioc, payload_len, errp);
}
if (ret < 0) {
return -EIO;
}
req->complete = true;
--
Eric Blake, Principal Software Engineer
Red Hat, Inc.
Virtualization: qemu.org | libguestfs.org