Skip to content

Commit 987574e

Browse files
committed
merge #72 into cyphar/filepath-securejoin:main
Aleksa Sarai (2): ci: add test run for libpathrs pathrs-lite: add libpathrs backend support LGTMs: cyphar
2 parents 482ca30 + ff371f0 commit 987574e

File tree

12 files changed

+408
-7
lines changed

12 files changed

+408
-7
lines changed

.github/workflows/ci.yml

Lines changed: 111 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'
@@ -189,6 +217,88 @@ jobs:
189217
token: ${{ secrets.CODECOV_TOKEN }}
190218
slug: cyphar/filepath-securejoin
191219

220+
test-libpathrs:
221+
strategy:
222+
fail-fast: false
223+
matrix:
224+
os:
225+
- ubuntu-22.04
226+
- ubuntu-latest
227+
go-version:
228+
- "1.18"
229+
- "oldstable"
230+
- "stable"
231+
runs-on: ${{ matrix.os }}
232+
steps:
233+
- uses: actions/checkout@v5
234+
235+
- uses: dtolnay/rust-toolchain@stable
236+
- name: find latest libpathrs release
237+
uses: actions/github-script@v8
238+
id: libpathrs-release-tarball
239+
with:
240+
result-encoding: string
241+
script: |-
242+
const latest_release = await github.rest.repos.getLatestRelease({
243+
owner: "cyphar",
244+
repo: "libpathrs",
245+
});
246+
console.log(latest_release);
247+
return latest_release.data.tarball_url;
248+
- name: install libpathrs
249+
run: |-
250+
mkdir -p /tmp/libpathrs
251+
cd /tmp/libpathrs
252+
253+
wget -O latest.tar.gz "${{ steps.libpathrs-release-tarball.outputs.result }}"
254+
tar xvf latest.tar.gz
255+
256+
cd *libpathrs-*/
257+
make release
258+
sudo ./install.sh --libdir=/usr/lib
259+
260+
- uses: actions/setup-go@v6
261+
with:
262+
go-version: ${{ matrix.go-version }}
263+
check-latest: true
264+
- name: mkdir gocoverdir
265+
# We can only use -test.gocoverdir for Go >= 1.20.
266+
if: ${{ matrix.go-version != '1.18' && matrix.go-version != '1.19' }}
267+
run: |
268+
GOCOVERDIR="$(mktemp --tmpdir -d gocoverdir.XXXXXXXX)"
269+
echo "GOCOVERDIR=$GOCOVERDIR" >>"$GITHUB_ENV"
270+
- name: go test
271+
run: |-
272+
pkgs=("./pathrs-lite" "./pathrs-lite/procfs")
273+
if [ -n "${GOCOVERDIR:-}" ]; then
274+
go test -v -timeout=30m -cover -coverpkg=./... "${pkgs[@]}" -args -test.gocoverdir="$GOCOVERDIR"
275+
else
276+
go test -v -timeout=30m -cover -coverpkg=./... -coverprofile codecov-coverage.txt "${pkgs[@]}"
277+
fi
278+
- name: sudo go test
279+
run: |-
280+
pkgs=("./pathrs-lite" "./pathrs-lite/procfs")
281+
if [ -n "${GOCOVERDIR:-}" ]; then
282+
sudo go test -v -timeout=30m -cover -coverpkg=./... "${pkgs[@]}" -args -test.gocoverdir="$GOCOVERDIR"
283+
else
284+
sudo go test -v -timeout=30m -cover -coverpkg=./... -coverprofile codecov-coverage-sudo.txt "${pkgs[@]}"
285+
fi
286+
- name: upload coverage artefact
287+
# We can only use -test.gocoverdir for Go >= 1.20.
288+
if: ${{ matrix.go-version != '1.18' && matrix.go-version != '1.19' }}
289+
uses: actions/upload-artifact@v4
290+
with:
291+
name: coverage-${{ runner.os }}-${{ github.job }}-${{ strategy.job-index }}
292+
path: ${{ env.GOCOVERDIR }}
293+
- name: collate coverage data
294+
if: ${{ env.GOCOVERDIR != '' }}
295+
run: go tool covdata textfmt -i "$GOCOVERDIR" -o "codecov-coverage.txt"
296+
- name: upload coverage to codecov
297+
uses: codecov/codecov-action@v5
298+
with:
299+
token: ${{ secrets.CODECOV_TOKEN }}
300+
slug: cyphar/filepath-securejoin
301+
192302
coverage:
193303
runs-on: ubuntu-latest
194304
needs:
@@ -244,6 +354,7 @@ jobs:
244354
- test-build
245355
- test-windows
246356
- test-unix
357+
- test-libpathrs
247358
- coverage
248359
- codespell
249360
steps:

.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=

hack/test.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ GO="${GO:-go}"
1818
silent=
1919
verbose=
2020
long=
21-
while getopts "svL" opt; do
21+
libpathrs=
22+
while getopts "svLl" opt; do
2223
case "$opt" in
2324
s)
2425
silent=1
@@ -29,6 +30,9 @@ while getopts "svL" opt; do
2930
L)
3031
long=1
3132
;;
33+
l)
34+
libpathrs=1
35+
;;
3236
*)
3337
echo "$0 [-s(ilent)]"
3438
exit 1
@@ -41,6 +45,7 @@ trap 'rm -rf $gocoverdir' EXIT
4145
test_args=("-count=1" "-cover" "-coverpkg=./...")
4246
[ -n "$verbose" ] && test_args+=("-v")
4347
[ -z "$long" ] && test_args+=("-short")
48+
[ -n "$libpathrs" ] && test_args+=("-tags" "libpathrs")
4449

4550
"$GO" test "${test_args[@]}" ./... -args -test.gocoverdir="$gocoverdir"
4651
sudo "$GO" test "${test_args[@]}" ./... -args -test.gocoverdir="$gocoverdir"

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

0 commit comments

Comments
 (0)