If a server replies to NBD_OPT_SET_META_CONTEXT first with
NBD_REP_META_CONTEXT, then with NBD_REP_ERR_*, we have already
modified h->meta_contexts, but fail to clean it up before moving on to
OPT_GO in the OPT_META_CONTEXT.CHECK_REPLY's default: handler (this
corner-case bug has been present since we got meta context support
working back in commits a97e2779 and 1b560b62). Per the NBD spec,
because the overall SET_META_CONTEXT did not succeed, we should
therefore not attempt NBD_CMD_BLOCK_STATUS; but because
h->meta_contexts was left non-empty, we mistakenly report
nbd_can_meta_context(first_string) as true, and allow
nbd_block_status() to proceed; this forms a protocol violation not
pre-filtered by our usual litany of nbd_set_strict_mode() checks, and
therefore we can trigger unspecified server behavior.
Rather than open-code a cleanup loop on all error paths, I instead fix
the problem by adding a meta_valid witness that is only set to true in
select places. The obvious place is when handling NBD_REP_ACK; but it
also makes sense if we didn't call NBD_OPT_SET_META_CONTEXT at all: if
negotiating structured replies failed (including oldstyle servers), or
if the user never called nbd_add_meta_context() and therefore doesn't
care (blindly returning 0 to nbd_can_meta_context() in those cases is
fine; our existing tests/oldstyle and tests/newstyle-limited cover
that). However, whereas we previously always returned a 0/1 answer
for nbd_can_meta_context once we are in transmission phase, this new
witness now means that in the corner case of explicit server failure
to NBD_OPT_SET_META_CONTEXT, nbd_can_meta_context() returns -1 to call
attention to the earlier server failure (although it is something that
is unlikely enough in practice that I doubt anyone will experience it
in the wild).
The change in nbd_can_meta_context() to use h->meta_valid instead of
h->eflags has an additional latent semantic difference not observable
at this time (because both h->meta_valid and h->eflags are set in
tandem by successful nbd_opt_go()/nbd_opt_info() in their current
two-command sequence), but which matters to future patches adding new
APIs. On the one hand, once we add nbd_set_request_meta_context() to
stop nbd_opt_go() from performing NBD_OPT_SET_META_CONTEXT, we want
nbd_can_meta_context() to fail during transmission phase if the
context was not set elsewhere during negotiation, rather than blindly
returning 0. On the other hand, when manually calling
nbd_opt_set_meta_context() during negotiation, we do not want it to
touch h->eflags, but do want nbd_can_meta_context() to start working
right away.
Note that the use of a witness flag also allows me to slightly change
when the list of previously-negotiated contexts is freed - instead of
doing it in nbd_internal_reset_size_and_flags(), that function now
simply marks any existing vector as invalid; and the actual wipe is
done when starting a new NBD_OPT_SET_META_CONTEXT or when closing
struct nbd_handle. There are still some oddities to be cleaned up in
later patches by moving when the flag is marked invalid (for example,
we really want nbd_set_export_name() to wipe both flags and meta
contexts, but the act of NBD_OPT_GO should not wipe contexts, and the
act of NBD_OPT_SET_META_CONTEXT should not wipe flags), but I'm
leaving those further tweaks for later patches to make this one easier
to review.
Testing this is surprisingly tricky; compliant servers don't do this
(hence I don't really have anything automatic to add to libnbd's
testsuite). However, I came up with the following temporary hack to
nbdkit to demonstrate the problem:
| diff --git i/server/protocol-handshake-newstyle.c
w/server/protocol-handshake-newstyle.c
| index fedee48f..b3f34c98 100644
| --- i/server/protocol-handshake-newstyle.c
| +++ w/server/protocol-handshake-newstyle.c
| @@ -884,7 +884,7 @@ negotiate_handshake_newstyle_options (void)
| opt_index += querylen;
| nr_queries--;
| }
| - if (send_newstyle_option_reply (option, NBD_REP_ACK) == -1)
| + if (send_newstyle_option_reply (option, NBD_REP_ERR_POLICY) == -1)
| return -1;
| }
| debug ("newstyle negotiation: %s: reply complete", optname);
| diff --git i/server/protocol.c w/server/protocol.c
| index 2ac77055..97235bda 100644
| --- i/server/protocol.c
| +++ w/server/protocol.c
| @@ -191,7 +191,7 @@ validate_request (uint16_t cmd, uint16_t flags, uint64_t offset,
uint32_t count,
|
| /* Block status allowed? */
| if (cmd == NBD_CMD_BLOCK_STATUS) {
| - if (!conn->structured_replies) {
| + if (!conn->structured_replies || 1) {
| nbdkit_error ("invalid request: "
| "%s: structured replies was not negotiated",
| name_of_nbd_cmd (cmd));
Coupled with this command line:
$ ./run /path/to/nbdkit -U- memory 1 --run 'nbdsh --base -u "$uri" -c
"
try:
print(h.can_meta_context(nbd.CONTEXT_BASE_ALLOCATION))
except nbd.Error as ex:
print(ex.string)
def f(c, o, e, er):
pass
try:
print(h.block_status(1, 0, f))
except nbd.Error as ex:
print(ex.string)
"'
Pre-patch, we see can_meta_context succeed, and that we let
NBD_CMD_BLOCK_STATUS leak through causing an nbdkit error message:
True
nbdkit: memory.0: error: invalid request: NBD_CMD_BLOCK_STATUS: structured replies was not
negotiated
nbd_block_status: block-status: command failed: Invalid argument
Post-patch, can_meta_context diagnoses the problem, and block_status
is blocked client-side with no messages needed from nbdkit:
nbd_can_meta_context: need a successful server meta context request first: Invalid
argument
nbd_block_status: did not negotiate any metadata contexts, either you did not call
nbd_add_meta_context before connecting or the server does not support it: Operation not
supported
Fixes: a97e2779 ("states: Negotiate base:allocation with the server.", v0.1)
---
lib/internal.h | 1 +
generator/API.ml | 4 +++-
generator/states-newstyle-opt-meta-context.c | 9 +++++++--
generator/states-reply-structured.c | 1 +
lib/flags.c | 14 ++++++++------
lib/handle.c | 5 +++++
lib/rw.c | 2 +-
7 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/lib/internal.h b/lib/internal.h
index d601d5d..8aaff15 100644
--- a/lib/internal.h
+++ b/lib/internal.h
@@ -180,6 +180,7 @@ struct nbd_handle {
bool structured_replies; /* If we negotiated NBD_OPT_STRUCTURED_REPLY */
/* Vector of negotiated metadata contexts. */
+ bool meta_valid;
meta_vector meta_contexts;
/* The socket or a wrapper if using GnuTLS. */
diff --git a/generator/API.ml b/generator/API.ml
index 3e948aa..62e2d54 100644
--- a/generator/API.ml
+++ b/generator/API.ml
@@ -1785,7 +1785,9 @@ "can_meta_context", {
longdesc = "\
Returns true if the server supports the given meta context
(see L<nbd_add_meta_context(3)>). Returns false if
-the server does not.
+the server does not. It is possible for this command to fail if
+meta contexts were requested but there is a missing or failed
+attempt at NBD_OPT_SET_META_CONTEXT during option negotiation.
The single parameter is the name of the metadata context,
for example C<LIBNBD_CONTEXT_BASE_ALLOCATION>.
diff --git a/generator/states-newstyle-opt-meta-context.c
b/generator/states-newstyle-opt-meta-context.c
index 8f4dee2..a6a5271 100644
--- a/generator/states-newstyle-opt-meta-context.c
+++ b/generator/states-newstyle-opt-meta-context.c
@@ -29,6 +29,9 @@ STATE_MACHINE {
*/
assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE);
nbd_internal_reset_size_and_flags (h);
+ for (i = 0; i < h->meta_contexts.len; ++i)
+ free (h->meta_contexts.ptr[i].name);
+ meta_vector_reset (&h->meta_contexts);
if (h->opt_current == NBD_OPT_LIST_META_CONTEXT) {
assert (h->opt_mode);
assert (h->structured_replies);
@@ -44,7 +47,7 @@ STATE_MACHINE {
}
}
- assert (h->meta_contexts.len == 0);
+ assert (!h->meta_valid);
/* Calculate the length of the option request data. */
len = 4 /* exportname len */ + strlen (h->export_name) + 4 /* nr queries */;
@@ -194,8 +197,10 @@ STATE_MACHINE {
CALL_CALLBACK (h->opt_cb.completion, &err);
nbd_internal_free_option (h);
}
- else
+ else {
SET_NEXT_STATE (%^OPT_GO.START);
+ h->meta_valid = true;
+ }
break;
case NBD_REP_META_CONTEXT: /* A context. */
if (len > maxpayload)
diff --git a/generator/states-reply-structured.c b/generator/states-reply-structured.c
index aaf75b8..f18c999 100644
--- a/generator/states-reply-structured.c
+++ b/generator/states-reply-structured.c
@@ -445,6 +445,7 @@ STATE_MACHINE {
assert (CALLBACK_IS_NOT_NULL (cmd->cb.fn.extent));
assert (h->bs_entries);
assert (length >= 12);
+ assert (h->meta_valid);
/* Need to byte-swap the entries returned, but apart from that we
* don't validate them.
diff --git a/lib/flags.c b/lib/flags.c
index 87c20ee..91efc1a 100644
--- a/lib/flags.c
+++ b/lib/flags.c
@@ -37,9 +37,7 @@ nbd_internal_reset_size_and_flags (struct nbd_handle *h)
h->exportsize = 0;
h->eflags = 0;
- for (i = 0; i < h->meta_contexts.len; ++i)
- free (h->meta_contexts.ptr[i].name);
- meta_vector_reset (&h->meta_contexts);
+ h->meta_valid = false;
h->block_minimum = 0;
h->block_preferred = 0;
h->block_maximum = 0;
@@ -75,6 +73,11 @@ nbd_internal_set_size_and_flags (struct nbd_handle *h,
eflags &= ~NBD_FLAG_SEND_FAST_ZERO;
}
+ if (!h->structured_replies || h->request_meta_contexts.len == 0) {
+ assert (h->meta_contexts.len == 0);
+ h->meta_valid = true;
+ }
+
h->exportsize = exportsize;
h->eflags = eflags;
return 0;
@@ -194,9 +197,8 @@ nbd_unlocked_can_meta_context (struct nbd_handle *h, const char
*name)
{
size_t i;
- if (h->eflags == 0) {
- set_error (EINVAL, "server has not returned export flags, "
- "you need to connect to the server first");
+ if (h->request_meta_contexts.len && !h->meta_valid) {
+ set_error (EINVAL, "need a successful server meta context request first");
return -1;
}
diff --git a/lib/handle.c b/lib/handle.c
index 8713e18..03f45a4 100644
--- a/lib/handle.c
+++ b/lib/handle.c
@@ -115,6 +115,8 @@ nbd_create (void)
void
nbd_close (struct nbd_handle *h)
{
+ size_t i;
+
nbd_internal_set_error_context ("nbd_close");
if (h == NULL)
@@ -127,6 +129,9 @@ nbd_close (struct nbd_handle *h)
free (h->bs_entries);
nbd_internal_reset_size_and_flags (h);
+ for (i = 0; i < h->meta_contexts.len; ++i)
+ free (h->meta_contexts.ptr[i].name);
+ meta_vector_reset (&h->meta_contexts);
nbd_internal_free_option (h);
free_cmd_list (h->cmds_to_issue);
free_cmd_list (h->cmds_in_flight);
diff --git a/lib/rw.c b/lib/rw.c
index 81f8f35..2ab85f3 100644
--- a/lib/rw.c
+++ b/lib/rw.c
@@ -482,7 +482,7 @@ nbd_unlocked_aio_block_status (struct nbd_handle *h,
return -1;
}
- if (h->meta_contexts.len == 0) {
+ if (!h->meta_valid || h->meta_contexts.len == 0) {
set_error (ENOTSUP, "did not negotiate any metadata contexts, "
"either you did not call nbd_add_meta_context before "
"connecting or the server does not support it");
--
2.37.2