You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

181 lines
4.6 KiB

  1. package os_test
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "syscall"
  10. "testing"
  11. "time"
  12. "github.com/stretchr/testify/require"
  13. tmos "github.com/tendermint/tendermint/libs/os"
  14. )
  15. func TestCopyFile(t *testing.T) {
  16. tmpfile, err := os.CreateTemp("", "example")
  17. if err != nil {
  18. t.Fatal(err)
  19. }
  20. defer os.Remove(tmpfile.Name())
  21. content := []byte("hello world")
  22. if _, err := tmpfile.Write(content); err != nil {
  23. t.Fatal(err)
  24. }
  25. copyfile := fmt.Sprintf("%s.copy", tmpfile.Name())
  26. if err := tmos.CopyFile(tmpfile.Name(), copyfile); err != nil {
  27. t.Fatal(err)
  28. }
  29. if _, err := os.Stat(copyfile); os.IsNotExist(err) {
  30. t.Fatal("copy should exist")
  31. }
  32. data, err := os.ReadFile(copyfile)
  33. if err != nil {
  34. t.Fatal(err)
  35. }
  36. if !bytes.Equal(data, content) {
  37. t.Fatalf("copy file content differs: expected %v, got %v", content, data)
  38. }
  39. os.Remove(copyfile)
  40. }
  41. func TestTrapSignal(t *testing.T) {
  42. if os.Getenv("TM_TRAP_SIGNAL_TEST") == "1" {
  43. t.Log("inside test process")
  44. killer()
  45. return
  46. }
  47. cmd, _, mockStderr := newTestProgram(t, "TM_TRAP_SIGNAL_TEST")
  48. err := cmd.Run()
  49. if err == nil {
  50. wantStderr := "exiting"
  51. if mockStderr.String() != wantStderr {
  52. t.Fatalf("stderr: want %q, got %q", wantStderr, mockStderr.String())
  53. }
  54. return
  55. }
  56. if e, ok := err.(*exec.ExitError); ok && !e.Success() {
  57. t.Fatalf("wrong exit code, want 0, got %d", e.ExitCode())
  58. }
  59. t.Fatal("this error should not be triggered")
  60. }
  61. func TestEnsureDir(t *testing.T) {
  62. tmp, err := os.MkdirTemp("", "ensure-dir")
  63. require.NoError(t, err)
  64. defer os.RemoveAll(tmp)
  65. // Should be possible to create a new directory.
  66. err = tmos.EnsureDir(filepath.Join(tmp, "dir"), 0755)
  67. require.NoError(t, err)
  68. require.DirExists(t, filepath.Join(tmp, "dir"))
  69. // Should succeed on existing directory.
  70. err = tmos.EnsureDir(filepath.Join(tmp, "dir"), 0755)
  71. require.NoError(t, err)
  72. // Should fail on file.
  73. err = os.WriteFile(filepath.Join(tmp, "file"), []byte{}, 0644)
  74. require.NoError(t, err)
  75. err = tmos.EnsureDir(filepath.Join(tmp, "file"), 0755)
  76. require.Error(t, err)
  77. // Should allow symlink to dir.
  78. err = os.Symlink(filepath.Join(tmp, "dir"), filepath.Join(tmp, "linkdir"))
  79. require.NoError(t, err)
  80. err = tmos.EnsureDir(filepath.Join(tmp, "linkdir"), 0755)
  81. require.NoError(t, err)
  82. // Should error on symlink to file.
  83. err = os.Symlink(filepath.Join(tmp, "file"), filepath.Join(tmp, "linkfile"))
  84. require.NoError(t, err)
  85. err = tmos.EnsureDir(filepath.Join(tmp, "linkfile"), 0755)
  86. require.Error(t, err)
  87. }
  88. type mockLogger struct{}
  89. func (ml mockLogger) Info(msg string, keyvals ...interface{}) {}
  90. func killer() {
  91. logger := mockLogger{}
  92. ctx, cancel := context.WithCancel(context.Background())
  93. defer cancel()
  94. tmos.TrapSignal(ctx, logger, func() { _, _ = fmt.Fprintf(os.Stderr, "exiting") })
  95. time.Sleep(1 * time.Second)
  96. p, err := os.FindProcess(os.Getpid())
  97. if err != nil {
  98. panic(err)
  99. }
  100. if err := p.Signal(syscall.SIGTERM); err != nil {
  101. panic(err)
  102. }
  103. time.Sleep(1 * time.Second)
  104. }
  105. func newTestProgram(t *testing.T, environVar string) (cmd *exec.Cmd, stdout *bytes.Buffer, stderr *bytes.Buffer) {
  106. t.Helper()
  107. cmd = exec.Command(os.Args[0], "-test.run="+t.Name())
  108. stdout, stderr = bytes.NewBufferString(""), bytes.NewBufferString("")
  109. cmd.Env = append(os.Environ(), fmt.Sprintf("%s=1", environVar))
  110. cmd.Stdout = stdout
  111. cmd.Stderr = stderr
  112. return
  113. }
  114. // Ensure that using CopyFile does not truncate the destination file before
  115. // the origin is positively a non-directory and that it is ready for copying.
  116. // See https://github.com/tendermint/tendermint/issues/6427
  117. func TestTrickedTruncation(t *testing.T) {
  118. tmpDir, err := os.MkdirTemp(os.TempDir(), "pwn_truncate")
  119. if err != nil {
  120. t.Fatal(err)
  121. }
  122. defer os.Remove(tmpDir)
  123. originalWALPath := filepath.Join(tmpDir, "wal")
  124. originalWALContent := []byte("I AM BECOME DEATH, DESTROYER OF ALL WORLDS!")
  125. if err := os.WriteFile(originalWALPath, originalWALContent, 0755); err != nil {
  126. t.Fatal(err)
  127. }
  128. // 1. Sanity check.
  129. readWAL, err := os.ReadFile(originalWALPath)
  130. if err != nil {
  131. t.Fatal(err)
  132. }
  133. if !bytes.Equal(readWAL, originalWALContent) {
  134. t.Fatalf("Cannot proceed as the content does not match\nGot: %q\nWant: %q", readWAL, originalWALContent)
  135. }
  136. // 2. Now cause the truncation of the original file.
  137. // It is absolutely legal to invoke os.Open on a directory.
  138. if err := tmos.CopyFile(tmpDir, originalWALPath); err == nil {
  139. t.Fatal("Expected an error")
  140. }
  141. // 3. Check the WAL's content
  142. reReadWAL, err := os.ReadFile(originalWALPath)
  143. if err != nil {
  144. t.Fatal(err)
  145. }
  146. if !bytes.Equal(reReadWAL, originalWALContent) {
  147. t.Fatalf("Oops, the WAL's content was changed :(\nGot: %q\nWant: %q", reReadWAL, originalWALContent)
  148. }
  149. }