Blame SOURCES/0007-copy-CVE-2022-0485-Fail-nbdcopy-if-NBD-read-or-write.patch

6aa05d
From 1b0b732e6a9b4979fccf6a09eb6704264edf675d Mon Sep 17 00:00:00 2001
6aa05d
From: Eric Blake <eblake@redhat.com>
6aa05d
Date: Thu, 3 Feb 2022 14:25:58 -0600
6aa05d
Subject: [PATCH] copy: CVE-2022-0485: Fail nbdcopy if NBD read or write fails
6aa05d
6aa05d
nbdcopy has a nasty bug when performing multi-threaded copies using
6aa05d
asynchronous nbd calls - it was blindly treating the completion of an
6aa05d
asynchronous command as successful, rather than checking the *error
6aa05d
parameter.  This can result in the silent creation of a corrupted
6aa05d
image in two different ways: when a read fails, we blindly wrote
6aa05d
garbage to the destination; when a write fails, we did not flag that
6aa05d
the destination was not written.
6aa05d
6aa05d
Since nbdcopy already calls exit() on a synchronous read or write
6aa05d
failure to a file, doing the same for an asynchronous op to an NBD
6aa05d
server is the simplest solution.  A nicer solution, but more invasive
6aa05d
to code and thus not done here, might be to allow up to N retries of
6aa05d
the transaction (in case the read or write failure was transient), or
6aa05d
even having a mode where as much data is copied as possible (portions
6aa05d
of the copy that failed would be logged on stderr, and nbdcopy would
6aa05d
still fail with a non-zero exit status, but this would copy more than
6aa05d
just stopping at the first error, as can be done with rsync or
6aa05d
ddrescue).
6aa05d
6aa05d
Note that since we rely on auto-retiring and do NOT call
6aa05d
nbd_aio_command_completed, our completion callbacks must always return
6aa05d
1 (if they do not exit() first), even when acting on *error, so as not
6aa05d
leave the command allocated until nbd_close.  As such, there is no
6aa05d
sane way to return an error to a manual caller of the callback, and
6aa05d
therefore we can drop dead code that calls perror() and exit() if the
6aa05d
callback "failed".  It is also worth documenting the contract on when
6aa05d
we must manually call the callback during the asynch_zero callback, so
6aa05d
that we do not leak or double-free the command; thankfully, all the
6aa05d
existing code paths were correct.
6aa05d
6aa05d
The added testsuite script demonstrates several scenarios, some of
6aa05d
which fail without the rest of this patch in place, and others which
6aa05d
showcase ways in which sparse images can bypass errors.
6aa05d
6aa05d
Once backports are complete, a followup patch on the main branch will
6aa05d
edit docs/libnbd-security.pod with the mailing list announcement of
6aa05d
the stable branch commit ids and release versions that incorporate
6aa05d
this fix.
6aa05d
6aa05d
Reported-by: Nir Soffer <nsoffer@redhat.com>
6aa05d
Fixes: bc896eec4d ("copy: Implement multi-conn, multiple threads, multiple requests in flight.", v1.5.6)
6aa05d
Fixes: https://bugzilla.redhat.com/2046194
6aa05d
Message-Id: <20220203202558.203013-6-eblake@redhat.com>
6aa05d
Acked-by: Richard W.M. Jones <rjones@redhat.com>
6aa05d
Acked-by: Nir Soffer <nsoffer@redhat.com>
6aa05d
[eblake: fix error message per Nir, tweak requires lines in unit test per Rich]
6aa05d
Reviewed-by: Laszlo Ersek <lersek@redhat.com>
6aa05d
6aa05d
(cherry picked from commit 8d444b41d09a700c7ee6f9182a649f3f2d325abb)
6aa05d
Conflicts:
6aa05d
	copy/nbdcopy.h - copyright context
6aa05d
	copy/null-ops.c - no backport of 0b16205e "copy: Implement "null:"
6aa05d
 destination."
