Eric Blake
2018-Dec-21 02:57 UTC
[Libguestfs] [nbdkit PATCH] connections: Don't use uninit memory on early client EOF
Fuzzing with afl found a bug where a 27 byte client sequence
can cause nbdkit to report a strange error message:
$ printf %s $'000\1IHAVEOPT000\6'$'000\7'$'000\1x00' |
tr 0 '\0' |
./nbdkit -s memory size=1m >/dev/null
nbdkit: memory: error: client exceeded maximum number of options (32)
The culprit? The client is hanging up on a message boundary,
so conn->recv() returns 0 for EOF instead of -1 for read error,
and our code blindly continues on in a for loop (re-)parsing
the leftover data from the previous option.
Let's fix things to uniformly quit trying to negotiate with a client
that has early EOF at a message boundary, just as we do for one that
quits mid-field, with the one difference that we treat a message
boundary as a warning instead of an error because a client hanging up
after an option response that it didn't like (rather than sending
NBD_OPT_ABORT to inform the server that it won't be negotiating
further) is a surprisingly common behavior among some existing clients.
Signed-off-by: Eric Blake <eblake@redhat.com>
---
src/connections.c | 60 ++++++++++++++++++++++++++++++-----------------
1 file changed, 39 insertions(+), 21 deletions(-)
diff --git a/src/connections.c b/src/connections.c
index 58ed6b0..577f466 100644
--- a/src/connections.c
+++ b/src/connections.c
@@ -600,6 +600,31 @@ send_newstyle_option_reply_info_export (struct connection
*conn,
return 0;
}
+/* Sub-function during _negotiate_handshake_newstyle, to uniformly handle
+ * a client hanging up on a message boundary.
+ */
+static int __attribute__ ((format (printf, 4, 5)))
+conn_recv_full (struct connection *conn, void *buf, size_t len,
+ const char *fmt, ...)
+{
+ int r = conn->recv (conn, buf, len);
+ va_list args;
+
+ if (r == -1) {
+ va_start (args, fmt);
+ nbdkit_verror (fmt, args);
+ va_end (args);
+ return -1;
+ }
+ if (r == 0) {
+ /* During negotiation, client EOF on message boundary is less
+ * severe than failure in the middle of the buffer. */
+ debug ("client closed input socket, closing connection");
+ return -1;
+ }
+ return r;
+}
+
/* Sub-function of _negotiate_handshake_newstyle_options below. It
* must be called on all non-error paths out of the options for-loop
* in that function.
@@ -639,10 +664,9 @@ _negotiate_handshake_newstyle_options (struct connection
*conn)
const char *optname;
for (nr_options = 0; nr_options < MAX_NR_OPTIONS; ++nr_options) {
- if (conn->recv (conn, &new_option, sizeof new_option) == -1) {
- nbdkit_error ("read: %m");
+ if (conn_recv_full (conn, &new_option, sizeof new_option,
+ "read: %m") == -1)
return -1;
- }
version = be64toh (new_option.version);
if (version != NEW_VERSION) {
@@ -675,10 +699,9 @@ _negotiate_handshake_newstyle_options (struct connection
*conn)
switch (option) {
case NBD_OPT_EXPORT_NAME:
- if (conn->recv (conn, data, optlen) == -1) {
- nbdkit_error ("read: %s: %m", name_of_nbd_opt (option));
+ if (conn_recv_full (conn, data, optlen,
+ "read: %s: %m", name_of_nbd_opt (option))
== -1)
return -1;
- }
/* Apart from printing it, ignore the export name. */
data[optlen] = '\0';
debug ("newstyle negotiation: %s: "
@@ -715,10 +738,9 @@ _negotiate_handshake_newstyle_options (struct connection
*conn)
if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_INVALID)
== -1)
return -1;
- if (conn->recv (conn, data, optlen) == -1) {
- nbdkit_error ("read: %s: %m", name_of_nbd_opt (option));
+ if (conn_recv_full (conn, data, optlen,
+ "read: %s: %m", name_of_nbd_opt (option))
== -1)
return -1;
- }
continue;
}
@@ -738,10 +760,9 @@ _negotiate_handshake_newstyle_options (struct connection
*conn)
if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_INVALID)
== -1)
return -1;
- if (conn->recv (conn, data, optlen) == -1) {
- nbdkit_error ("read: %s: %m", name_of_nbd_opt (option));
+ if (conn_recv_full (conn, data, optlen,
+ "read: %s: %m", name_of_nbd_opt (option))
== -1)
return -1;
- }
continue;
}
@@ -780,10 +801,9 @@ _negotiate_handshake_newstyle_options (struct connection
*conn)
case NBD_OPT_INFO:
case NBD_OPT_GO:
optname = name_of_nbd_opt (option);
- if (conn->recv (conn, data, optlen) == -1) {
- nbdkit_error ("read: %s: %m", optname);
+ if (conn_recv_full (conn, data, optlen,
+ "read: %s: %m", optname) == -1)
return -1;
- }
if (optlen < 6) { /* 32 bit export length + 16 bit nr info */
debug ("newstyle negotiation: %s option length < 6",
optname);
@@ -880,10 +900,9 @@ _negotiate_handshake_newstyle_options (struct connection
*conn)
/* Unknown option. */
if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_UNSUP) == -1)
return -1;
- if (conn->recv (conn, data, optlen) == -1) {
- nbdkit_error ("read: %m");
+ if (conn_recv_full (conn, data, optlen,
+ "read: %m") == -1)
return -1;
- }
}
/* Note, since it's not very clear from the protocol doc, that the
@@ -931,10 +950,9 @@ _negotiate_handshake_newstyle (struct connection *conn)
}
/* Client now sends us its 32 bit flags word ... */
- if (conn->recv (conn, &conn->cflags, sizeof conn->cflags) == -1)
{
- nbdkit_error ("read: %m");
+ if (conn_recv_full (conn, &conn->cflags, sizeof conn->cflags,
+ "read: %m") == -1)
return -1;
- }
conn->cflags = be32toh (conn->cflags);
/* ... which we check for accuracy. */
debug ("newstyle negotiation: client flags: 0x%x",
conn->cflags);
--
2.17.2
Richard W.M. Jones
2018-Dec-21 07:56 UTC
Re: [Libguestfs] [nbdkit PATCH] connections: Don't use uninit memory on early client EOF
On Thu, Dec 20, 2018 at 08:57:02PM -0600, Eric Blake wrote:> Fuzzing with afl found a bug where a 27 byte client sequence > can cause nbdkit to report a strange error message: > > $ printf %s $'000\1IHAVEOPT000\6'$'000\7'$'000\1x00' | tr 0 '\0' | > ./nbdkit -s memory size=1m >/dev/null > nbdkit: memory: error: client exceeded maximum number of options (32) > > The culprit? The client is hanging up on a message boundary, > so conn->recv() returns 0 for EOF instead of -1 for read error, > and our code blindly continues on in a for loop (re-)parsing > the leftover data from the previous option. > > Let's fix things to uniformly quit trying to negotiate with a client > that has early EOF at a message boundary, just as we do for one that > quits mid-field, with the one difference that we treat a message > boundary as a warning instead of an error because a client hanging up > after an option response that it didn't like (rather than sending > NBD_OPT_ABORT to inform the server that it won't be negotiating > further) is a surprisingly common behavior among some existing clients. > > Signed-off-by: Eric Blake <eblake@redhat.com> > --- > src/connections.c | 60 ++++++++++++++++++++++++++++++----------------- > 1 file changed, 39 insertions(+), 21 deletions(-) > > diff --git a/src/connections.c b/src/connections.c > index 58ed6b0..577f466 100644 > --- a/src/connections.c > +++ b/src/connections.c > @@ -600,6 +600,31 @@ send_newstyle_option_reply_info_export (struct connection *conn, > return 0; > } > > +/* Sub-function during _negotiate_handshake_newstyle, to uniformly handle > + * a client hanging up on a message boundary. > + */ > +static int __attribute__ ((format (printf, 4, 5))) > +conn_recv_full (struct connection *conn, void *buf, size_t len, > + const char *fmt, ...) > +{ > + int r = conn->recv (conn, buf, len); > + va_list args; > + > + if (r == -1) { > + va_start (args, fmt); > + nbdkit_verror (fmt, args); > + va_end (args); > + return -1; > + } > + if (r == 0) { > + /* During negotiation, client EOF on message boundary is less > + * severe than failure in the middle of the buffer. */ > + debug ("client closed input socket, closing connection"); > + return -1; > + } > + return r; > +} > + > /* Sub-function of _negotiate_handshake_newstyle_options below. It > * must be called on all non-error paths out of the options for-loop > * in that function. > @@ -639,10 +664,9 @@ _negotiate_handshake_newstyle_options (struct connection *conn) > const char *optname; > > for (nr_options = 0; nr_options < MAX_NR_OPTIONS; ++nr_options) { > - if (conn->recv (conn, &new_option, sizeof new_option) == -1) { > - nbdkit_error ("read: %m"); > + if (conn_recv_full (conn, &new_option, sizeof new_option, > + "read: %m") == -1) > return -1; > - } > > version = be64toh (new_option.version); > if (version != NEW_VERSION) { > @@ -675,10 +699,9 @@ _negotiate_handshake_newstyle_options (struct connection *conn) > > switch (option) { > case NBD_OPT_EXPORT_NAME: > - if (conn->recv (conn, data, optlen) == -1) { > - nbdkit_error ("read: %s: %m", name_of_nbd_opt (option)); > + if (conn_recv_full (conn, data, optlen, > + "read: %s: %m", name_of_nbd_opt (option)) == -1) > return -1; > - } > /* Apart from printing it, ignore the export name. */ > data[optlen] = '\0'; > debug ("newstyle negotiation: %s: " > @@ -715,10 +738,9 @@ _negotiate_handshake_newstyle_options (struct connection *conn) > if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_INVALID) > == -1) > return -1; > - if (conn->recv (conn, data, optlen) == -1) { > - nbdkit_error ("read: %s: %m", name_of_nbd_opt (option)); > + if (conn_recv_full (conn, data, optlen, > + "read: %s: %m", name_of_nbd_opt (option)) == -1) > return -1; > - } > continue; > } > > @@ -738,10 +760,9 @@ _negotiate_handshake_newstyle_options (struct connection *conn) > if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_INVALID) > == -1) > return -1; > - if (conn->recv (conn, data, optlen) == -1) { > - nbdkit_error ("read: %s: %m", name_of_nbd_opt (option)); > + if (conn_recv_full (conn, data, optlen, > + "read: %s: %m", name_of_nbd_opt (option)) == -1) > return -1; > - } > continue; > } > > @@ -780,10 +801,9 @@ _negotiate_handshake_newstyle_options (struct connection *conn) > case NBD_OPT_INFO: > case NBD_OPT_GO: > optname = name_of_nbd_opt (option); > - if (conn->recv (conn, data, optlen) == -1) { > - nbdkit_error ("read: %s: %m", optname); > + if (conn_recv_full (conn, data, optlen, > + "read: %s: %m", optname) == -1) > return -1; > - } > > if (optlen < 6) { /* 32 bit export length + 16 bit nr info */ > debug ("newstyle negotiation: %s option length < 6", optname); > @@ -880,10 +900,9 @@ _negotiate_handshake_newstyle_options (struct connection *conn) > /* Unknown option. */ > if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_UNSUP) == -1) > return -1; > - if (conn->recv (conn, data, optlen) == -1) { > - nbdkit_error ("read: %m"); > + if (conn_recv_full (conn, data, optlen, > + "read: %m") == -1) > return -1; > - } > } > > /* Note, since it's not very clear from the protocol doc, that the > @@ -931,10 +950,9 @@ _negotiate_handshake_newstyle (struct connection *conn) > } > > /* Client now sends us its 32 bit flags word ... */ > - if (conn->recv (conn, &conn->cflags, sizeof conn->cflags) == -1) { > - nbdkit_error ("read: %m"); > + if (conn_recv_full (conn, &conn->cflags, sizeof conn->cflags, > + "read: %m") == -1) > return -1; > - } > conn->cflags = be32toh (conn->cflags); > /* ... which we check for accuracy. */ > debug ("newstyle negotiation: client flags: 0x%x", conn->cflags); > -- > 2.17.2Thanks for diagnosing this. I have pushed the patch because I wanted to continue with fuzzing. There are some small changes (only in the error message text) because I rebased it on top of a patch I had queued up to improve error reporting. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com virt-top is 'top' for virtual machines. Tiny program with many powerful monitoring features, net stats, disk stats, logging, etc. http://people.redhat.com/~rjones/virt-top
Reasonably Related Threads
- [PATCH nbdkit 2/2] server: Split out NBD protocol code from connections code.
- [PATCH nbdkit 0/2] server: Split out NBD protocol code from connections code.
- [PATCH nbdkit] server: Implement minimal implementation of set/list metadata contexts.
- [nbdkit PATCH] connections: Implement NBD_OPT_INFO
- [nbdkit PATCH] server: Adjust limit on max NBD_OPT_* from client