[Libguestfs] [PATCH nbdkit 2/2] file: Allow a file descriptor to be passed to the plugin

Richard W.M. Jones
2022-08-17T15:38:11Z

This allows you to pass in a file descriptor to the plugin, eg:

  $ exec 6<>/var/tmp/fedora-36.img
  $ ./nbdkit file fd=6 --run 'nbdinfo $uri'

Note this was previously possible on most platforms using /dev/fd/<N>,
but not all platforms that have file descriptors support this
(although it is POSIX) and it seems better to make this explicit.
---
 plugins/file/nbdkit-file-plugin.pod |  22 +++++-
 tests/Makefile.am                   |   2 +
 plugins/file/file.c                 | 108 +++++++++++++++++++---------
 tests/test-file-fd.sh               |  65 +++++++++++++++++
 4 files changed, 160 insertions(+), 37 deletions(-)

diff --git a/plugins/file/nbdkit-file-plugin.pod
b/plugins/file/nbdkit-file-plugin.pod
index 49c330663..dadbe9432 100644
--- a/plugins/file/nbdkit-file-plugin.pod
+++ b/plugins/file/nbdkit-file-plugin.pod
@@ -11,6 +11,10 @@ nbdkit-file-plugin - nbdkit file plugin

  nbdkit file dir=DIRECTORY

+=for paragraph
+
+ nbdkit file fd=FILE_DESCRIPTOR
+
 =head1 DESCRIPTION

 C<nbdkit-file-plugin> is a file serving plugin for L<nbdkit(1)>.
@@ -22,10 +26,16 @@ If you use the C parameter the plugin works in a
different mode
 where it serves files from the given C<DIRECTORY>, chosen by the
 client using the NBD export name.

+If you use the C<fd> parameter then you can pass the file descriptor
+of a single disk to the plugin, inherited from the parent process.
+This can be useful where special permissions / capabilities are needed
+to open the file or you want to run nbdkit in a sandboxed environment.
+
 =head1 PARAMETERS

-Either B<file> or B must be given which controls the mode of
the
-plugin, either serving a single file or the files in a directory.
+B<file>, B or B<fd> must be given. This controls the
mode of the
+plugin, either serving a single file, the files in a directory, or a
+single file descriptor.

 =over 4

@@ -72,6 +82,14 @@ read-ahead.  See also L<posix_fadvise(2)>.

 The default is C<normal>.

+=item B<fd=>FILE_DESCRIPTOR
+
+(nbdkit E<ge> 1.34, not Windows)
+
+The parameter is the number of a file descriptor.  Serve the file or
+device already open on this file descriptor.  The file descriptor is
+usually inherited from the parent process.
+
 =item [B<file=>]FILENAME

 Serve the file named C<FILENAME>.  A local block device name can also
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 3dc428540..3667a1dea 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -732,10 +732,12 @@ EXTRA_DIST += \
 TESTS += \
 	test-file.sh \
 	test-file-readonly.sh \
+	test-file-fd.sh \
 	$(NULL)
 EXTRA_DIST += \
 	test-file.sh \
 	test-file-readonly.sh \
+	test-file-fd.sh \
 	$(NULL)
 LIBGUESTFS_TESTS += test-file-block

diff --git a/plugins/file/file.c b/plugins/file/file.c
index f673ec132..4e41e738a 100644
--- a/plugins/file/file.c
+++ b/plugins/file/file.c
@@ -70,9 +70,15 @@
 #include "isaligned.h"
 #include "fdatasync.h"

-static enum { mode_none, mode_filename, mode_directory } mode = mode_none;
+static enum {
+  mode_none,
+  mode_filename,
+  mode_directory,
+  mode_filedesc,
+} mode = mode_none;
 static char *filename = NULL;
 static char *directory = NULL;
+static int filedesc = -1;

 /* posix_fadvise mode: -1 = don't set it, or POSIX_FADV_*. */
 static int fadvise_mode @@ -184,7 +190,7 @@ file_config (const char *key, const
char *value)
     if (mode != mode_none) {
     wrong_mode:
       nbdkit_error ("%s parameter can only appear once on the command
line",
-                    "file|dir");
+                    "file|dir|fd");
       return -1;
     }
     mode = mode_filename;
@@ -202,6 +208,17 @@ file_config (const char *key, const char *value)
     if (!directory)
       return -1;
   }
