Skip to content

Commit 01a8084

Browse files
committed
pathrs-lite: add libpathrs backend support
Signed-off-by: Aleksa Sarai <[email protected]>
1 parent 482ca30 commit 01a8084

File tree

11 files changed

+319
-6
lines changed

11 files changed

+319
-6
lines changed

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,34 @@ jobs:
2424
runs-on: ubuntu-latest
2525
steps:
2626
- uses: actions/checkout@v5
27+
28+
# We need libpathrs so that golangci-lint can typecheck
29+
# "cyphar.com/go-pathrs" (the package needs to be buildable).
30+
- uses: dtolnay/rust-toolchain@stable
31+
- name: find latest libpathrs release
32+
uses: actions/github-script@v8
33+
id: libpathrs-release-tarball
34+
with:
35+
result-encoding: string
36+
script: |-
37+
const latest_release = await github.rest.repos.getLatestRelease({
38+
owner: "cyphar",
39+
repo: "libpathrs",
40+
});
41+
console.log(latest_release);
42+
return latest_release.data.tarball_url;
43+
- name: install libpathrs
44+
run: |-
45+
mkdir -p /tmp/libpathrs
46+
cd /tmp/libpathrs
47+
48+
wget -O latest.tar.gz "${{ steps.libpathrs-release-tarball.outputs.result }}"
49+
tar xvf latest.tar.gz
50+
51+
cd *libpathrs-*/
52+
make release
53+
sudo ./install.sh --libdir=/usr/lib
54+
2755
- uses: actions/setup-go@v6
2856
with:
2957
go-version: '^1'

.golangci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
version: "2"
1111

12+
run:
13+
build-tags:
14+
- libpathrs
15+
1216
linters:
1317
enable:
1418
- asasalint

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111
`Reopen` wrappers have been removed. Please switch to using `pathrs-lite`
1212
directly.
1313

14+
### Added ###
15+
- `pathrs-lite` now has support for using libpathrs as a backend. This is
16+
opt-in and can be enabled at build time with the `libpathrs` build tag. The
17+
intention is to allow for downstream libraries and other projects to make use
18+
of the pure-Go `github.com/cyphar/filepath-securejoin/pathrs-lite` package
19+
and distributors can then opt-in to using `libpathrs` for the entire binary
20+
if they wish.
21+
1422
## [0.5.0] - 2025-09-26 ##
1523

1624
> Let the past die. Kill it if you have to.

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ module github.com/cyphar/filepath-securejoin
33
go 1.18
44

55
require (
6+
cyphar.com/go-pathrs v0.2.1-0.20251018101956-15cb2100ac4a
67
github.com/stretchr/testify v1.11.1
7-
golang.org/x/sys v0.18.0
8+
golang.org/x/sys v0.26.0
89
)
910

1011
require (

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
cyphar.com/go-pathrs v0.2.1-0.20251018101956-15cb2100ac4a h1:WHrOWFQrQ0eB7FedSqcJdAvlbE2gDxfPjecCtW6aJVU=
2+
cyphar.com/go-pathrs v0.2.1-0.20251018101956-15cb2100ac4a/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
13
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
24
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
46
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
57
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
68
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
7-
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
8-
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
9+
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
10+
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
911
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1012
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1113
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

pathrs-lite/mkdir_libpathrs.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
//go:build libpathrs
4+
5+
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
6+
// Copyright (C) 2024-2025 SUSE LLC
7+
//
8+
// This Source Code Form is subject to the terms of the Mozilla Public
9+
// License, v. 2.0. If a copy of the MPL was not distributed with this
10+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
12+
package pathrs
13+
14+
import (
15+
"os"
16+
17+
"cyphar.com/go-pathrs"
18+
)
19+
20+
// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
21+
// in two respects:
22+
//
23+
// - The caller provides the root directory as an *[os.File] (preferably O_PATH)
24+
// handle. This means that the caller can be sure which root directory is
25+
// being used. Note that this can be emulated by using /proc/self/fd/... as
26+
// the root path with [os.MkdirAll].
27+
//
28+
// - Once all of the directories have been created, an *[os.File] O_PATH handle
29+
// to the directory at unsafePath is returned to the caller. This is done in
30+
// an effectively-race-free way (an attacker would only be able to swap the
31+
// final directory component), which is not possible to emulate with
32+
// [MkdirAll].
33+
//
34+
// In addition, the returned handle is obtained far more efficiently than doing
35+
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
36+
// doing [MkdirAll]. If you intend to open the directory after creating it, you
37+
// should use MkdirAllHandle.
38+
//
39+
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
40+
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error) {
41+
rootRef, err := pathrs.RootFromFile(root)
42+
if err != nil {
43+
return nil, err
44+
}
45+
defer rootRef.Close() //nolint:errcheck // close failures aren't critical here
46+
47+
handle, err := rootRef.MkdirAll(unsafePath, mode)
48+
if err != nil {
49+
return nil, err
50+
}
51+
return handle.IntoFile(), nil
52+
}

