summaryrefslogtreecommitdiff
path: root/internal/io
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-10 18:03:29 +0300
committerPaul Buetow <paul@buetow.org>2026-04-10 18:03:29 +0300
commit28f6319b77d35c6da6b99ad7e35d0d5602dc2ee6 (patch)
tree687b2c38755a087694cacacb73cd73b8ef244ce7 /internal/io
parent13b21feb07c86f65760f7338f284f3b492364cd9 (diff)
Fix known-hosts trust deadlock, host key stat, and optional nozstd build
- stdout logger: release mutex while waiting on pause resume so prompt callbacks can log (fixes hang after trusting new hosts; known_hosts was written but Resume never ran). - known hosts callback: stop borrowing the SSH dial throttle channel (could block or interact badly with parallel handshakes). - host key path: use errors.Is(..., fs.ErrNotExist) for RootedPath.Stat wrapped errors; stat errors now fail fast instead of mis-read. - public key path: same ErrNotExist check for authorized_keys miss. - Build: optional DTAIL_NO_ZSTD=yes / nozstd tag for CGO-free builds; split zstd readers into tagged files. - Docs/examples: firewalld note for port 2222, log prune timer+script, SSHBindAddress note, dserver unit disabled-by-default comment; firewalld helper script example. - Regression test for stdout pause/mutex behavior. Made-with: Cursor
Diffstat (limited to 'internal/io')
-rw-r--r--internal/io/dlog/loggers/stdout.go9
-rw-r--r--internal/io/dlog/loggers/stdout_test.go36
-rw-r--r--internal/io/fs/readfile.go7
-rw-r--r--internal/io/fs/readfile_nozstd.go16
-rw-r--r--internal/io/fs/readfile_zstd.go20
5 files changed, 79 insertions, 9 deletions
diff --git a/internal/io/dlog/loggers/stdout.go b/internal/io/dlog/loggers/stdout.go
index a2575c8..b4e695a 100644
--- a/internal/io/dlog/loggers/stdout.go
+++ b/internal/io/dlog/loggers/stdout.go
@@ -44,14 +44,17 @@ func (s *stdout) RawWithColors(now time.Time, message, coloredMessage string) {
func (s *stdout) log(message string, nl bool) {
s.mutex.Lock()
- defer s.mutex.Unlock()
-
select {
case <-s.pauseCh:
- // Pause until resumed.
+ // Wait for Resume without holding the mutex: the prompt path calls
+ // dlog after the user answers while Pause is still active; holding the
+ // mutex here would deadlock (Info blocks on Lock, Resume never runs).
+ s.mutex.Unlock()
<-s.resumeCh
+ s.mutex.Lock()
default:
}
+ defer s.mutex.Unlock()
if nl {
fmt.Println(message)
diff --git a/internal/io/dlog/loggers/stdout_test.go b/internal/io/dlog/loggers/stdout_test.go
new file mode 100644
index 0000000..4f70efc
--- /dev/null
+++ b/internal/io/dlog/loggers/stdout_test.go
@@ -0,0 +1,36 @@
+package loggers
+
+import (
+ "testing"
+ "time"
+)
+
+// Regression: during an interactive prompt, dlog.Common.Pause() unblocks when some
+// goroutine hits stdout.log(); that goroutine must not hold the stdout mutex while
+// waiting on resume, or dlog.Client.Info from the prompt callback deadlocks forever.
+func TestStdoutSecondLogDuringPauseWaitDoesNotDeadlock(t *testing.T) {
+ s := newStdout()
+
+ go s.Pause()
+ time.Sleep(50 * time.Millisecond)
+
+ go func() {
+ s.Log(time.Now(), "first log consumes pause and waits on resume")
+ }()
+ time.Sleep(50 * time.Millisecond)
+
+ secondDone := make(chan struct{})
+ go func() {
+ s.Log(time.Now(), "second log must acquire mutex while first waits for Resume")
+ close(secondDone)
+ }()
+
+ select {
+ case <-secondDone:
+ case <-time.After(2 * time.Second):
+ t.Fatal("deadlock: second Log blocked on mutex while first waits for Resume")
+ }
+
+ s.Resume()
+ time.Sleep(50 * time.Millisecond)
+}
diff --git a/internal/io/fs/readfile.go b/internal/io/fs/readfile.go
index d305c4d..5241556 100644
--- a/internal/io/fs/readfile.go
+++ b/internal/io/fs/readfile.go
@@ -19,8 +19,6 @@ import (
"github.com/mimecast/dtail/internal/io/pool"
"github.com/mimecast/dtail/internal/lcontext"
"github.com/mimecast/dtail/internal/regex"
-
- "github.com/DataDog/zstd"
)
type readStatus int
@@ -193,10 +191,7 @@ func (f *readFile) makeCompressedFileReader(fd *os.File) (reader *bufio.Reader,
decompressor = gzipReader
reader = bufio.NewReader(gzipReader)
case strings.HasSuffix(f.FilePath(), ".zst"):
- dlog.Common.Info(f.FilePath(), "Detected zstd compression format")
- zstdReader := zstd.NewReader(fd)
- decompressor = zstdReader
- reader = bufio.NewReader(zstdReader)
+ return f.makeZstdReader(fd)
default:
reader = bufio.NewReader(fd)
}
diff --git a/internal/io/fs/readfile_nozstd.go b/internal/io/fs/readfile_nozstd.go
new file mode 100644
index 0000000..afd4523
--- /dev/null
+++ b/internal/io/fs/readfile_nozstd.go
@@ -0,0 +1,16 @@
+//go:build nozstd
+
+package fs
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+)
+
+func (f *readFile) makeZstdReader(fd *os.File) (reader *bufio.Reader, decompressor io.Closer, err error) {
+ _ = fd
+ err = fmt.Errorf("%s: zstd is not supported in this build (built with -tags nozstd)", f.FilePath())
+ return
+}
diff --git a/internal/io/fs/readfile_zstd.go b/internal/io/fs/readfile_zstd.go
new file mode 100644
index 0000000..a7e479b
--- /dev/null
+++ b/internal/io/fs/readfile_zstd.go
@@ -0,0 +1,20 @@
+//go:build !nozstd
+
+package fs
+
+import (
+ "bufio"
+ "io"
+ "os"
+
+ "github.com/DataDog/zstd"
+ "github.com/mimecast/dtail/internal/io/dlog"
+)
+
+func (f *readFile) makeZstdReader(fd *os.File) (reader *bufio.Reader, decompressor io.Closer, err error) {
+ dlog.Common.Info(f.FilePath(), "Detected zstd compression format")
+ zstdReader := zstd.NewReader(fd)
+ decompressor = zstdReader
+ reader = bufio.NewReader(zstdReader)
+ return
+}