+  else if (strcmp (key, "fd") == 0) {
+    if (mode != mode_none) goto wrong_mode;
+    mode = mode_filedesc;
+    assert (filedesc == -1);
+    if (nbdkit_parse_int ("fd", value, &filedesc) == -1)
+      return -1;
+    if (filedesc <= 2) {
+      nbdkit_error ("file descriptor must be >= 3");
+      return -1;
+    }
+  }
   else if (strcmp (key, "fadvise") == 0) {
     /* As this is a hint, if the kernel doesn't support the feature
      * ignore the parameter.
@@ -263,18 +280,26 @@ file_config_complete (void)
   struct stat sb;

   if (mode == mode_none) {
-    nbdkit_error ("you must supply either [file=]<FILENAME> or
"
-                  "dir=<DIRNAME> parameter after the plugin name
"
+    nbdkit_error ("you must supply [file=]<FILENAME>, "
+                  "dir=<DIRNAME> or fd=<FD> "
+                  "parameter after the plugin name "
                   "on the command line");
     return -1;
   }
   if (mode == mode_filename) {
     assert (filename != NULL);
     assert (directory == NULL);
+    assert (filedesc == -1);
   }
   if (mode == mode_directory) {
     assert (filename == NULL);
     assert (directory != NULL);
+    assert (filedesc == -1);
+  }
+  if (mode == mode_filedesc) {
+    assert (filename == NULL);
+    assert (directory == NULL);
+    assert (filedesc >= 3);
   }

   /* Sanity check now, rather than waiting for first client open.
@@ -299,6 +324,8 @@ file_config_complete (void)
       return -1;
     }
     break;
+  case mode_filedesc:
+    break;
   default: abort ();
   }

@@ -341,6 +368,7 @@ file_list_exports (int readonly, int default_only,

   switch (mode) {
   case mode_filename:
+  case mode_filedesc:
     return nbdkit_add_export (exports, "", NULL);

   case mode_directory:
@@ -408,59 +436,69 @@ file_open (int readonly)
   const char *file;
   int dfd = -1;

+  h = malloc (sizeof *h);
+  if (h == NULL) {
+    nbdkit_error ("malloc: %m");
+    return NULL;
+  }
+  h->can_write = !readonly;
+  h->fd = -1;
+
   switch (mode) {
   case mode_directory:
     file = nbdkit_export_name ();
     if (strchr (file, '/')) {
       nbdkit_error ("exportname cannot contain /");
+      free (h);
       errno = EINVAL;
       return NULL;
     }
     dfd = open (directory, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
     if (dfd == -1) {
       nbdkit_error ("open %s: %m", directory);
+      free (h);
       return NULL;
     }
     break;
   case mode_filename:
     file = filename;
     break;
+  case mode_filedesc:
+    h->fd = dup (filedesc);
+    if (h->fd == -1) {
+      nbdkit_error ("dup fd=%d: %m", filedesc);
+      free (h);
+      return NULL;
+    }
+    file = "<file descriptor>"; /* This is needed for error
messages. */
+    break;
   default: abort ();
   }

-  h = malloc (sizeof *h);
-  if (h == NULL) {
-    nbdkit_error ("malloc: %m");
-    if (dfd != -1)
-      close (dfd);
-    return NULL;
-  }
-
-  flags = O_CLOEXEC|O_NOCTTY;
-  if (readonly) {
-    flags |= O_RDONLY;
-    h->can_write = false;
-  }
-  else {
-    flags |= O_RDWR;
-    h->can_write = true;
-  }
-
-  h->fd = openat (dfd, file, flags);
-  if (h->fd == -1 && !readonly) {
-    nbdkit_debug ("open O_RDWR failed, falling back to read-only: %s:
%m",
-                  file);
-    flags = (flags & ~O_ACCMODE) | O_RDONLY;
-    h->fd = openat (dfd, file, flags);
-    h->can_write = false;
-  }
   if (h->fd == -1) {
-    nbdkit_error ("open: %s: %m", file);
-    if (dfd != -1)
-      close (dfd);
-    free (h);
-    return NULL;
+    flags = O_CLOEXEC|O_NOCTTY;
+    if (readonly)
+      flags |= O_RDONLY;
+    else
+      flags |= O_RDWR;
+
+    h->fd = openat (dfd, file, flags);
+    if (h->fd == -1 && !readonly) {
+      nbdkit_debug ("open O_RDWR failed, falling back to read-only: %s:
%m",
+                    file);
+      flags = (flags & ~O_ACCMODE) | O_RDONLY;
+      h->fd = openat (dfd, file, flags);
+      h->can_write = false;
+    }
+    if (h->fd == -1) {
+      nbdkit_error ("open: %s: %m", file);
+      if (dfd != -1)
+        close (dfd);
+      free (h);
+      return NULL;
+    }
   }
+
   if (dfd != -1)
     close (dfd);

diff --git a/tests/test-file-fd.sh b/tests/test-file-fd.sh
new file mode 100755
index 000000000..6c98ad9e5
--- /dev/null
+++ b/tests/test-file-fd.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# nbdkit
+# Copyright (C) 2013-2022 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# * Neither the name of Red Hat nor the names of its contributors may be
+# used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS
IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+# Test the file plugin fd parameter.
+
+source ./functions.sh
+set -e
+set -x
+
+requires_plugin file
+requires_nbdsh_uri
+requires $TRUNCATE --version
+
+sock=$(mktemp -u /tmp/nbdkit-test-sock.XXXXXX)
+files="file-fd.pid file-fd.img $sock"
+rm -f $files
+cleanup_fn rm -f $files
+
+$TRUNCATE -s 16384 file-fd.img
+exec 6<>file-fd.img
+start_nbdkit -P file-fd.pid -U $sock file fd=6
+
+nbdsh -u "nbd+unix://?socket=$sock" -c '
+assert not h.is_read_only()
+assert h.get_size() == 16384
+
+buf0 = bytearray(1024)
+buf1 = b"1" * 1024
+buf2 = b"2" * 1024
+h.pwrite(buf1 + buf2 + buf1 + buf2, 1024)
+buf = h.pread(8192, 0)
+assert buf == buf0 + buf1 + buf2 + buf1 + buf2 + buf0*3
+h.zero(4096, 1024)
+buf = h.pread(8192, 0)
+assert buf == buf0*8
+'
--
2.37.0.rc2