6aa05d
        copy/copy-nbd-error.sh - no backport of d5f65e56 ("copy: Do not use
6aa05d
 trim for zeroing"), so one test needed an additional error-trim-rate;
6aa05d
 no backport of 4ff9e62d (copy: Add --request-size option") and friends, so
6aa05d
 this version uses larger transactions, so change error rate of 0.5 to 1;
6aa05d
 no backport of 0b16205e "copy: Implement "null:" destination.", so use
6aa05d
 nbdkit null instead
6aa05d
Note that while the use of NBD_CMD_TRIM can create data corruption, it is
6aa05d
not as severe as what this patch fixes, since trim corruption will only
6aa05d
expose what had previously been on the disk, compared to this patch fixing
6aa05d
a potential leak of nbdcopy heap contents into the destination.
6aa05d
(cherry picked from commit 6c8f2f859926b82094fb5e85c446ea099700fa10)
6aa05d
---
6aa05d
 TODO                        |  1 +
6aa05d
 copy/Makefile.am            |  4 +-
6aa05d
 copy/copy-nbd-error.sh      | 81 +++++++++++++++++++++++++++++++++++++
6aa05d
 copy/file-ops.c             | 17 +++-----
6aa05d
 copy/multi-thread-copying.c | 13 ++++++
6aa05d
 copy/nbdcopy.h              |  7 ++--
6aa05d
 6 files changed, 107 insertions(+), 16 deletions(-)
6aa05d
 create mode 100755 copy/copy-nbd-error.sh
6aa05d
6aa05d
diff --git a/TODO b/TODO
6aa05d
index 510c219..19c21d4 100644
6aa05d
--- a/TODO
6aa05d
+++ b/TODO
6aa05d
@@ -35,6 +35,7 @@ nbdcopy:
6aa05d
  - Better page cache usage, see nbdkit-file-plugin options
6aa05d
    fadvise=sequential cache=none.
6aa05d
  - Consider io_uring if there are performance bottlenecks.
6aa05d
+ - Configurable retries in response to read or write failures.
6aa05d
 
6aa05d
 nbdfuse:
6aa05d
  - If you write beyond the end of the virtual file, it returns EIO.
6aa05d
diff --git a/copy/Makefile.am b/copy/Makefile.am
6aa05d
index d318388..3406cd8 100644
6aa05d
--- a/copy/Makefile.am
6aa05d
+++ b/copy/Makefile.am
6aa05d
@@ -1,5 +1,5 @@
6aa05d
 # nbd client library in userspace
6aa05d
-# Copyright (C) 2020 Red Hat Inc.
6aa05d
+# Copyright (C) 2020-2022 Red Hat Inc.
6aa05d
 #
6aa05d
 # This library is free software; you can redistribute it and/or
6aa05d
 # modify it under the terms of the GNU Lesser General Public
6aa05d
@@ -30,6 +30,7 @@ EXTRA_DIST = \
6aa05d
 	copy-nbd-to-small-nbd-error.sh \
6aa05d
 	copy-nbd-to-sparse-file.sh \
6aa05d
 	copy-nbd-to-stdout.sh \
6aa05d
+	copy-nbd-error.sh \
6aa05d
 	copy-progress-bar.sh \
6aa05d
 	copy-sparse.sh \
6aa05d
 	copy-sparse-allocated.sh \
6aa05d
@@ -105,6 +106,7 @@ TESTS += \
6aa05d
 	copy-nbd-to-sparse-file.sh \
6aa05d
 	copy-stdin-to-nbd.sh \
6aa05d
 	copy-nbd-to-stdout.sh \
6aa05d
+	copy-nbd-error.sh \
6aa05d
 	copy-progress-bar.sh \
6aa05d
 	copy-sparse.sh \
6aa05d
 	copy-sparse-allocated.sh \
6aa05d
diff --git a/copy/copy-nbd-error.sh b/copy/copy-nbd-error.sh
6aa05d
new file mode 100755
6aa05d
index 0000000..bba71db
6aa05d
--- /dev/null
6aa05d
+++ b/copy/copy-nbd-error.sh
6aa05d
@@ -0,0 +1,81 @@
6aa05d
+#!/usr/bin/env bash
6aa05d
+# nbd client library in userspace
6aa05d
+# Copyright (C) 2022 Red Hat Inc.
6aa05d
+#
6aa05d
+# This library is free software; you can redistribute it and/or
6aa05d
+# modify it under the terms of the GNU Lesser General Public
6aa05d
+# License as published by the Free Software Foundation; either
6aa05d
+# version 2 of the License, or (at your option) any later version.
6aa05d
+#
6aa05d
+# This library is distributed in the hope that it will be useful,
6aa05d
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
6aa05d
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
6aa05d
+# Lesser General Public License for more details.
6aa05d
+#
6aa05d
+# You should have received a copy of the GNU Lesser General Public
6aa05d
+# License along with this library; if not, write to the Free Software
6aa05d
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6aa05d
+
6aa05d
+# Tests several scenarios of handling NBD server errors
6aa05d
+# Serves as a regression test for the CVE-2022-0485 fix.
6aa05d
+
6aa05d
+. ../tests/functions.sh
6aa05d
+
6aa05d
+set -e
6aa05d
+set -x
6aa05d
+
6aa05d
+requires nbdkit --exit-with-parent --version
6aa05d
+requires nbdkit --filter=noextents null --version
6aa05d
+requires nbdkit --filter=error pattern --version
6aa05d
+requires nbdkit --filter=nozero memory --version
6aa05d
+
6aa05d
+fail=0
6aa05d
+
6aa05d
+# Failure to get block status should not be fatal, but merely downgrade to
6aa05d
+# reading the entire image as if data
6aa05d
+echo "Testing extents failures on source"
6aa05d
+$VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \
6aa05d
+    error-extents-rate=1 ] [ nbdkit --exit-with-parent -v null 5M ] || fail=1
6aa05d
+
6aa05d
+# Failure to read should be fatal
6aa05d
+echo "Testing read failures on non-sparse source"
6aa05d
+$VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \
6aa05d
+    error-pread-rate=1 ] [ nbdkit --exit-with-parent -v null 5M ] && fail=1
6aa05d
+
6aa05d
+# However, reliable block status on a sparse image can avoid the need to read
6aa05d
+echo "Testing read failures on sparse source"
6aa05d
+$VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error null 5M \
6aa05d
+    error-pread-rate=1 ] [ nbdkit --exit-with-parent -v null 5M ] || fail=1
6aa05d
+
6aa05d
+# Failure to write data should be fatal
6aa05d
+echo "Testing write data failures on arbitrary destination"
6aa05d
+$VG nbdcopy -- [ nbdkit --exit-with-parent -v pattern 5M ] \
6aa05d
+    [ nbdkit --exit-with-parent -v --filter=error --filter=noextents \
6aa05d
+        memory 5M error-pwrite-rate=1 ] && fail=1
6aa05d
+
6aa05d
+# However, writing zeroes can bypass the need for normal writes
6aa05d
+echo "Testing write data failures from sparse source"
6aa05d
+$VG nbdcopy -- [ nbdkit --exit-with-parent -v null 5M ] \
6aa05d
+    [ nbdkit --exit-with-parent -v --filter=error --filter=noextents \
6aa05d
+        memory 5M error-pwrite-rate=1 ] || fail=1
6aa05d
+
6aa05d
+# Failure to write zeroes should be fatal
6aa05d
+echo "Testing write zero failures on arbitrary destination"
6aa05d
+$VG nbdcopy -- [ nbdkit --exit-with-parent -v null 5M ] \
6aa05d
+    [ nbdkit --exit-with-parent -v --filter=error memory 5M \
6aa05d
+        error-trim-rate=1 error-zero-rate=1 ] && fail=1
6aa05d
+
6aa05d
+# However, assuming/learning destination is zero can skip need to write
6aa05d
+echo "Testing write failures on pre-zeroed destination"
6aa05d
+$VG nbdcopy --destination-is-zero -- \
6aa05d
+    [ nbdkit --exit-with-parent -v null 5M ] \
6aa05d
+    [ nbdkit --exit-with-parent -v --filter=error memory 5M \
6aa05d
+        error-pwrite-rate=1 error-zero-rate=1 ] || fail=1
6aa05d
+
6aa05d
+# Likewise, when write zero is not advertised, fallback to normal write works
6aa05d
+echo "Testing write zeroes to destination without zero support"
6aa05d
+$VG nbdcopy -- [ nbdkit --exit-with-parent -v null 5M ] \
6aa05d
+    [ nbdkit --exit-with-parent -v --filter=nozero --filter=error memory 5M \
6aa05d
+        error-zero-rate=1 ] || fail=1
6aa05d
+
6aa05d
+exit $fail
6aa05d
diff --git a/copy/file-ops.c b/copy/file-ops.c
6aa05d
index cc312b4..b19af04 100644
6aa05d
--- a/copy/file-ops.c
6aa05d
+++ b/copy/file-ops.c
6aa05d
@@ -162,10 +162,8 @@ file_asynch_read (struct rw *rw,
6aa05d
 
6aa05d
   file_synch_read (rw, slice_ptr (command->slice),
6aa05d
                    command->slice.len, command->offset);
6aa05d
-  if (cb.callback (cb.user_data, &dummy) == -1) {
6aa05d
-    perror (rw->name);
6aa05d
-    exit (EXIT_FAILURE);
6aa05d
-  }
6aa05d
+  /* file_synch_read called exit() on error */
6aa05d
+  cb.callback (cb.user_data, &dummy);
6aa05d
 }
