utils.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package utils
  2. import (
  3. "encoding/binary"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "strconv"
  10. "strings"
  11. "unsafe"
  12. securejoin "github.com/cyphar/filepath-securejoin"
  13. "golang.org/x/sys/unix"
  14. )
  15. const (
  16. exitSignalOffset = 128
  17. )
  18. // NativeEndian is the native byte order of the host system.
  19. var NativeEndian binary.ByteOrder
  20. func init() {
  21. // Copied from <golang.org/x/net/internal/socket/sys.go>.
  22. i := uint32(1)
  23. b := (*[4]byte)(unsafe.Pointer(&i))
  24. if b[0] == 1 {
  25. NativeEndian = binary.LittleEndian
  26. } else {
  27. NativeEndian = binary.BigEndian
  28. }
  29. }
  30. // ExitStatus returns the correct exit status for a process based on if it
  31. // was signaled or exited cleanly
  32. func ExitStatus(status unix.WaitStatus) int {
  33. if status.Signaled() {
  34. return exitSignalOffset + int(status.Signal())
  35. }
  36. return status.ExitStatus()
  37. }
  38. // WriteJSON writes the provided struct v to w using standard json marshaling
  39. func WriteJSON(w io.Writer, v interface{}) error {
  40. data, err := json.Marshal(v)
  41. if err != nil {
  42. return err
  43. }
  44. _, err = w.Write(data)
  45. return err
  46. }
  47. // CleanPath makes a path safe for use with filepath.Join. This is done by not
  48. // only cleaning the path, but also (if the path is relative) adding a leading
  49. // '/' and cleaning it (then removing the leading '/'). This ensures that a
  50. // path resulting from prepending another path will always resolve to lexically
  51. // be a subdirectory of the prefixed path. This is all done lexically, so paths
  52. // that include symlinks won't be safe as a result of using CleanPath.
  53. func CleanPath(path string) string {
  54. // Deal with empty strings nicely.
  55. if path == "" {
  56. return ""
  57. }
  58. // Ensure that all paths are cleaned (especially problematic ones like
  59. // "/../../../../../" which can cause lots of issues).
  60. path = filepath.Clean(path)
  61. // If the path isn't absolute, we need to do more processing to fix paths
  62. // such as "../../../../<etc>/some/path". We also shouldn't convert absolute
  63. // paths to relative ones.
  64. if !filepath.IsAbs(path) {
  65. path = filepath.Clean(string(os.PathSeparator) + path)
  66. // This can't fail, as (by definition) all paths are relative to root.
  67. path, _ = filepath.Rel(string(os.PathSeparator), path)
  68. }
  69. // Clean the path again for good measure.
  70. return filepath.Clean(path)
  71. }
  72. // stripRoot returns the passed path, stripping the root path if it was
  73. // (lexicially) inside it. Note that both passed paths will always be treated
  74. // as absolute, and the returned path will also always be absolute. In
  75. // addition, the paths are cleaned before stripping the root.
  76. func stripRoot(root, path string) string {
  77. // Make the paths clean and absolute.
  78. root, path = CleanPath("/"+root), CleanPath("/"+path)
  79. switch {
  80. case path == root:
  81. path = "/"
  82. case root == "/":
  83. // do nothing
  84. case strings.HasPrefix(path, root+"/"):
  85. path = strings.TrimPrefix(path, root+"/")
  86. }
  87. return CleanPath("/" + path)
  88. }
  89. // WithProcfd runs the passed closure with a procfd path (/proc/self/fd/...)
  90. // corresponding to the unsafePath resolved within the root. Before passing the
  91. // fd, this path is verified to have been inside the root -- so operating on it
  92. // through the passed fdpath should be safe. Do not access this path through
  93. // the original path strings, and do not attempt to use the pathname outside of
  94. // the passed closure (the file handle will be freed once the closure returns).
  95. func WithProcfd(root, unsafePath string, fn func(procfd string) error) error {
  96. // Remove the root then forcefully resolve inside the root.
  97. unsafePath = stripRoot(root, unsafePath)
  98. path, err := securejoin.SecureJoin(root, unsafePath)
  99. if err != nil {
  100. return fmt.Errorf("resolving path inside rootfs failed: %w", err)
  101. }
  102. // Open the target path.
  103. fh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
  104. if err != nil {
  105. return fmt.Errorf("open o_path procfd: %w", err)
  106. }
  107. defer fh.Close()
  108. // Double-check the path is the one we expected.
  109. procfd := "/proc/self/fd/" + strconv.Itoa(int(fh.Fd()))
  110. if realpath, err := os.Readlink(procfd); err != nil {
  111. return fmt.Errorf("procfd verification failed: %w", err)
  112. } else if realpath != path {
  113. return fmt.Errorf("possibly malicious path detected -- refusing to operate on %s", realpath)
  114. }
  115. // Run the closure.
  116. return fn(procfd)
  117. }
  118. // SearchLabels searches a list of key-value pairs for the provided key and
  119. // returns the corresponding value. The pairs must be separated with '='.
  120. func SearchLabels(labels []string, query string) string {
  121. for _, l := range labels {
  122. parts := strings.SplitN(l, "=", 2)
  123. if len(parts) < 2 {
  124. continue
  125. }
  126. if parts[0] == query {
  127. return parts[1]
  128. }
  129. }
  130. return ""
  131. }
  132. // Annotations returns the bundle path and user defined annotations from the
  133. // libcontainer state. We need to remove the bundle because that is a label
  134. // added by libcontainer.
  135. func Annotations(labels []string) (bundle string, userAnnotations map[string]string) {
  136. userAnnotations = make(map[string]string)
  137. for _, l := range labels {
  138. parts := strings.SplitN(l, "=", 2)
  139. if len(parts) < 2 {
  140. continue
  141. }
  142. if parts[0] == "bundle" {
  143. bundle = parts[1]
  144. } else {
  145. userAnnotations[parts[0]] = parts[1]
  146. }
  147. }
  148. return
  149. }