package os_test import ( "bytes" "context" "fmt" "os" "os/exec" "path/filepath" "syscall" "testing" "time" "github.com/stretchr/testify/require" tmos "github.com/tendermint/tendermint/libs/os" ) func TestCopyFile(t *testing.T) { tmpfile, err := os.CreateTemp("", "example") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) content := []byte("hello world") if _, err := tmpfile.Write(content); err != nil { t.Fatal(err) } copyfile := fmt.Sprintf("%s.copy", tmpfile.Name()) if err := tmos.CopyFile(tmpfile.Name(), copyfile); err != nil { t.Fatal(err) } if _, err := os.Stat(copyfile); os.IsNotExist(err) { t.Fatal("copy should exist") } data, err := os.ReadFile(copyfile) if err != nil { t.Fatal(err) } if !bytes.Equal(data, content) { t.Fatalf("copy file content differs: expected %v, got %v", content, data) } os.Remove(copyfile) } func TestTrapSignal(t *testing.T) { if os.Getenv("TM_TRAP_SIGNAL_TEST") == "1" { t.Log("inside test process") killer() return } cmd, _, mockStderr := newTestProgram(t, "TM_TRAP_SIGNAL_TEST") err := cmd.Run() if err == nil { wantStderr := "exiting" if mockStderr.String() != wantStderr { t.Fatalf("stderr: want %q, got %q", wantStderr, mockStderr.String()) } return } if e, ok := err.(*exec.ExitError); ok && !e.Success() { t.Fatalf("wrong exit code, want 0, got %d", e.ExitCode()) } t.Fatal("this error should not be triggered") } func TestEnsureDir(t *testing.T) { tmp, err := os.MkdirTemp("", "ensure-dir") require.NoError(t, err) defer os.RemoveAll(tmp) // Should be possible to create a new directory. err = tmos.EnsureDir(filepath.Join(tmp, "dir"), 0755) require.NoError(t, err) require.DirExists(t, filepath.Join(tmp, "dir")) // Should succeed on existing directory. err = tmos.EnsureDir(filepath.Join(tmp, "dir"), 0755) require.NoError(t, err) // Should fail on file. err = os.WriteFile(filepath.Join(tmp, "file"), []byte{}, 0644) require.NoError(t, err) err = tmos.EnsureDir(filepath.Join(tmp, "file"), 0755) require.Error(t, err) // Should allow symlink to dir. err = os.Symlink(filepath.Join(tmp, "dir"), filepath.Join(tmp, "linkdir")) require.NoError(t, err) err = tmos.EnsureDir(filepath.Join(tmp, "linkdir"), 0755) require.NoError(t, err) // Should error on symlink to file. err = os.Symlink(filepath.Join(tmp, "file"), filepath.Join(tmp, "linkfile")) require.NoError(t, err) err = tmos.EnsureDir(filepath.Join(tmp, "linkfile"), 0755) require.Error(t, err) } type mockLogger struct{} func (ml mockLogger) Info(msg string, keyvals ...interface{}) {} func killer() { logger := mockLogger{} ctx, cancel := context.WithCancel(context.Background()) defer cancel() tmos.TrapSignal(ctx, logger, func() { _, _ = fmt.Fprintf(os.Stderr, "exiting") }) time.Sleep(1 * time.Second) p, err := os.FindProcess(os.Getpid()) if err != nil { panic(err) } if err := p.Signal(syscall.SIGTERM); err != nil { panic(err) } time.Sleep(1 * time.Second) } func newTestProgram(t *testing.T, environVar string) (cmd *exec.Cmd, stdout *bytes.Buffer, stderr *bytes.Buffer) { t.Helper() cmd = exec.Command(os.Args[0], "-test.run="+t.Name()) stdout, stderr = bytes.NewBufferString(""), bytes.NewBufferString("") cmd.Env = append(os.Environ(), fmt.Sprintf("%s=1", environVar)) cmd.Stdout = stdout cmd.Stderr = stderr return } // Ensure that using CopyFile does not truncate the destination file before // the origin is positively a non-directory and that it is ready for copying. // See https://github.com/tendermint/tendermint/issues/6427 func TestTrickedTruncation(t *testing.T) { tmpDir, err := os.MkdirTemp(os.TempDir(), "pwn_truncate") if err != nil { t.Fatal(err) } defer os.Remove(tmpDir) originalWALPath := filepath.Join(tmpDir, "wal") originalWALContent := []byte("I AM BECOME DEATH, DESTROYER OF ALL WORLDS!") if err := os.WriteFile(originalWALPath, originalWALContent, 0755); err != nil { t.Fatal(err) } // 1. Sanity check. readWAL, err := os.ReadFile(originalWALPath) if err != nil { t.Fatal(err) } if !bytes.Equal(readWAL, originalWALContent) { t.Fatalf("Cannot proceed as the content does not match\nGot: %q\nWant: %q", readWAL, originalWALContent) } // 2. Now cause the truncation of the original file. // It is absolutely legal to invoke os.Open on a directory. if err := tmos.CopyFile(tmpDir, originalWALPath); err == nil { t.Fatal("Expected an error") } // 3. Check the WAL's content reReadWAL, err := os.ReadFile(originalWALPath) if err != nil { t.Fatal(err) } if !bytes.Equal(reReadWAL, originalWALContent) { t.Fatalf("Oops, the WAL's content was changed :(\nGot: %q\nWant: %q", reReadWAL, originalWALContent) } }