[Libguestfs] [PATCH nbdkit v2] tmpdisk: Pass any parameters as shell variables to the command.
Eric Blake
eblake at redhat.com
Tue Apr 7 18:59:29 UTC 2020
On 4/7/20 11:24 AM, Richard W.M. Jones wrote:
> This allows us to be much more flexible about what commands can be
> used. It also means we do not need to encode any special behaviour
> for type or label parameters.
> ---
> plugins/tmpdisk/nbdkit-tmpdisk-plugin.pod | 91 +++++++++-----
> plugins/tmpdisk/tmpdisk.c | 147 ++++++++++++++--------
> plugins/tmpdisk/default-command.sh.in | 6 +
> 3 files changed, 164 insertions(+), 80 deletions(-)
>
> diff --git a/plugins/tmpdisk/nbdkit-tmpdisk-plugin.pod b/plugins/tmpdisk/nbdkit-tmpdisk-plugin.pod
> index 490bcf6c..f9e3296a 100644
> --- a/plugins/tmpdisk/nbdkit-tmpdisk-plugin.pod
> +++ b/plugins/tmpdisk/nbdkit-tmpdisk-plugin.pod
> @@ -4,9 +4,9 @@ nbdkit-tmpdisk-plugin - create a fresh temporary filesystem for each client
>
> =head1 SYNOPSIS
>
> - nbdkit tmpdisk [size=]SIZE
> - [type=ext4|xfs|vfat|...] [label=LABEL]
> - [command=COMMAND]
> + nbdkit tmpdisk [size=]SIZE [type=ext4|xfs|vfat|...] [label=LABEL]
> +
> + nbdkit tmpdisk [size=]SIZE command=COMMAND
>
Is it worth spelling this:
nbdkit tmpdisk [size=]SIZE command=COMMAND [VAR=VALUE...]
> +=item C<$size>
> +
> +The virtual size in bytes. This is the C<size> parameter, converted
> +to bytes.
Is it worth mentioning that if the command ends up rounding the passed
in size, the size passed in to nbdkit may differ from the size
advertised to the client?
> +=head2 Serve a fresh operating system to each client
> +
> + nbdkit tmpdisk 16G \
> + command=' virt-builder -o "$disk" --size ${size}b "$os" ' \
> + os=fedora-31
Cool, even if it is time-consuming setup :)
> @@ -96,6 +119,10 @@ Select the filesystem label. The default is not set.
>
> Specify the virtual size of the disk image.
>
> +If using C<command>, this is only a suggested size. The actual size
> +of the resulting disk will be the size of the disk created by
> +C<command>.
Ah, this addresses my comment above.
> +++ b/plugins/tmpdisk/tmpdisk.c
> +/* Shell variables. */
> +static struct var {
> + struct var *next;
> + const char *key, *value;
> +} *vars;
Linked-list, where...
> static int
> tmpdisk_config (const char *key, const char *value)
> {
> + /* Any other parameter will be forwarded to a shell variable. */
> else {
> - nbdkit_error ("unknown parameter '%s'", key);
> - return -1;
> + struct var *v_next = vars;
> +
> + vars = malloc (sizeof *vars);
> + if (vars == NULL) {
> + perror ("malloc");
> + exit (EXIT_FAILURE);
> + }
> + vars->next = v_next;
> + vars->key = key;
> + vars->value = value;
...the list is populated with later entries first, and...
> @@ -163,6 +191,7 @@ run_command (const char *disk)
> CLEANUP_FREE char *cmd = NULL;
> size_t len = 0;
> int r;
> + struct var *v;
>
> fp = open_memstream (&cmd, &len);
> if (fp == NULL) {
> @@ -173,21 +202,26 @@ run_command (const char *disk)
> /* Avoid stdin/stdout leaking (because of nbdkit -s). */
> fprintf (fp, "exec </dev/null >/dev/null\n");
>
> - /* Set the shell variables. */
> + /* Set the standard shell variables. */
> fprintf (fp, "disk=");
> shell_quote (disk, fp);
> putc ('\n', fp);
> - if (label) {
> - fprintf (fp, "label=");
> - shell_quote (label, fp);
> + fprintf (fp, "size=%" PRIi64 "\n", requested_size);
> + putc ('\n', fp);
> +
> + /* The other parameters/shell variables. */
> + for (v = vars; v != NULL; v = v->next) {
> + /* Keys probably can never contain shell-unsafe chars (because of
> + * nbdkit's own restrictions), but quoting it makes it safe.
> + */
> + shell_quote (v->key, fp);
You are correct that nbdkit itself prevents non-safe names from being
passed as a key to config. But if it did not, (for example, if '1=a'
were allowed through instead of an error), quoting the key would end up
trying to invoke a command rather than setting a shell variable. If
anything, I think it might be a bit cleaner to assert() that key is a
valid shell name, instead of trying to shell_quote() it. On the other
hand, I don't see it as a security bug: even if 'nbdkit tmpdisk size=...
command=... 1=a' sneaks through and caused the shell to attempt
execution of a command named '1=a', it's merely user error rather than a
CVE because nbdkit isn't running with any additional privileges compared
to the user just running '1=a' himself in the first place.
But it's all theoretical (shell_quote() is a no-op on safe names, and as
the comment says, we are currently guaranteed a safe name), so I can
live with what you have.
> + putc ('=', fp);
> + shell_quote (v->value, fp);
> putc ('\n', fp);
> }
...then processed front-to-back in passing to the shell. This means that:
nbdkit tmpdisk command=... a=1 a=2
results in command seeing $a as 1, which is a bit confusing (most
command lines allow later parameters to override earlier ones). Fixing
it would mean maintaining two pointers: the head of the list
(unchanging, used when reading the list), and the tail (updated with
each call to .config when building the list).
> @@ -228,36 +264,25 @@ tmpdisk_open (int readonly)
> goto error;
> }
> h->fd = -1;
> + h->size = -1;
> h->can_punch_hole = true;
>
> - /* Create the new disk image for this connection. */
> - if (asprintf (&disk, "%s/tmpdiskXXXXXX", tmpdir) == -1) {
> + /* For security reasons we have to create a temporary directory
> + * under tmpdir that only the current user can access. If we
> + * created it in a shared directory then another user might be able
> + * to see the temporary file being created and interfere with it
> + * before we reopen it in the plugin below.
> + */
> + if (asprintf (&dir, "%s/tmpdiskXXXXXX", tmpdir) == -1) {
> nbdkit_error ("asprintf: %m");
> goto error;
> }
> -
> -#ifdef HAVE_MKOSTEMP
> - h->fd = mkostemp (disk, O_CLOEXEC);
> -#else
> - /* Racy, fix your libc. */
> - h->fd = mkstemp (disk);
> - if (h->fd >= 0) {
> - h->fd = set_cloexec (h->fd);
> - if (h->fd == -1) {
> - int e = errno;
> - unlink (disk);
> - errno = e;
> - }
> - }
> -#endif
> - if (h->fd == -1) {
> - nbdkit_error ("mkstemp: %m");
> + if (mkdtemp (dir) == NULL) {
Hmm - even though we know Haiku still lacks mkostemp,
https://github.com/haiku/haiku/blob/master/src/system/libroot/posix/stdlib/mktemp.c
says that it at least has mkdtemp. Blindly using it for now is fine; if
we later need to add a fallback for whatever platform lacks it, we can
deal with it then. The use of mkdtemp is indeed safe.
> + nbdkit_error ("%s: %m", dir);
> goto error;
> }
> -
> - /* Truncate the disk to a sparse file of the right size. */
> - if (ftruncate (h->fd, size) == -1) {
> - nbdkit_error ("ftruncate: %s: %m", disk);
> + if (asprintf (&disk, "%s/disk", dir) == -1) {
> + nbdkit_error ("asprintf: %m");
> goto error;
> }
>
> @@ -265,11 +290,34 @@ tmpdisk_open (int readonly)
> if (run_command (disk) == -1)
> goto error;
>
> + /* The external command must have created the disk, and then we must
> + * find the true size.
> + */
> + if (readonly)
> + flags = O_RDONLY | O_CLOEXEC;
> + else
> + flags = O_RDWR | O_CLOEXEC | O_NOCTTY;
It looks odd to use O_NOCTTY for write but not read. O_NOCTTY is
important if we are worried that the user may have created a pty at
$disk (in which case, we do not want that pty becoming the controlling
terminal for nbdkit itself), but such a user is crazy ($command should
normally create a regular file). And if we really are worried about
O_NOCTTY, why are we not worried about O_NONBLOCK to avoid hanging if
the user creates a FIFO? Are we also worried enough about the user
trying to trick us into a symlink attack to use O_NOFOLLOW? I could
live with just 'O_RDRW | O_CLOEXEC'.
I suspect Haiku might need a followup for handling the fact that it
lacks O_CLOEXEC, but saving it for a separate patch is fine.
> + h->fd = open (disk, flags);
> + if (h->fd == -1) {
> + nbdkit_error ("open: %s: %m", disk);
> + goto error;
> + }
> +
> + if (fstat (h->fd, &statbuf) == -1) {
> + nbdkit_error ("fstat: %s: %m", disk);
> + goto error;
> + }
> +
> + h->size = statbuf.st_size;
Is this going to be the right size if command created a block device
(either directly at $disk via mknod, or more likely elsewhere with $disk
being a symlink to that alternate location)? Should we check fstat()
results for S_ISREG/S_ISBLK to decide whether we need to lseek() to
determine the actual size?
But as to your question about whether the security looks reasonable,
yes, I think your use of mkdtemp ensures that no one else is trampling
on the image between $command creating it and when we finally open() our fd.
Overall this looks like a sensible direction to head in.
--
Eric Blake, Principal Software Engineer
Red Hat, Inc. +1-919-301-3226
Virtualization: qemu.org | libvirt.org
More information about the Libguestfs
mailing list