pathrs-lite/mkdir_purego.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: MPL-2.0
22

3-
//go:build linux
3+
//go:build linux && !libpathrs
44

55
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
66
// Copyright (C) 2024-2025 SUSE LLC

pathrs-lite/open_libpathrs.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
//go:build libpathrs
4+
5+
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
6+
// Copyright (C) 2024-2025 SUSE LLC
7+
//
8+
// This Source Code Form is subject to the terms of the Mozilla Public
9+
// License, v. 2.0. If a copy of the MPL was not distributed with this
10+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
12+
package pathrs
13+
14+
import (
15+
"os"
16+
17+
"cyphar.com/go-pathrs"
18+
)
19+
20+
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
21+
// using an *[os.File] handle, to ensure that the correct root directory is used.
22+
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
23+
rootRef, err := pathrs.RootFromFile(root)
24+
if err != nil {
25+
return nil, err
26+
}
27+
defer rootRef.Close() //nolint:errcheck // close failures aren't critical here
28+
29+
handle, err := rootRef.Resolve(unsafePath)
30+
if err != nil {
31+
return nil, err
32+
}
33+
return handle.IntoFile(), nil
34+
}
35+
36+
// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
37+
// Reopen(file, flags) is effectively equivalent to
38+
//
39+
// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
40+
// os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
41+
//
42+
// But with some extra hardenings to ensure that we are not tricked by a
43+
// maliciously-configured /proc mount. While this attack scenario is not
44+
// common, in container runtimes it is possible for higher-level runtimes to be
45+
// tricked into configuring an unsafe /proc that can be used to attack file
46+
// operations. See [CVE-2019-19921] for more details.
47+
//
48+
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
49+
func Reopen(file *os.File, flags int) (*os.File, error) {
50+
handle, err := pathrs.HandleFromFile(file)
51+
if err != nil {
52+
return nil, err
53+
}
54+
defer handle.Close() //nolint:errcheck // close failures aren't critical here
55+
56+
return handle.OpenFile(flags)
57+
}

pathrs-lite/open_purego.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: MPL-2.0
22

3-
//go:build linux
3+
//go:build linux && !libpathrs
44

