chmod_linux.go 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. //go:build linux
  2. package localapi
  3. import (
  4. "fmt"
  5. "os"
  6. "strconv"
  7. "golang.org/x/sys/unix"
  8. )
  9. // applyAccess sets the owning group and the mode of the socket node at path, and of
  10. // nothing else. The node is opened O_PATH, which takes a reference to the inode without
  11. // opening the socket itself (a socket cannot be opened, and O_PATH does not try), and
  12. // O_NOFOLLOW, so a symlink at path is a refusal rather than a target. What the
  13. // descriptor holds is checked to be a socket of ours, and the chown and the chmod are
  14. // then made through /proc/self/fd, which names that inode whatever is at path by then:
  15. // a node swapped between the look and the change, for a link or for a hard link to a
  16. // file of somebody's choosing, is not what gets the group or the mode. This is how
  17. // glibc implements fchmodat(AT_SYMLINK_NOFOLLOW) too.
  18. //
  19. // Without /proc the change falls back to fchownat and fchmodat2, which do not follow a
  20. // link but are by path again, so a swap in the one syscall between the check and each
  21. // would be changed. Older than kernel 6.6 with no /proc, the mode is not set and the
  22. // socket is not served.
  23. func applyAccess(path string, acc Access) error {
  24. // The look is for the message: a link or a plain file is named as what it is, which
  25. // the O_PATH open's ELOOP or the fstat below would say less well.
  26. fi, err := os.Lstat(path)
  27. if err != nil {
  28. return err
  29. }
  30. if err := checkOwnSocket(path, fi); err != nil {
  31. return err
  32. }
  33. fd, err := unix.Open(path, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
  34. if err != nil {
  35. return &os.PathError{Op: "open", Path: path, Err: err}
  36. }
  37. defer unix.Close(fd)
  38. // What the descriptor holds is what the changes land on; the Lstat above looked at
  39. // the path, which may have changed since.
  40. var st unix.Stat_t
  41. if err := unix.Fstat(fd, &st); err != nil {
  42. return &os.PathError{Op: "fstat", Path: path, Err: err}
  43. }
  44. if st.Mode&unix.S_IFMT != unix.S_IFSOCK || st.Uid != currentUID() {
  45. return fmt.Errorf("%s was replaced while its access was being set, refusing to change it", path)
  46. }
  47. // The /proc name of the inode the descriptor holds. ENOENT from it means no /proc.
  48. held := "/proc/self/fd/" + strconv.Itoa(fd)
  49. // The group first: a chown by root clears the setgid bit, which a socket never has,
  50. // but the mode is pinned afterwards either way. With no group configured the socket
  51. // is handed back to the daemon's own, so taking the key out of the config on a reload
  52. // takes the access away as surely as a restart would.
  53. gid, err := gidFor(acc)
  54. if err != nil {
  55. return err
  56. }
  57. err = unix.Chown(held, -1, gid)
  58. if err == unix.ENOENT {
  59. err = unix.Fchownat(unix.AT_FDCWD, path, -1, gid, unix.AT_SYMLINK_NOFOLLOW)
  60. }
  61. if err != nil {
  62. return &os.PathError{Op: "chown", Path: path, Err: err}
  63. }
  64. perm := uint32(acc.Mode.Perm())
  65. err = unix.Chmod(held, perm)
  66. if err == unix.ENOENT {
  67. err = unix.Fchmodat(unix.AT_FDCWD, path, perm, unix.AT_SYMLINK_NOFOLLOW)
  68. }
  69. if err != nil {
  70. return &os.PathError{Op: "chmod", Path: path, Err: err}
  71. }
  72. return nil
  73. }