server: Fix regression for NBD_OPT_INFO before NBD_OPT_GO
commita6b88b195a959b17524d1c8353fd425d4891dc5f
authorEric Blake <eblake@redhat.com>
Wed, 18 Sep 2019 22:14:14 +0000 (18 17:14 -0500)
committerEric Blake <eblake@redhat.com>
Thu, 19 Sep 2019 11:33:27 +0000 (19 06:33 -0500)
tree06134cd4ae4b6630f40f2d3a82aa97587a028e40
parentfd2deeb10de15dff97d07a4f8e4f188f7084b1b4
server: Fix regression for NBD_OPT_INFO before NBD_OPT_GO

Most known NBD clients do not bother with NBD_OPT_INFO (except for
clients like 'qemu-nbd --list' that don't ever intend to connect), but
go straight to NBD_OPT_GO.  However, it's not too hard to hack up qemu
to add in an extra client step (whether info on the same name, or more
interestingly, info on a different name), as a patch against qemu
commit 6f214b30445:

| diff --git i/nbd/client.c w/nbd/client.c
| index f6733962b49b..425292ac5ea9 100644
| --- i/nbd/client.c
| +++ w/nbd/client.c
| @@ -1038,6 +1038,14 @@ int nbd_receive_negotiate(AioContext *aio_context, QIOChannel *ioc,
|           * TLS).  If it is not available, fall back to
|           * NBD_OPT_LIST for nicer error messages about a missing
|           * export, then use NBD_OPT_EXPORT_NAME.  */
| +        if (getenv ("HACK"))
| +            info->name[0]++;
| +        result = nbd_opt_info_or_go(ioc, NBD_OPT_INFO, info, errp);
| +        if (getenv ("HACK"))
| +            info->name[0]--;
| +        if (result < 0) {
| +            return -EINVAL;
| +        }
|          result = nbd_opt_info_or_go(ioc, NBD_OPT_GO, info, errp);
|          if (result < 0) {
|              return -EINVAL;

This works just fine in 1.14.0, where we call .open only once (so the
INFO and GO repeat calls into the same plugin handle), but in 1.14.1
it regressed into causing an assertion failure: we are now calling
.open a second time on a connection that is already opened:

$ nbdkit -rfv null &
$ hacked-qemu-io -f raw -r nbd://localhost -c quit
...
nbdkit: null[1]: debug: null: open readonly=1
nbdkit: backend.c:179: backend_open: Assertion `h->handle == NULL' failed.

Worse, on the mainline development, we have recently made it possible
for plugins to actively report different information for different
export names; for example, a plugin may choose to report different
answers for .can_write on export A than for export B; but if we share
cached handles, then an NBD_OPT_INFO on one export prevents correct
answers for NBD_OPT_GO on the second export name.  (The HACK envvar in
my qemu modifications can be used to demonstrate cross-name requests,
which are even less likely in a real client).

The solution is to call .close after NBD_OPT_INFO, coupled with enough
glue logic to reset cached connection handles back to the state
expected by .open.  This in turn means factoring out another backend_*
function, but also gives us an opportunity to change
backend_set_handle to no longer accept NULL.

The assertion failure is, to some extent, a possible denial of service
attack (one client can force nbdkit to exit by merely sending OPT_INFO
before OPT_GO, preventing the next client from connecting), although
this is mitigated by using TLS to weed out untrusted clients.  Still,
the fact that we introduced a potential DoS attack while trying to fix
a traffic amplification security bug is not very nice.

Sadly, as there are no known clients that easily trigger this mode of
operation (OPT_INFO before OPT_GO), there is no easy way to cover this
via a testsuite addition.  I may end up hacking something into libnbd.

Fixes: c05686f957
Signed-off-by: Eric Blake <eblake@redhat.com>
server/backend.c
server/connections.c
server/filters.c
server/internal.h
server/plugins.c
server/protocol-handshake-newstyle.c