summaryrefslogtreecommitdiff
path: root/cmd/ioworkload/scenario_chown.go
blob: e171284f5eefd909e866161e908a5c89036ac378 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package main

import (
	"fmt"
	"path/filepath"
	"runtime"
	"syscall"
	"unsafe"
)

// chownBasic drives the chown ownership-change family end-to-end on a file and
// a symlink the caller owns. Every call passes owner/group -1/-1 ("don't
// change either id"), which the kernel accepts without CAP_CHOWN, so the whole
// scenario is UNPRIVILEGED and nothing is actually modified. It exercises, in
// order:
//
//   - chown(path, -1, -1)               — path at args[0], KindPathname, FamilyFS
//   - lchown(symlink, -1, -1)           — path at args[0], KindPathname, FamilyFS
//   - fchownat(AT_FDCWD, path, -1,-1,0) — path at args[1], KindPathname, FamilyFS
//   - fchown(fd, -1, -1)                — fd   at args[0], KindFd,       FamilyFS
//
// We use raw syscalls (rather than os.Chown / syscall.Chown etc.) so each
// distinct tracepoint actually fires; glibc/Go wrappers can redirect chown to
// fchownat and hide the syscall under test. The temp file and symlink are
// cleaned up afterwards.
func chownBasic() error {
	dir, cleanup, err := makeTempDir("chown-basic")
	if err != nil {
		return err
	}
	defer cleanup()

	path := filepath.Join(dir, "chownfile.txt")
	fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
	if err != nil {
		return fmt.Errorf("open: %w", err)
	}
	defer syscall.Close(fd)

	symlink := filepath.Join(dir, "chownlink")
	if err := syscall.Symlink(path, symlink); err != nil {
		return fmt.Errorf("symlink: %w", err)
	}

	pathBytes, err := syscall.BytePtrFromString(path)
	if err != nil {
		return fmt.Errorf("path bytes: %w", err)
	}
	symlinkBytes, err := syscall.BytePtrFromString(symlink)
	if err != nil {
		return fmt.Errorf("symlink bytes: %w", err)
	}

	if err := callChown(pathBytes); err != nil {
		return err
	}
	if err := callLchown(symlinkBytes); err != nil {
		return err
	}
	if err := callFchownat(pathBytes); err != nil {
		return err
	}
	if err := callFchown(fd); err != nil {
		return err
	}
	return nil
}

// callChown issues raw chown(path, -1, -1). chown takes the filesystem path at
// args[0], so ior captures it as a KindPathname event under enter_chown. Owner
// and group -1 mean "leave both unchanged", so no CAP_CHOWN is required.
func callChown(pathBytes *byte) error {
	_, _, errno := syscall.Syscall(
		syscall.SYS_CHOWN,
		uintptr(unsafe.Pointer(pathBytes)),
		uintptr(_UID_NOCHANGE),
		uintptr(_GID_NOCHANGE),
	)
	runtime.KeepAlive(pathBytes)
	if errno != 0 {
		return fmt.Errorf("chown: %w", errno)
	}
	return nil
}

// callLchown issues raw lchown(symlink, -1, -1). Like chown the path is at
// args[0] (KindPathname), but lchown acts on the symlink itself rather than its
// target. The -1/-1 owner/group keep it unprivileged.
func callLchown(symlinkBytes *byte) error {
	_, _, errno := syscall.Syscall(
		syscall.SYS_LCHOWN,
		uintptr(unsafe.Pointer(symlinkBytes)),
		uintptr(_UID_NOCHANGE),
		uintptr(_GID_NOCHANGE),
	)
	runtime.KeepAlive(symlinkBytes)
	if errno != 0 {
		return fmt.Errorf("lchown: %w", errno)
	}
	return nil
}

// callFchownat issues raw fchownat(AT_FDCWD, path, -1, -1, 0). The path is at
// args[1] (after the dirfd), so ior captures it as a KindPathname event under
// enter_fchownat. A runtime int holds AT_FDCWD so the negative value survives
// the uintptr conversion instead of overflowing.
func callFchownat(pathBytes *byte) error {
	dirfd := _AT_FDCWD
	_, _, errno := syscall.Syscall6(
		syscall.SYS_FCHOWNAT,
		uintptr(dirfd),
		uintptr(unsafe.Pointer(pathBytes)),
		uintptr(_UID_NOCHANGE),
		uintptr(_GID_NOCHANGE),
		0, // flags
		0,
	)
	runtime.KeepAlive(pathBytes)
	if errno != 0 {
		return fmt.Errorf("fchownat: %w", errno)
	}
	return nil
}

// callFchown issues raw fchown(fd, -1, -1). fchown operates on an open fd at
// args[0] (KindFd) and carries no path, so ior records enter_fchown keyed by
// the descriptor rather than a filename.
func callFchown(fd int) error {
	_, _, errno := syscall.Syscall(
		syscall.SYS_FCHOWN,
		uintptr(fd),
		uintptr(_UID_NOCHANGE),
		uintptr(_GID_NOCHANGE),
	)
	if errno != 0 {
		return fmt.Errorf("fchown: %w", errno)
	}
	return nil
}

// _UID_NOCHANGE and _GID_NOCHANGE are the (uid_t)-1 / (gid_t)-1 sentinels that
// tell the chown family "leave this id unchanged". Using them for both owner
// and group means the call performs no ownership change and therefore needs no
// CAP_CHOWN, keeping the scenario fully unprivileged. They are -1 cast to the
// unsigned 32-bit id types, i.e. 0xFFFFFFFF, which we form directly so the
// value passed through uintptr is the kernel's expected (uid_t)-1.
const (
	_UID_NOCHANGE uint32 = 0xFFFFFFFF
	_GID_NOCHANGE uint32 = 0xFFFFFFFF
)