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.

177 lines
4.5 KiB

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