6aa05d
 
6aa05d
 static void
6aa05d
@@ -177,10 +175,8 @@ file_asynch_write (struct rw *rw,
6aa05d
 
6aa05d
   file_synch_write (rw, slice_ptr (command->slice),
6aa05d
                     command->slice.len, command->offset);
6aa05d
-  if (cb.callback (cb.user_data, &dummy) == -1) {
6aa05d
-    perror (rw->name);
6aa05d
-    exit (EXIT_FAILURE);
6aa05d
-  }
6aa05d
+  /* file_synch_write called exit() on error */
6aa05d
+  cb.callback (cb.user_data, &dummy);
6aa05d
 }
6aa05d
 
6aa05d
 static bool
6aa05d
@@ -206,10 +202,7 @@ file_asynch_zero (struct rw *rw, struct command *command,
6aa05d
 
6aa05d
   if (!file_synch_zero (rw, command->offset, command->slice.len))
6aa05d
     return false;
6aa05d
-  if (cb.callback (cb.user_data, &dummy) == -1) {
6aa05d
-    perror (rw->name);
6aa05d
-    exit (EXIT_FAILURE);
6aa05d
-  }
6aa05d
+  cb.callback (cb.user_data, &dummy);
6aa05d
   return true;
6aa05d
 }
6aa05d
 