55
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
66
// Copyright (C) 2024-2025 SUSE LLC
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
3+
//go:build libpathrs
4+
5+
// Copyright (C) 2024-2025 Aleksa Sarai <[email protected]>
6+
// Copyright (C) 2024-2025 SUSE LLC
7+
//
8+
// This Source Code Form is subject to the terms of the Mozilla Public
9+
// License, v. 2.0. If a copy of the MPL was not distributed with this
10+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
12+
// Package procfs provides a safe API for operating on /proc on Linux.
13+
package procfs
14+
15+
import (
16+
"os"
17+
"strconv"
18+
19+
"cyphar.com/go-pathrs/procfs"
20+
"golang.org/x/sys/unix"
21+
)
22+
23+
// ProcThreadSelfCloser is a callback that needs to be called when you are done
24+
// operating on an [os.File] fetched using [Handle.OpenThreadSelf].
25+
//
26+
// [os.File]: https://pkg.go.dev/os#File
27+
type ProcThreadSelfCloser = procfs.ThreadCloser
28+
29+
// Handle is a wrapper around an *os.File handle to "/proc", which can be used
30+
// to do further procfs-related operations in a safe way.
31+
type Handle struct {
32+
inner *procfs.Handle
33+
}
34+
35+
// Close close the resources associated with this [Handle]. Note that if this
36+
// [Handle] was created with [OpenProcRoot], on some kernels the underlying
37+
// procfs handle is cached and so this Close operation may be a no-op. However,
38+
// you should always call Close on [Handle]s once you are done with them.
39+
func (proc *Handle) Close() error { return proc.inner.Close() }
40+
41+
// OpenProcRoot tries to open a "safer" handle to "/proc" (i.e., one with the
42+
// "subset=pid" mount option applied, available from Linux 5.8). Unless you
43+
// plan to do many [Handle.OpenRoot] operations, users should prefer to use
44+
// this over [OpenUnsafeProcRoot] which is far more dangerous to keep open.
45+
//
46+
// If a safe handle cannot be opened, OpenProcRoot will fall back to opening a
47+
// regular "/proc" handle.
48+
//
49+
// Note that using [Handle.OpenRoot] will still work with handles returned by
50+
// this function. If a subpath cannot be operated on with a safe "/proc"
51+
// handle, then [OpenUnsafeProcRoot] will be called internally and a temporary
52+
// unsafe handle will be used.
53+
func OpenProcRoot() (*Handle, error) {
54+
proc, err := procfs.Open()
55+
if err != nil {
56+
return nil, err
57+
}
58+
return &Handle{inner: proc}, nil
59+
}
60+
61+
// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or
62+
// masked paths. You must be extremely careful to make sure this handle is
63+
// never leaked to a container and that you program cannot be tricked into
64+
// writing to arbitrary paths within it.
65+
//
66+
// This is not necessary if you just wish to use [Handle.OpenRoot], as handles
67+
// returned by [OpenProcRoot] will fall back to using a *temporary* unsafe
68+
// handle in that case. You should only really use this if you need to do many
69+
// operations with [Handle.OpenRoot] and the performance overhead of making
70+
// many procfs handles is an issue. If you do use OpenUnsafeProcRoot, you
71+
// should make sure to close the handle as soon as possible to avoid
72+
// known-fd-number attacks.
73+
func OpenUnsafeProcRoot() (*Handle, error) {
74+
proc, err := procfs.Open(procfs.UnmaskedProcRoot)
75+
if err != nil {
76+
return nil, err
77+
}
78+
return &Handle{inner: proc}, nil
79+
}
80+
81+
// OpenThreadSelf returns a handle to "/proc/thread-self/<subpath>" (or an
82+
// equivalent handle on older kernels where "/proc/thread-self" doesn't exist).
83+
// Once finished with the handle, you must call the returned closer function
84+
// ([runtime.UnlockOSThread]). You must not pass the returned *os.File to other
85+
// Go threads or use the handle after calling the closer.
86+
//
87+
// [runtime.UnlockOSThread]: https://pkg.go.dev/runtime#UnlockOSThread
88+
func (proc *Handle) OpenThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) {
89+
return proc.inner.OpenThreadSelf(subpath, unix.O_PATH|unix.O_NOFOLLOW)
90+
}
91+
92+
// OpenSelf returns a handle to /proc/self/<subpath>.
93+
//
94+
// Note that in Go programs with non-homogenous threads, this may result in
95+
// spurious errors. If you are monkeying around with APIs that are
96+
// thread-specific, you probably want to use [Handle.OpenThreadSelf] instead
97+
// which will guarantee that the handle refers to the same thread as the caller
98+
// is executing on.
99+
func (proc *Handle) OpenSelf(subpath string) (*os.File, error) {
100+
return proc.inner.OpenSelf(subpath, unix.O_PATH|unix.O_NOFOLLOW)
101+
}
102+
103+
// OpenRoot returns a handle to /proc/<subpath>.
104+
//
105+
// You should only use this when you need to operate on global procfs files
106+
// (such as sysctls in /proc/sys). Unlike [Handle.OpenThreadSelf],
107+
// [Handle.OpenSelf], and [Handle.OpenPid], the procfs handle used internally
108+
// for this operation will never use "subset=pid", which makes it a more juicy
109+
// target for [CVE-2024-21626]-style attacks (and doing something like opening
110+
// a directory with OpenRoot effectively leaks [OpenUnsafeProcRoot] as long as
111+
// the file descriptor is open).
112+
//
113+
// [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
114+
func (proc *Handle) OpenRoot(subpath string) (*os.File, error) {
115+
return proc.inner.OpenRoot(subpath, unix.O_PATH|unix.O_NOFOLLOW)
116+
}
117+
118+
// OpenPid returns a handle to /proc/$pid/<subpath> (pid can be a pid or tid).
119+
// This is mainly intended for usage when operating on other processes.
120+
//
121+
// You should not use this for the current thread, as special handling is
122+
// needed for /proc/thread-self (or /proc/self/task/<tid>) when dealing with
123+
// goroutine scheduling -- use [Handle.OpenThreadSelf] instead.
124+
//
125+
// To refer to the current thread-group, you should use prefer
126+
// [Handle.OpenSelf] to passing os.Getpid as the pid argument.
127+
func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) {
128+
return proc.inner.OpenPid(pid, subpath, unix.O_PATH|unix.O_NOFOLLOW)
129+
}
130+
131+
// ProcSelfFdReadlink gets the real path of the given file by looking at
132+
// /proc/self/fd/<fd> with [readlink]. It is effectively just shorthand for
133+
// something along the lines of:
134+
//
135+
// proc, err := procfs.OpenProcRoot()
136+
// if err != nil {
137+
// return err
138+
// }
139+
// link, err := proc.OpenThreadSelf(fmt.Sprintf("fd/%d", f.Fd()))
140+
// if err != nil {
141+
// return err
142+
// }
143+
// defer link.Close()
144+
// var buf [4096]byte
145+
// n, err := unix.Readlinkat(int(link.Fd()), "", buf[:])
146+
// if err != nil {
147+
// return err
148+
// }
149+
// pathname := buf[:n]
150+
//
151+
// [readlink]: https://pkg.go.dev/golang.org/x/sys/unix#Readlinkat
152+
func ProcSelfFdReadlink(f *os.File) (string, error) {
153+
proc, err := procfs.Open()
154+
if err != nil {
155+
return "", err
156+
}
157+
defer proc.Close() //nolint:errcheck // close failures aren't critical here
158+
159+
fdPath := "fd/" + strconv.Itoa(int(f.Fd()))
160+
return proc.Readlink(procfs.ProcThreadSelf, fdPath)
161+
}

0 commit comments

Comments
 (0)