File: root_linux.go

package info (click to toggle)
rust-pathrs 0.2.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,912 kB
  • sloc: python: 1,138; sh: 371; ansic: 259; makefile: 151
file content (367 lines) | stat: -rw-r--r-- 12,219 bytes parent folder | download | duplicates (2)
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
//go:build linux

// SPDX-License-Identifier: MPL-2.0
/*
 * libpathrs: safe path resolution on Linux
 * Copyright (C) 2019-2025 Aleksa Sarai <cyphar@cyphar.com>
 * Copyright (C) 2019-2025 SUSE LLC
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

package pathrs

import (
	"errors"
	"fmt"
	"os"
	"syscall"

	"cyphar.com/go-pathrs/internal/fdutils"
	"cyphar.com/go-pathrs/internal/libpathrs"
)

// Root is a handle to the root of a directory tree to resolve within. The only
// purpose of this "root handle" is to perform operations within the directory
// tree, or to get a [Handle] to inodes within the directory tree.
//
// At time of writing, it is considered a *VERY BAD IDEA* to open a [Root]
// inside a possibly-attacker-controlled directory tree. While we do have
// protections that should defend against it, it's far more dangerous than just
// opening a directory tree which is not inside a potentially-untrusted
// directory.
type Root struct {
	inner *os.File
}

// OpenRoot creates a new [Root] handle to the directory at the given path.
func OpenRoot(path string) (*Root, error) {
	fd, err := libpathrs.OpenRoot(path)
	if err != nil {
		return nil, err
	}
	file, err := fdutils.MkFile(fd)
	if err != nil {
		return nil, err
	}
	return &Root{inner: file}, nil
}

// RootFromFile creates a new [Root] handle from an [os.File] referencing a
// directory. The provided file will be duplicated, so the original file should
// still be closed by the caller.
//
// This is effectively the inverse operation of [Root.IntoFile].
//
// [os.File]: https://pkg.go.dev/os#File
func RootFromFile(file *os.File) (*Root, error) {
	newFile, err := fdutils.DupFile(file)
	if err != nil {
		return nil, fmt.Errorf("duplicate root fd: %w", err)
	}
	return &Root{inner: newFile}, nil
}

// Resolve resolves the given path within the [Root]'s directory tree, and
// returns a [Handle] to the resolved path. The path must already exist,
// otherwise an error will occur.
//
// All symlinks (including trailing symlinks) are followed, but they are
// resolved within the rootfs. If you wish to open a handle to the symlink
// itself, use [ResolveNoFollow].
func (r *Root) Resolve(path string) (*Handle, error) {
	return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) {
		handleFd, err := libpathrs.InRootResolve(rootFd, path)
		if err != nil {
			return nil, err
		}
		handleFile, err := fdutils.MkFile(handleFd)
		if err != nil {
			return nil, err
		}
		return &Handle{inner: handleFile}, nil
	})
}

// ResolveNoFollow is effectively an O_NOFOLLOW version of [Resolve]. Their
// behaviour is identical, except that *trailing* symlinks will not be
// followed. If the final component is a trailing symlink, an O_PATH|O_NOFOLLOW
// handle to the symlink itself is returned.
func (r *Root) ResolveNoFollow(path string) (*Handle, error) {
	return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) {
		handleFd, err := libpathrs.InRootResolveNoFollow(rootFd, path)
		if err != nil {
			return nil, err
		}
		handleFile, err := fdutils.MkFile(handleFd)
		if err != nil {
			return nil, err
		}
		return &Handle{inner: handleFile}, nil
	})
}

// Open is effectively shorthand for [Resolve] followed by [Handle.Open], but
// can be slightly more efficient (it reduces CGo overhead and the number of
// syscalls used when using the openat2-based resolver) and is arguably more
// ergonomic to use.
//
// This is effectively equivalent to [os.Open].
//
// [os.Open]: https://pkg.go.dev/os#Open
func (r *Root) Open(path string) (*os.File, error) {
	return r.OpenFile(path, os.O_RDONLY)
}

// OpenFile is effectively shorthand for [Resolve] followed by
// [Handle.OpenFile], but can be slightly more efficient (it reduces CGo
// overhead and the number of syscalls used when using the openat2-based
// resolver) and is arguably more ergonomic to use.
//
// However, if flags contains os.O_NOFOLLOW and the path is a symlink, then
// OpenFile's behaviour will match that of openat2. In most cases an error will
// be returned, but if os.O_PATH is provided along with os.O_NOFOLLOW then a
// file equivalent to [ResolveNoFollow] will be returned instead.
//
// This is effectively equivalent to [os.OpenFile], except that os.O_CREAT is
// not supported.
//
// [os.OpenFile]: https://pkg.go.dev/os#OpenFile
func (r *Root) OpenFile(path string, flags int) (*os.File, error) {
	return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) {
		fd, err := libpathrs.InRootOpen(rootFd, path, flags)
		if err != nil {
			return nil, err
		}
		return fdutils.MkFile(fd)
	})
}

// Create creates a file within the [Root]'s directory tree at the given path,
// and returns a handle to the file. The provided mode is used for the new file
// (the process's umask applies).
//
// Unlike [os.Create], if the file already exists an error is created rather
// than the file being opened and truncated.
//
// [os.Create]: https://pkg.go.dev/os#Create
func (r *Root) Create(path string, flags int, mode os.FileMode) (*os.File, error) {
	unixMode, err := toUnixMode(mode, false)
	if err != nil {
		return nil, err
	}
	return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) {
		handleFd, err := libpathrs.InRootCreat(rootFd, path, flags, unixMode)
		if err != nil {
			return nil, err
		}
		return fdutils.MkFile(handleFd)
	})
}

// Rename two paths within a [Root]'s directory tree. The flags argument is
// identical to the RENAME_* flags to the renameat2(2) system call.
func (r *Root) Rename(src, dst string, flags uint) error {
	_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootRename(rootFd, src, dst, flags)
		return struct{}{}, err
	})
	return err
}

// RemoveDir removes the named empty directory within a [Root]'s directory
// tree.
func (r *Root) RemoveDir(path string) error {
	_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootRmdir(rootFd, path)
		return struct{}{}, err
	})
	return err
}

// RemoveFile removes the named file within a [Root]'s directory tree.
func (r *Root) RemoveFile(path string) error {
	_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootUnlink(rootFd, path)
		return struct{}{}, err
	})
	return err
}

// Remove removes the named file or (empty) directory within a [Root]'s
// directory tree.
//
// This is effectively equivalent to [os.Remove].
//
// [os.Remove]: https://pkg.go.dev/os#Remove
func (r *Root) Remove(path string) error {
	// In order to match os.Remove's implementation we need to also do both
	// syscalls unconditionally and adjust the error based on whether
	// pathrs_inroot_rmdir() returned ENOTDIR.
	unlinkErr := r.RemoveFile(path)
	if unlinkErr == nil {
		return nil
	}
	rmdirErr := r.RemoveDir(path)
	if rmdirErr == nil {
		return nil
	}
	// Both failed, adjust the error in the same way that os.Remove does.
	err := rmdirErr
	if errors.Is(err, syscall.ENOTDIR) {
		err = unlinkErr
	}
	return err
}

// RemoveAll recursively deletes a path and all of its children.
//
// This is effectively equivalent to [os.RemoveAll].
//
// [os.RemoveAll]: https://pkg.go.dev/os#RemoveAll
func (r *Root) RemoveAll(path string) error {
	_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootRemoveAll(rootFd, path)
		return struct{}{}, err
	})
	return err
}

// Mkdir creates a directory within a [Root]'s directory tree. The provided
// mode is used for the new directory (the process's umask applies).
//
// This is effectively equivalent to [os.Mkdir].
//
// [os.Mkdir]: https://pkg.go.dev/os#Mkdir
func (r *Root) Mkdir(path string, mode os.FileMode) error {
	unixMode, err := toUnixMode(mode, false)
	if err != nil {
		return err
	}

	_, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootMkdir(rootFd, path, unixMode)
		return struct{}{}, err
	})
	return err
}

// MkdirAll creates a directory (and any parent path components if they don't
// exist) within a [Root]'s directory tree. The provided mode is used for any
// directories created by this function (the process's umask applies).
//
// This is effectively equivalent to [os.MkdirAll].
//
// [os.MkdirAll]: https://pkg.go.dev/os#MkdirAll
func (r *Root) MkdirAll(path string, mode os.FileMode) (*Handle, error) {
	unixMode, err := toUnixMode(mode, false)
	if err != nil {
		return nil, err
	}

	return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) {
		handleFd, err := libpathrs.InRootMkdirAll(rootFd, path, unixMode)
		if err != nil {
			return nil, err
		}
		handleFile, err := fdutils.MkFile(handleFd)
		if err != nil {
			return nil, err
		}
		return &Handle{inner: handleFile}, err
	})
}

// Mknod creates a new device inode of the given type within a [Root]'s
// directory tree. The provided mode is used for the new directory (the
// process's umask applies).
//
// This is effectively equivalent to [unix.Mknod].
//
// [unix.Mknod]: https://pkg.go.dev/golang.org/x/sys/unix#Mknod
func (r *Root) Mknod(path string, mode os.FileMode, dev uint64) error {
	unixMode, err := toUnixMode(mode, true)
	if err != nil {
		return err
	}

	_, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootMknod(rootFd, path, unixMode, dev)
		return struct{}{}, err
	})
	return err
}

// Symlink creates a symlink within a [Root]'s directory tree. The symlink is
// created at path and is a link to target.
//
// This is effectively equivalent to [os.Symlink].
//
// [os.Symlink]: https://pkg.go.dev/os#Symlink
func (r *Root) Symlink(path, target string) error {
	_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootSymlink(rootFd, path, target)
		return struct{}{}, err
	})
	return err
}

// Hardlink creates a hardlink within a [Root]'s directory tree. The hardlink
// is created at path and is a link to target. Both paths are within the
// [Root]'s directory tree (you cannot hardlink to a different [Root] or the
// host).
//
// This is effectively equivalent to [os.Link].
//
// [os.Link]: https://pkg.go.dev/os#Link
func (r *Root) Hardlink(path, target string) error {
	_, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) {
		err := libpathrs.InRootHardlink(rootFd, path, target)
		return struct{}{}, err
	})
	return err
}

// Readlink returns the target of a symlink with a [Root]'s directory tree.
//
// This is effectively equivalent to [os.Readlink].
//
// [os.Readlink]: https://pkg.go.dev/os#Readlink
func (r *Root) Readlink(path string) (string, error) {
	return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (string, error) {
		return libpathrs.InRootReadlink(rootFd, path)
	})
}

// IntoFile unwraps the [Root] into its underlying [os.File].
//
// It is critical that you do not operate on this file descriptor yourself,
// because the security properties of libpathrs depend on users doing all
// relevant filesystem operations through libpathrs.
//
// This operation returns the internal [os.File] of the [Root] directly, so
// calling [Root.Close] will also close any copies of the returned [os.File].
// If you want to get an independent copy, use [Root.Clone] followed by
// [Root.IntoFile] on the cloned [Root].
//
// [os.File]: https://pkg.go.dev/os#File
func (r *Root) IntoFile() *os.File {
	// TODO: Figure out if we really don't want to make a copy.
	// TODO: We almost certainly want to clear r.inner here, but we can't do
	//       that easily atomically (we could use atomic.Value but that'll make
	//       things quite a bit uglier).
	return r.inner
}

// Clone creates a copy of a [Root] handle, such that it has a separate
// lifetime to the original (while referring to the same underlying directory).
func (r *Root) Clone() (*Root, error) {
	return RootFromFile(r.inner)
}

// Close frees all of the resources used by the [Root] handle.
func (r *Root) Close() error {
	return r.inner.Close()
}