Tar – End of Tape Detection (ENOSPC) in Multi-Volume Mode with Pipes for Encryption

encryptionpipetapetar

Using tar in multi-volume mode relies on a ENOSPC error to detect the end of the first tape and prompt the user for the next tape.
To simulate this behaviour consider the following example by writing to /dev/full

tar -cvf - --multi-volume . > /dev/full

as expected results in

[...]
Prepare volume #2 for ‘-’ and hit return:

A problem arises when piping the output of tar through an encyption program like aespipe or gpg

tar -cvf - --multi-volume . | gpg -c --batch -q --passphrase 123 > /dev/full

which causes gpg to exit with code 2

gpg: [stdout]: write error: No space left on device
gpg: [stdout]: write error: No space left on device
gpg: filter_flush failed on close: No space left on device

The ENOSPC is obviously not propagated to tar, which isn't made aware of the specific errno. Is there a way to catch the error from gpg and "re-raise" the ENOSPC error to tar with a bash script?

For example, using tar with a named pipe results in a broken pipe once gpg fails and tar subsequently exists with SIGPIPE 141 — however ENOSPC still has to be signaled to tar in some way instead of the broken pipe error.

I would like to avoid the workaround of specifying a fixed tape size.
I am also aware of using mbuffer to handle tape spanning, which is undesireable because tapes can not be extracted individually.

EDIT: I just realized this is going to be a lot more complicated, as the data that has already left tar and was in the buffer when ENOSPC was encountered is most likely lost. Though most tape driver implementations allow another write operation after that, gpg and aespipe include no retry logic to save the data in the buffer.

EDIT 2: Further research shows that star on FreeBSD with the -compress-program option to perform the encryption in conjunction with -multivol and new-volume-script=... raises the error

star: Operation not permitted. Cannot lock fifo memory.
star: Can only compress files

when writing to a device instead of a file. So that's a dead end too.

Best Answer

It's not possible to propagate write errors back through a pipeline

And even if it were possible with some kind of hack, the pipes are buffering and by the time the pipe reader tries to "signal" the pipe writer, the latter could've already written the data which is causing the error further down the line, already got a successful status (>0) and updated its state accordingly. For it to work, the writing process would have to go back in time. On top of that, the pipe reader itself may do its own buffering and state keeping which would go out of sync.

The only way out is for tar to call the encryption routines directly, instead of passing the data through some kind of channel. Instead of modifying its source code and recompiling it, that could be done by monkey/live patching it with a LD_PRELOAD hack which overrides the write() library function and processes the data before passing it to the original write().

How to simulate ENOSPC with a LD_PRELOAD hack

This will cause an write to fd 1 (stdout) to fail with ENOSPC as soon as it tries to write more than 40960 bytes to it, after which it resets the counter and succeeds again, etc.

If you want it to work with tar -cf filename, instead of tar -cf -, you should probably change the fd == 1 test to fd != 2.

$ cat <<'EOT' >enospc.c
#define _GNU_SOURCE
#include <unistd.h>
#include <dlfcn.h>
#include <err.h>
#include <errno.h>

#define MAX     40960

ssize_t write(int fd, const void *b, size_t z){
        ssize_t w;
        static typeof (write) *o_write;
        static size_t count;
        if(!o_write) o_write = dlsym(RTLD_NEXT, "write");
        if(fd == 1 && count + z > MAX){
                count = 0;
                errno = ENOSPC;
                return -1;
        }
        w = o_write(fd, b, z);
        if(w > 0) count += w;
        return w;
}
EOT

$ cc -Wall -shared enospc.c -o enospc.so -ldl

$ seq -f 'n foo%04g.tar' 1 10000 |
  LD_PRELOAD=./enospc.so tar -M -cf- /etc/X11 > foo0000.tar
tar: Removing leading `/' from member names
Prepare volume #2 for ‘-’ and hit return: Prepare volume #3 for ‘/tmp/foo0001.tar’ and hit return: Prepare volume #4 for ‘/tmp/foo0002.tar’ and hit return: Prepare volume #5 for ‘/tmp/foo0003.tar’ and hit return: Prepare volume #6 for ‘/tmp/foo0004.tar’ and hit return: Prepare volume #7 for ‘/tmp/foo0005.tar’ and hit return: Prepare volume #8 for ‘/tmp/foo0006.tar’ and hit return: Prepare volume #9 for ‘/tmp/foo0007.tar’ and hit return: $

$ ls foo000*
foo0000.tar  foo0002.tar  foo0004.tar  foo0006.tar  foo0008.tar
foo0001.tar  foo0003.tar  foo0005.tar  foo0007.tar
Related Question