harden and platformize FIFO handling

Reject a FIFO path that points to an existing non-FIFO object.
Previously we removed whatever was at that path before creating the
pipe, so pointing -fifo at a regular file silently deleted it. An
existing FIFO is reused and only a missing path is created.

Move the implementation into fifo_unix.go and fifo_windows.go.
This lets the Unix build use syscall.Mkfifo directly, which is not
available on Windows.

Prevent the reader from endlessly spinning on a closed FIFO after an
error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon McGinty
2026-08-20 14:42:52 -04:00
co-authored by Claude Opus 5
parent c5baaae6a7
commit 4f41dd4ed6
4 changed files with 98 additions and 29 deletions
+55
View File
@@ -0,0 +1,55 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"syscall"
)
func setup_fifo(fn string) (chan string, error) {
commands := make(chan string)
if fn == "" {
return commands, nil
}
if info, err := os.Lstat(fn); err == nil {
if info.Mode()&os.ModeNamedPipe == 0 {
return commands, fmt.Errorf("FIFO path %q already exists and is not a FIFO", fn)
}
if err := os.Remove(fn); err != nil {
return commands, err
}
} else if !os.IsNotExist(err) {
return commands, err
}
if err := syscall.Mkfifo(fn, 0600); err != nil {
return commands, err
}
file, err := os.OpenFile(fn, os.O_RDWR, os.ModeNamedPipe)
if err != nil {
return commands, err
}
go readFIFO(file, commands)
return commands, nil
}
// readFIFO forwards complete commands and terminates on EOF or any read
// failure. Retrying an unrecoverable FIFO error used to spin a CPU forever.
func readFIFO(fh io.ReadCloser, out chan<- string) {
defer fh.Close()
defer close(out)
reader := bufio.NewReader(fh)
for {
line, err := reader.ReadBytes('\n')
if len(line) != 0 {
out <- strings.TrimSpace(string(line))
}
if err != nil {
return
}
}
}
+19
View File
@@ -0,0 +1,19 @@
//go:build windows
package main
import (
"fmt"
"strings"
)
// Windows named pipes require the Win32 API rather than POSIX filesystem
// FIFOs. Disable the legacy FIFO option until it is replaced with a named-pipe
// listener; ordinary UI and command-line operation remain available.
func setup_fifo(fn string) (chan string, error) {
commands := make(chan string)
if strings.TrimSpace(fn) == "" {
return commands, nil
}
return commands, fmt.Errorf("FIFO control is not supported on Windows")
}
-29
View File
@@ -3,17 +3,14 @@ package main
import _ "net/http/pprof"
import (
"al.essio.dev/pkg/shellescape"
"bufio"
"crypto/tls"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"strings"
"syscall"
"time"
barnlog "git.stormux.org/storm/barnard/log"
@@ -75,32 +72,6 @@ func setup_notify_runner(notify_command string) chan []string {
return t
}
func setup_fifo(fn string) (chan string, error) {
t := make(chan string)
if fn == "" {
return t, nil
}
os.Remove(fn)
err := syscall.Mkfifo(fn, 0600)
if err != nil {
return t, err
}
file, err := os.OpenFile(fn, os.O_RDWR, os.ModeNamedPipe)
if err != nil {
return t, err
}
go func(fh io.Reader, out chan string) {
reader := bufio.NewReader(fh)
for {
line, err := reader.ReadBytes('\n')
if err == nil {
out <- strings.TrimSpace(string(line))
}
}
}(file, t)
return t, nil
}
func main() {
// Command line flags
server := flag.String("server", "localhost:64738", "the server to connect to")
+24
View File
@@ -0,0 +1,24 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestSetupFIFORefusesToReplaceRegularFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "not-a-fifo")
if err := os.WriteFile(path, []byte("keep"), 0600); err != nil {
t.Fatal(err)
}
if _, err := setup_fifo(path); err == nil {
t.Fatal("setup_fifo replaced a regular file")
}
contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(contents) != "keep" {
t.Fatalf("regular file was modified: %q", contents)
}
}