6aa05d
diff --git a/copy/multi-thread-copying.c b/copy/multi-thread-copying.c
6aa05d
index 2593ff7..28749ae 100644
6aa05d
--- a/copy/multi-thread-copying.c
6aa05d
+++ b/copy/multi-thread-copying.c
6aa05d
@@ -28,6 +28,7 @@
6aa05d
 #include <errno.h>
6aa05d
 #include <assert.h>
6aa05d
 #include <sys/stat.h>
6aa05d
+#include <inttypes.h>
6aa05d
 
6aa05d
 #include <pthread.h>
6aa05d
 
6aa05d
@@ -374,6 +375,12 @@ finished_read (void *vp, int *error)
6aa05d
 {
6aa05d
   struct command *command = vp;
6aa05d
 
6aa05d
+  if (*error) {
6aa05d
+    fprintf (stderr, "read at offset %" PRId64 " failed: %s\n",
6aa05d
+             command->offset, strerror (*error));
6aa05d
+    exit (EXIT_FAILURE);
6aa05d
+  }
6aa05d
+
6aa05d
   if (allocated || sparse_size == 0) {
6aa05d
     /* If sparseness detection (see below) is turned off then we write
6aa05d
      * the whole command.
6aa05d
@@ -552,6 +559,12 @@ free_command (void *vp, int *error)
6aa05d
   struct command *command = vp;
6aa05d
   struct buffer *buffer = command->slice.buffer;
6aa05d
 
6aa05d
+  if (*error) {
6aa05d
+    fprintf (stderr, "write at offset %" PRId64 " failed: %s\n",
6aa05d
+             command->offset, strerror (*error));
6aa05d
+    exit (EXIT_FAILURE);
6aa05d
+  }
6aa05d
+
6aa05d
   if (buffer != NULL) {
6aa05d
     if (--buffer->refs == 0) {
6aa05d
       free (buffer->data);
6aa05d
diff --git a/copy/nbdcopy.h b/copy/nbdcopy.h
6aa05d
index 3dcc6df..9626a52 100644
6aa05d
--- a/copy/nbdcopy.h
6aa05d
+++ b/copy/nbdcopy.h
6aa05d
@@ -1,5 +1,5 @@
6aa05d
 /* NBD client library in userspace.
6aa05d
- * Copyright (C) 2020 Red Hat Inc.
6aa05d
+ * Copyright (C) 2020-2022 Red Hat Inc.
6aa05d
  *
6aa05d
  * This library is free software; you can redistribute it and/or
6aa05d
  * modify it under the terms of the GNU Lesser General Public
6aa05d
@@ -134,7 +134,8 @@ struct rw_ops {
6aa05d
   bool (*synch_zero) (struct rw *rw, uint64_t offset, uint64_t count);
6aa05d
 
6aa05d
   /* Asynchronous I/O operations.  These start the operation and call
6aa05d
-   * 'cb' on completion.
6aa05d
+   * 'cb' on completion.  'cb' will return 1, for auto-retiring with
6aa05d
+   * asynchronous libnbd calls.
6aa05d
    *
6aa05d
    * The file_ops versions are actually implemented synchronously, but
6aa05d
    * still call 'cb'.
6aa05d
@@ -156,7 +157,7 @@ struct rw_ops {
6aa05d
                        nbd_completion_callback cb);
6aa05d
 
6aa05d
   /* Asynchronously zero.  command->slice.buffer is not used.  If not possible,
6aa05d
-   * returns false.
6aa05d
+   * returns false.  'cb' must be called only if returning true.
6aa05d
    */
6aa05d
   bool (*asynch_zero) (struct rw *rw, struct command *command,
6aa05d
                        nbd_completion_callback cb);
6aa05d
-- 
6aa05d
2.31.1
6aa05d