diff --git a/secboot/keymgr/keymgr_luks2_test.go b/secboot/keymgr/keymgr_luks2_test.go index c5ca11d5afd..33c2d54281d 100644 --- a/secboot/keymgr/keymgr_luks2_test.go +++ b/secboot/keymgr/keymgr_luks2_test.go @@ -23,6 +23,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "testing" sb "github.com/snapcore/secboot" @@ -79,16 +80,12 @@ while [ "$#" -gt 1 ]; do cat > %s shift ;; - --key-file) - cat "$2" > %s - shift 2 - ;; *) shift ;; esac done -`, filepath.Join(s.rootDir, "new.key"), filepath.Join(s.rootDir, "unlock.key"))) +`, filepath.Join(s.rootDir, "cryptsetup.input"))) return cmd } @@ -96,12 +93,11 @@ func (s *keymgrSuite) verifyCryptsetupAddKey(c *C, cmd *testutil.MockCmd, unlock c.Assert(cmd, NotNil) calls := cmd.Calls() c.Assert(calls, HasLen, 2) - c.Assert(calls[0], HasLen, 16) - c.Assert(calls[0][5], testutil.Contains, s.rootDir) - calls[0][5] = "" + c.Assert(calls[0], HasLen, 19) c.Assert(calls[0], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(unlockKey)), + "--batch-mode", "--pbkdf", "argon2i", "--pbkdf-force-iterations", "4", "--pbkdf-memory", "202834", @@ -111,8 +107,8 @@ func (s *keymgrSuite) verifyCryptsetupAddKey(c *C, cmd *testutil.MockCmd, unlock c.Assert(calls[1], DeepEquals, []string{ "cryptsetup", "config", "--priority", "prefer", "--key-slot", "0", "/dev/foobar", }) - c.Check(filepath.Join(s.rootDir, "unlock.key"), testutil.FileEquals, unlockKey) - c.Check(filepath.Join(s.rootDir, "new.key"), testutil.FileEquals, newKey) + inputToCryptsetup := append(unlockKey, newKey...) + c.Check(filepath.Join(s.rootDir, "cryptsetup.input"), testutil.FileEquals, inputToCryptsetup) } func (s *keymgrSuite) TestAddRecoveryKeyToDeviceUnlockFromKeyring(c *C) { @@ -160,17 +156,6 @@ func (s *keymgrSuite) TestAddRecoveryKeyToDeviceCryptsetupFail(c *C) { defer restore() cmd := testutil.MockCommand(c, "cryptsetup", ` -while [ "$#" -gt 1 ]; do - case "$1" in - --key-file) - cat "$2" > /dev/null - shift 2 - ;; - *) - shift 1 - ;; - esac -done echo "Other error, cryptsetup boom" exit 1 `) @@ -194,17 +179,6 @@ func (s *keymgrSuite) TestAddRecoveryKeyToDeviceOccupiedSlot(c *C) { defer restore() cmd := testutil.MockCommand(c, "cryptsetup", ` -while [ "$#" -gt 1 ]; do - case "$1" in - --key-file) - cat "$2" > /dev/null - shift 2 - ;; - *) - shift 1 - ;; - esac -done echo "Key slot 1 is full, please select another one." >&2 exit 1 `) @@ -214,7 +188,7 @@ exit 1 c.Assert(getCalls, Equals, 1) calls := cmd.Calls() c.Assert(calls, HasLen, 1) - c.Assert(calls[0], HasLen, 16) + c.Assert(calls[0], HasLen, 19) c.Assert(calls[0][:2], DeepEquals, []string{"cryptsetup", "luksAddKey"}) // should match the keyslot full error too c.Assert(keymgr.IsKeyslotAlreadyUsed(err), Equals, true) @@ -366,13 +340,12 @@ done c.Assert(calls[0], DeepEquals, []string{ "cryptsetup", "luksKillSlot", "--type", "luks2", "--key-file", "-", "/dev/foobar", "2", }) - c.Assert(calls[1], HasLen, 14) - c.Assert(calls[1][5], testutil.Contains, dirs.RunDir) - calls[1][5] = "" + c.Assert(calls[1], HasLen, 17) // temporary key c.Assert(calls[1], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(unlockKey)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "2", @@ -418,13 +391,12 @@ fi c.Assert(calls[0], DeepEquals, []string{ "cryptsetup", "luksKillSlot", "--type", "luks2", "--key-file", "-", "/dev/foobar", "2", }) - c.Assert(calls[1], HasLen, 14) - c.Assert(calls[1][5], testutil.Contains, dirs.RunDir) - calls[1][5] = "" + c.Assert(calls[1], HasLen, 17) // temporary key c.Assert(calls[1], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(unlockKey)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "2", @@ -459,10 +431,6 @@ func (s *keymgrSuite) TestChangeEncryptionTempKeyFails(c *C) { cmd := testutil.MockCommand(c, "cryptsetup", ` while [ "$#" -gt 1 ]; do case "$1" in - --key-file) - cat "$2" > /dev/null - shift - ;; luksAddKey) add=1 ;; @@ -490,7 +458,7 @@ done func (s *keymgrSuite) TestTransitionEncryptionKeyExpectedHappy(c *C) { restore := keymgr.MockGetDiskUnlockKeyFromKernel(func(prefix, devicePath string, remove bool) (sb.DiskUnlockKey, error) { - c.Errorf("unepected call") + c.Errorf("unexpected call") return nil, fmt.Errorf("unexpected call") }) defer restore() @@ -532,13 +500,12 @@ fi calls := cmd.Calls() c.Assert(calls, HasLen, 5) // probing the key slot use - c.Assert(calls[0], HasLen, 14) - c.Assert(calls[0][5], testutil.Contains, dirs.RunDir) - calls[0][5] = "" + c.Assert(calls[0], HasLen, 17) // temporary key c.Assert(calls[0], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(key)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "2", @@ -549,12 +516,11 @@ fi "cryptsetup", "luksKillSlot", "--type", "luks2", "--key-file", "-", "/dev/foobar", "0", }) // adding the new encryption key - c.Assert(calls[2], HasLen, 14) - c.Assert(calls[2][5], testutil.Contains, dirs.RunDir) - calls[2][5] = "" + c.Assert(calls[2], HasLen, 17) c.Assert(calls[2], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(key)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "0", @@ -572,7 +538,7 @@ fi func (s *keymgrSuite) TestTransitionEncryptionKeyHappyKillSlotsInactive(c *C) { restore := keymgr.MockGetDiskUnlockKeyFromKernel(func(prefix, devicePath string, remove bool) (sb.DiskUnlockKey, error) { - c.Errorf("unepected call") + c.Errorf("unexpected call") return nil, fmt.Errorf("unexpected call") }) defer restore() @@ -621,7 +587,7 @@ fi calls := cmd.Calls() c.Assert(calls, HasLen, 5) // probing the key slot use - c.Assert(calls[0], HasLen, 14) + c.Assert(calls[0], HasLen, 17) // temporary key c.Assert(calls[0][:2], DeepEquals, []string{ "cryptsetup", "luksAddKey", @@ -631,12 +597,11 @@ fi "cryptsetup", "luksKillSlot", "--type", "luks2", "--key-file", "-", "/dev/foobar", "0", }) // adding the new encryption key - c.Assert(calls[2], HasLen, 14) - c.Assert(calls[2][5], testutil.Contains, dirs.RunDir) - calls[2][5] = "" + c.Assert(calls[2], HasLen, 17) c.Assert(calls[2], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(key)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "0", @@ -654,7 +619,7 @@ fi func (s *keymgrSuite) TestTransitionEncryptionKeyHappyOtherErrs(c *C) { restore := keymgr.MockGetDiskUnlockKeyFromKernel(func(prefix, devicePath string, remove bool) (sb.DiskUnlockKey, error) { - c.Errorf("unepected call") + c.Errorf("unexpected call") return nil, fmt.Errorf("unexpected call") }) defer restore() @@ -694,7 +659,7 @@ fi calls := cmd.Calls() c.Assert(calls, HasLen, 3) // probing the key slot use - c.Assert(calls[0], HasLen, 14) + c.Assert(calls[0], HasLen, 17) // temporary key c.Assert(calls[0][:2], DeepEquals, []string{ "cryptsetup", "luksAddKey", @@ -704,12 +669,11 @@ fi "cryptsetup", "luksKillSlot", "--type", "luks2", "--key-file", "-", "/dev/foobar", "0", }) // adding the new encryption key - c.Assert(calls[2], HasLen, 14) - c.Assert(calls[2][5], testutil.Contains, dirs.RunDir) - calls[2][5] = "" + c.Assert(calls[2], HasLen, 17) c.Assert(calls[2], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(key)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "0", @@ -722,7 +686,7 @@ func (s *keymgrSuite) TestTransitionEncryptionKeyCannotAddKeyNotStaged(c *C) { // staged restore := keymgr.MockGetDiskUnlockKeyFromKernel(func(prefix, devicePath string, remove bool) (sb.DiskUnlockKey, error) { - c.Errorf("unepected call") + c.Errorf("unexpected call") return nil, fmt.Errorf("unexpected call") }) defer restore() @@ -734,10 +698,6 @@ while [ "$#" -gt 1 ]; do keyadd=1 shift ;; - --key-file) - cat "$2" > /dev/null - shift 2 - ;; *) shift 1 ;; @@ -756,13 +716,12 @@ fi calls := cmd.Calls() c.Assert(calls, HasLen, 1) // probing the key slot use - c.Assert(calls[0], HasLen, 14) - c.Assert(calls[0][5], testutil.Contains, dirs.RunDir) - calls[0][5] = "" + c.Assert(calls[0], HasLen, 17) // temporary key c.Assert(calls[0], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(key)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "2", @@ -772,7 +731,7 @@ fi func (s *keymgrSuite) TestTransitionEncryptionKeyPostRebootHappy(c *C) { restore := keymgr.MockGetDiskUnlockKeyFromKernel(func(prefix, devicePath string, remove bool) (sb.DiskUnlockKey, error) { - c.Errorf("unepected call") + c.Errorf("unexpected call") return nil, fmt.Errorf("unexpected call") }) defer restore() @@ -797,14 +756,13 @@ done c.Assert(err, IsNil) calls := cmd.Calls() c.Assert(calls, HasLen, 2) - c.Assert(calls[0], HasLen, 14) - c.Assert(calls[0][5], testutil.Contains, dirs.RunDir) - calls[0][5] = "" + c.Assert(calls[0], HasLen, 17) // adding to a temporary key slot is successful, indicating a previously // successful transition c.Assert(calls[0], DeepEquals, []string{ "cryptsetup", "luksAddKey", "--type", "luks2", - "--key-file", "", + "--key-file", "-", "--keyfile-size", strconv.Itoa(len(key)), + "--batch-mode", "--pbkdf", "argon2i", "--iter-time", "100", "--key-slot", "2", @@ -820,7 +778,7 @@ func (s *keymgrSuite) TestTransitionEncryptionKeyPostRebootCannotKillSlot(c *C) // a post reboot scenario in which the luksKillSlot fails unexpectedly restore := keymgr.MockGetDiskUnlockKeyFromKernel(func(prefix, devicePath string, remove bool) (sb.DiskUnlockKey, error) { - c.Errorf("unepected call") + c.Errorf("unexpected call") return nil, fmt.Errorf("unexpected call") }) defer restore() @@ -853,7 +811,7 @@ fi c.Assert(err, ErrorMatches, "cannot kill temporary key slot: cryptsetup failed with: mock error") calls := cmd.Calls() c.Assert(calls, HasLen, 2) - c.Assert(calls[0], HasLen, 14) + c.Assert(calls[0], HasLen, 17) c.Assert(calls[0][:2], DeepEquals, []string{ "cryptsetup", "luksAddKey", }) diff --git a/secboot/luks2/cryptsetup.go b/secboot/luks2/cryptsetup.go index 0881cb2ca4f..122d16e191c 100644 --- a/secboot/luks2/cryptsetup.go +++ b/secboot/luks2/cryptsetup.go @@ -21,18 +21,13 @@ package luks2 import ( "bytes" - "errors" "fmt" "io" - "os" "os/exec" "strconv" - "syscall" "time" "github.com/snapcore/snapd/osutil" - - "golang.org/x/xerrors" ) const ( @@ -115,22 +110,6 @@ type AddKeyOptions struct { Slot int } -var writeExistingKeyToFifo = func(fifoPath string, existingKey []byte) error { - f, err := os.OpenFile(fifoPath, os.O_WRONLY, 0) - if err != nil { - return xerrors.Errorf("cannot open FIFO for passing existing key to cryptsetup: %w", err) - } - defer f.Close() - - if _, err := f.Write(existingKey); err != nil { - return xerrors.Errorf("cannot pass existing key to cryptsetup: %w", err) - } - if err := f.Close(); err != nil { - return xerrors.Errorf("cannot close write end of FIFO: %w", err) - } - return nil -} - // AddKey adds the supplied key in to a new keyslot for specified LUKS2 container. In order to do this, // an existing key must be provided. The KDF for the new keyslot will be configured to use argon2i with // the supplied benchmark time. The key will be added to the supplied slot. @@ -142,77 +121,40 @@ func AddKey(devicePath string, existingKey, key []byte, options *AddKeyOptions) options = &AddKeyOptions{Slot: AnySlot} } - fifoPath, cleanupFifo, err := mkFifo() - if err != nil { - return xerrors.Errorf("cannot create FIFO for passing existing key to cryptsetup: %w", err) - } - defer cleanupFifo() - args := []string{ // add a new key "luksAddKey", // LUKS2 only "--type", "luks2", - // read existing key from named pipe - "--key-file", fifoPath} + // read existing key from stdin, specifying key size so + // cryptsetup knows where the existing key ends and the new key + // starts (we are passing both keys via stdin). Otherwise it + // would interpret new lines as separator for the keys, while + // we actually allow '\n' to be part of the keys. + "--key-file", "-", + "--keyfile-size", strconv.Itoa(len(existingKey)), + // remove warnings and confirmation questions + "--batch-mode"} // apply KDF options args = options.KDFOptions.appendArguments(args) if options.Slot != AnySlot { + // TODO use --new-key-slot for newer crypsetup versions args = append(args, "--key-slot", strconv.Itoa(options.Slot)) } args = append(args, // container to add key to devicePath, - // read new key from stdin. - // Note that we can't supply the new key and existing key via the same channel - // because pipes and FIFOs aren't seekable - we would need to use an actual file - // in order to be able to do this. - "-") - - cmd := exec.Command("cryptsetup", args...) - cmd.Stdin = bytes.NewReader(key) - - // Writing to the fifo must happen in a go-routine as it may block - // if the other side is not connected. Special care must be taken - // about the cleanup. - fifoErrCh := make(chan error) - go func() { - fifoErr := writeExistingKeyToFifo(fifoPath, existingKey) - if fifoErr != nil { - // kill to ensure cmd is not lingering - if cmd.Process != nil { - cmd.Process.Kill() - } - // also ensure fifo is closed - cleanupFifo() - } - fifoErrCh <- fifoErr - }() - - output, cmdErr := cmd.CombinedOutput() - if cmdErr != nil { - // cleanupFifo will open/close the fifo to ensure the - // writeExistingKeyToFifo() does not leak while waiting - // for the other side of the fifo to connect (it may never - // do) - cleanupFifo() - } - fifoErr := <-fifoErrCh - - switch { - case cmdErr != nil && (fifoErr == nil || errors.Is(fifoErr, syscall.EPIPE)): - // cmdErr and EPIPE means the problem is with cmd, no - // need to display the EPIPE error - return fmt.Errorf("cryptsetup failed with: %v", osutil.OutputErr(output, cmdErr)) - case cmdErr != nil || fifoErr != nil: - // For all other cases show a generic error message - return fmt.Errorf("cryptsetup failed with: %v (fifo failed with: %v)", osutil.OutputErr(output, err), fifoErr) - } - - return nil + // we read raw bytes up to EOF (so new key can contain '\n': + // without the option it would be interpreted as end of key) + "-", + ) + + // existing and new key are both read from stdin + cmdInput := bytes.NewReader(append(existingKey, key...)) + return cryptsetupCmd(cmdInput, args...) } // KillSlot erases the keyslot with the supplied slot number from the specified LUKS2 container. diff --git a/secboot/luks2/cryptsetup_test.go b/secboot/luks2/cryptsetup_test.go index 2d32df273cb..802ceb9667c 100644 --- a/secboot/luks2/cryptsetup_test.go +++ b/secboot/luks2/cryptsetup_test.go @@ -23,6 +23,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "testing" . "gopkg.in/check.v1" @@ -68,8 +69,6 @@ func (s *luks2Suite) TestAddKeyHappy(c *C) { c.Assert(err, IsNil) mockCryptsetup := testutil.MockCommand(c, "cryptsetup", fmt.Sprintf(` -FIFO="$5" -cat "$FIFO" > %[1]s/fifo cat - > %[1]s/stdout 2>%[1]s/stderr `, s.tmpdir)) defer mockCryptsetup.Restore() @@ -77,13 +76,12 @@ cat - > %[1]s/stdout 2>%[1]s/stderr err = luks2.AddKey("/my/device", []byte("old-key"), []byte("new-key"), nil) c.Check(err, IsNil) c.Check(mockCryptsetup.Calls(), HasLen, 1) - fifoPath := mockCryptsetup.Calls()[0][5] + lenExisting := strconv.Itoa(len("old-key")) c.Check(mockCryptsetup.Calls(), DeepEquals, [][]string{ - {"cryptsetup", "luksAddKey", "--type", "luks2", "--key-file", fifoPath, "--pbkdf", "argon2i", "/my/device", "-"}, + {"cryptsetup", "luksAddKey", "--type", "luks2", "--key-file", "-", "--keyfile-size", lenExisting, "--batch-mode", "--pbkdf", "argon2i", "/my/device", "-"}, }) - c.Check(filepath.Join(s.tmpdir, "stdout"), testutil.FileEquals, "new-key") + c.Check(filepath.Join(s.tmpdir, "stdout"), testutil.FileEquals, "old-keynew-key") c.Check(filepath.Join(s.tmpdir, "stderr"), testutil.FileEquals, "") - c.Check(filepath.Join(s.tmpdir, "fifo"), testutil.FileEquals, "old-key") } func (s *luks2Suite) TestAddKeyBadCryptsetup(c *C) { @@ -96,23 +94,3 @@ func (s *luks2Suite) TestAddKeyBadCryptsetup(c *C) { err = luks2.AddKey("/my/device", []byte("old-key"), []byte("new-key"), nil) c.Check(err, ErrorMatches, "cryptsetup failed with: some-error") } - -func (s *luks2Suite) TestAddKeyBadWriteExistingKeyToFifo(c *C) { - err := os.MkdirAll(filepath.Join(s.tmpdir, "run"), 0755) - c.Assert(err, IsNil) - - mockCryptsetup := testutil.MockCommand(c, "cryptsetup", fmt.Sprintf(` -FIFO="$5" -cat "$FIFO" > %[1]s/fifo -cat - > %[1]s/stdout 2>%[1]s/stderr -`, s.tmpdir)) - defer mockCryptsetup.Restore() - - restore := luks2.MockWriteExistingKeyToFifo(func(string, []byte) error { - return fmt.Errorf("writeExistingKeyToFifo error") - }) - defer restore() - - err = luks2.AddKey("/my/device", []byte("old-key"), []byte("new-key"), nil) - c.Check(err, ErrorMatches, `cryptsetup failed with: .* \(fifo failed with: writeExistingKeyToFifo error\)`) -} diff --git a/secboot/luks2/export_test.go b/secboot/luks2/export_test.go deleted file mode 100644 index de391c8bcc0..00000000000 --- a/secboot/luks2/export_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2023 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package luks2 - -import ( - "github.com/snapcore/snapd/testutil" -) - -func MockWriteExistingKeyToFifo(f func(string, []byte) error) (restore func()) { - restore = testutil.Backup(&writeExistingKeyToFifo) - writeExistingKeyToFifo = f - return restore - -} diff --git a/secboot/luks2/fifo.go b/secboot/luks2/fifo.go deleted file mode 100644 index 7157b324537..00000000000 --- a/secboot/luks2/fifo.go +++ /dev/null @@ -1,72 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2022 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package luks2 - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "golang.org/x/sys/unix" - "golang.org/x/xerrors" - - "github.com/snapcore/snapd/dirs" -) - -func mkFifo() (string, func(), error) { - // /run is not world writable but we create a unique directory here because this - // code can be invoked by a public API and we shouldn't fail if more than one - // process reaches here at the same time. - dir, err := ioutil.TempDir(dirs.RunDir, filepath.Base(os.Args[0])+".") - if err != nil { - return "", nil, xerrors.Errorf("cannot create temporary directory: %w", err) - } - fifo := filepath.Join(dir, "fifo") - - cleanup := func() { - // Cleanup any pending readers/writers of the FIFO by - // opening/closing (O_RDRW will not block on - // linux). Otherwise we may leave file - // descriptors/go-routine behind that are stuck in opening - // one side of the fifo. - if f, err := os.OpenFile(fifo, os.O_RDWR, 0); err == nil { - f.Close() - } - if err := os.RemoveAll(dir); err != nil { - fmt.Fprintf(stderr, "luks2.mkFifo: cannot remove fifo: %v\n", err) - } - } - - succeeded := false - defer func() { - if succeeded { - return - } - cleanup() - }() - - if err := unix.Mkfifo(fifo, 0600); err != nil { - return "", nil, xerrors.Errorf("cannot create FIFO: %w", err) - } - - succeeded = true - return fifo, cleanup, nil -}