mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
deps: update to github.com/opencontainers/selinux@v0.13.0
This new version includes the fixes for CVE-2025-52881, so we can remove the internal/third_party copy of the library we added in commited6b1693b8("selinux: use safe procfs API for labels") as well as the "replace" directive in go.mod (which is problematic for "go get" installs). Fixes:ed6b1693b8("selinux: use safe procfs API for labels") Signed-off-by: Aleksa Sarai <cyphar@cyphar.com> (cherry picked from commit96f1962f91) Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
This commit is contained in:
@@ -6,7 +6,7 @@ require (
|
||||
github.com/checkpoint-restore/go-criu/v6 v6.3.0
|
||||
github.com/containerd/console v1.0.5
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/cyphar/filepath-securejoin v0.5.1
|
||||
github.com/cyphar/filepath-securejoin v0.6.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/moby/sys/capability v0.4.0
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/mrunalp/fileutils v0.5.1
|
||||
github.com/opencontainers/cgroups v0.0.4
|
||||
github.com/opencontainers/runtime-spec v1.2.1
|
||||
github.com/opencontainers/selinux v1.12.0
|
||||
github.com/opencontainers/selinux v1.13.0
|
||||
github.com/seccomp/libseccomp-golang v0.10.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/urfave/cli v1.22.16
|
||||
@@ -27,13 +27,9 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
cyphar.com/go-pathrs v0.2.1 // indirect
|
||||
github.com/cilium/ebpf v0.17.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
)
|
||||
|
||||
// FIXME: This is only intended as a short-term solution to include a patch for
|
||||
// CVE-2025-52881 in go-selinux without pushing the patches upstream. This
|
||||
// should be removed as soon as possible after the embargo is lifted.
|
||||
replace github.com/opencontainers/selinux => ./internal/third_party/selinux
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=
|
||||
cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/checkpoint-restore/go-criu/v6 v6.3.0 h1:mIdrSO2cPNWQY1truPg6uHLXyKHk3Z5Odx4wjKOASzA=
|
||||
github.com/checkpoint-restore/go-criu/v6 v6.3.0/go.mod h1:rrRTN/uSwY2X+BPRl/gkulo9gsKOSAeVp9/K2tv7xZI=
|
||||
@@ -10,8 +12,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
|
||||
github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -53,6 +55,8 @@ github.com/opencontainers/cgroups v0.0.4 h1:XVj8P/IHVms/j+7eh8ggdkTLAxjz84ZzuFyG
|
||||
github.com/opencontainers/cgroups v0.0.4/go.mod h1:s8lktyhlGUqM7OSRL5P7eAW6Wb+kWPNvt4qvVfzA5vs=
|
||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.13.0 h1:Zza88GWezyT7RLql12URvoxsbLfjFx988+LGaWfbL84=
|
||||
github.com/opencontainers/selinux v1.13.0/go.mod h1:XxWTed+A/s5NNq4GmYScVy+9jzXhGBVEOAyucdRUY8s=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
@@ -73,8 +77,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
[codespell]
|
||||
skip = ./.git,./go.sum,./go-selinux/testdata
|
||||
@@ -1,10 +0,0 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
# Dependencies listed in .github/workflows/*.yml
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -1,163 +0,0 @@
|
||||
name: validate
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
commit:
|
||||
runs-on: ubuntu-24.04
|
||||
# Only check commits on pull requests.
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: get pr commits
|
||||
id: 'get-pr-commits'
|
||||
uses: tim-actions/get-pr-commits@v1.3.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: check subject line length
|
||||
uses: tim-actions/commit-message-checker-with-regex@v0.3.2
|
||||
with:
|
||||
commits: ${{ steps.get-pr-commits.outputs.commits }}
|
||||
pattern: '^.{0,72}(\n.*)*$'
|
||||
error: 'Subject too long (max 72)'
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
- uses: golangci/golangci-lint-action@v7
|
||||
with:
|
||||
version: v2.0
|
||||
|
||||
codespell:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: install deps
|
||||
# Version of codespell bundled with Ubuntu is way old, so use pip.
|
||||
run: pip install codespell
|
||||
- name: run codespell
|
||||
run: codespell
|
||||
|
||||
cross:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: cross
|
||||
run: make build-cross
|
||||
|
||||
test-stubs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
- uses: golangci/golangci-lint-action@v7
|
||||
with:
|
||||
version: v2.0
|
||||
- name: test-stubs
|
||||
run: make test
|
||||
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.19.x, 1.23.x, 1.24.x]
|
||||
race: ["-race", ""]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: install go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: build
|
||||
run: make BUILDFLAGS="${{ matrix.race }}" build
|
||||
|
||||
- name: test
|
||||
run: make TESTFLAGS="${{ matrix.race }}" test
|
||||
|
||||
vm:
|
||||
name: "VM"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template:
|
||||
- template://almalinux-8
|
||||
- template://centos-stream-9
|
||||
- template://fedora
|
||||
- template://experimental/opensuse-tumbleweed
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: "Install Lima"
|
||||
uses: lima-vm/lima-actions/setup@v1
|
||||
id: lima-actions-setup
|
||||
|
||||
- name: "Cache ~/.cache/lima"
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/lima
|
||||
key: lima-${{ steps.lima-actions-setup.outputs.version }}-${{ matrix.template }}
|
||||
|
||||
- name: "Start VM"
|
||||
# --plain is set to disable file sharing, port forwarding, built-in containerd, etc. for faster start up
|
||||
run: limactl start --plain --name=default ${{ matrix.template }}
|
||||
|
||||
- name: "Initialize VM"
|
||||
run: |
|
||||
set -eux -o pipefail
|
||||
# Sync the current directory to /tmp/selinux in the guest
|
||||
limactl cp -r . default:/tmp/selinux
|
||||
# Install packages
|
||||
if lima command -v dnf >/dev/null; then
|
||||
lima sudo dnf install --setopt=install_weak_deps=false --setopt=tsflags=nodocs -y git-core make golang
|
||||
elif lima command -v zypper >/dev/null; then
|
||||
lima sudo zypper install -y git make go
|
||||
else
|
||||
echo >&2 "Unsupported distribution"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: "make test"
|
||||
continue-on-error: true
|
||||
run: lima make -C /tmp/selinux test
|
||||
|
||||
- name: "32-bit test"
|
||||
continue-on-error: true
|
||||
run: lima make -C /tmp/selinux GOARCH=386 test
|
||||
|
||||
# https://github.com/opencontainers/selinux/issues/222
|
||||
# https://github.com/opencontainers/selinux/issues/225
|
||||
- name: "racy test"
|
||||
continue-on-error: true
|
||||
run: lima bash -c 'cd /tmp/selinux && go test -timeout 10m -count 100000 ./go-selinux'
|
||||
|
||||
- name: "Show AVC denials"
|
||||
run: lima sudo ausearch -m AVC,USER_AVC || true
|
||||
|
||||
all-done:
|
||||
needs:
|
||||
- commit
|
||||
- lint
|
||||
- codespell
|
||||
- cross
|
||||
- test-stubs
|
||||
- test
|
||||
- vm
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- run: echo "All jobs completed"
|
||||
@@ -1 +0,0 @@
|
||||
build
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
version: "2"
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
|
||||
linters:
|
||||
enable:
|
||||
# - copyloopvar # Detects places where loop variables are copied. TODO enable for Go 1.22+
|
||||
- dupword # Detects duplicate words.
|
||||
- errorlint # Detects code that may cause problems with Go 1.13 error wrapping.
|
||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||
- gosec # Detects security problems.
|
||||
- misspell # Detects commonly misspelled English words in comments.
|
||||
- nilerr # Detects code that returns nil even if it checks that the error is not nil.
|
||||
- nolintlint # Detects ill-formed or insufficient nolint directives.
|
||||
- prealloc # Detects slice declarations that could potentially be pre-allocated.
|
||||
- predeclared # Detects code that shadows one of Go's predeclared identifiers
|
||||
- revive # Metalinter; drop-in replacement for golint.
|
||||
- thelper # Detects test helpers without t.Helper().
|
||||
- tparallel # Detects inappropriate usage of t.Parallel().
|
||||
- unconvert # Detects unnecessary type conversions.
|
||||
- usetesting # Reports uses of functions with replacement inside the testing package.
|
||||
settings:
|
||||
govet:
|
||||
enable-all: true
|
||||
settings:
|
||||
shadow:
|
||||
strict: true
|
||||
exclusions:
|
||||
generated: strict
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- govet
|
||||
text: '^shadow: declaration of "err" shadows declaration'
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
-1
@@ -1 +0,0 @@
|
||||
* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
## Contribution Guidelines
|
||||
|
||||
### Security issues
|
||||
|
||||
If you are reporting a security issue, do not create an issue or file a pull
|
||||
request on GitHub. Instead, disclose the issue responsibly by sending an email
|
||||
to security@opencontainers.org (which is inhabited only by the maintainers of
|
||||
the various OCI projects).
|
||||
|
||||
### Pull requests are always welcome
|
||||
|
||||
We are always thrilled to receive pull requests, and do our best to
|
||||
process them as fast as possible. Not sure if that typo is worth a pull
|
||||
request? Do it! We will appreciate it.
|
||||
|
||||
If your pull request is not accepted on the first try, don't be
|
||||
discouraged! If there's a problem with the implementation, hopefully you
|
||||
received feedback on what to improve.
|
||||
|
||||
We're trying very hard to keep the project lean and focused. We don't want it
|
||||
to do everything for everybody. This means that we might decide against
|
||||
incorporating a new feature.
|
||||
|
||||
|
||||
### Conventions
|
||||
|
||||
Fork the repo and make changes on your fork in a feature branch.
|
||||
For larger bugs and enhancements, consider filing a leader issue or mailing-list thread for discussion that is independent of the implementation.
|
||||
Small changes or changes that have been discussed on the project mailing list may be submitted without a leader issue.
|
||||
|
||||
If the project has a test suite, submit unit tests for your changes. Take a
|
||||
look at existing tests for inspiration. Run the full test suite on your branch
|
||||
before submitting a pull request.
|
||||
|
||||
Update the documentation when creating or modifying features. Test
|
||||
your documentation changes for clarity, concision, and correctness, as
|
||||
well as a clean documentation build. See ``docs/README.md`` for more
|
||||
information on building the docs and how docs get released.
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
committing your changes. Most editors have plugins that do this automatically.
|
||||
|
||||
Pull requests descriptions should be as clear as possible and include a
|
||||
reference to all the issues that they address.
|
||||
|
||||
Commit messages must start with a capitalized and short summary
|
||||
written in the imperative, followed by an optional, more detailed
|
||||
explanatory text which is separated from the summary by an empty line.
|
||||
|
||||
Code review comments may be added to your pull request. Discuss, then make the
|
||||
suggested modifications and push additional commits to your feature branch. Be
|
||||
sure to post a comment after pushing. The new commits will show up in the pull
|
||||
request automatically, but the reviewers will not be notified unless you
|
||||
comment.
|
||||
|
||||
Before the pull request is merged, make sure that you squash your commits into
|
||||
logical units of work using `git rebase -i` and `git push -f`. After every
|
||||
commit the test suite (if any) should be passing. Include documentation changes
|
||||
in the same commit so that a revert would remove all traces of the feature or
|
||||
fix.
|
||||
|
||||
Commits that fix or close an issue should include a reference like `Closes #XXX`
|
||||
or `Fixes #XXX`, which will automatically close the issue when merged.
|
||||
|
||||
### Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the
|
||||
patch, which certifies that you wrote it or otherwise have the right to
|
||||
pass it on as an open-source patch. The rules are pretty simple: if you
|
||||
can certify the below (from
|
||||
[developercertificate.org](http://developercertificate.org/)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Joe Smith <joe@gmail.com>
|
||||
|
||||
using your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
You can add the sign off when creating the git commit via `git commit -s`.
|
||||
Vendored
-201
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
Antonio Murdaca <runcom@redhat.com> (@runcom)
|
||||
Daniel J Walsh <dwalsh@redhat.com> (@rhatdan)
|
||||
Mrunal Patel <mpatel@redhat.com> (@mrunalp)
|
||||
Sebastiaan van Stijn <github@gone.nl> (@thaJeztah)
|
||||
Kirill Kolyshikin <kolyshkin@gmail.com> (@kolyshkin)
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
GO ?= go
|
||||
|
||||
all: build build-cross
|
||||
|
||||
define go-build
|
||||
GOOS=$(1) GOARCH=$(2) $(GO) build ${BUILDFLAGS} ./...
|
||||
endef
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
$(call go-build,linux,amd64)
|
||||
|
||||
.PHONY: build-cross
|
||||
build-cross:
|
||||
$(call go-build,linux,386)
|
||||
$(call go-build,linux,arm)
|
||||
$(call go-build,linux,arm64)
|
||||
$(call go-build,linux,ppc64le)
|
||||
$(call go-build,linux,s390x)
|
||||
$(call go-build,linux,mips64le)
|
||||
$(call go-build,linux,riscv64)
|
||||
$(call go-build,windows,amd64)
|
||||
$(call go-build,windows,386)
|
||||
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(GO) test -timeout 3m ${TESTFLAGS} -v ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
.PHONY: vendor
|
||||
vendor:
|
||||
$(GO) mod tidy
|
||||
$(GO) mod verify
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
# selinux
|
||||
|
||||
[](https://godoc.org/github.com/opencontainers/selinux) [](https://goreportcard.com/report/github.com/opencontainers/selinux) [](https://travis-ci.org/opencontainers/selinux)
|
||||
|
||||
Common SELinux package used across the container ecosystem.
|
||||
|
||||
## Usage
|
||||
|
||||
Prior to v1.8.0, the `selinux` build tag had to be used to enable selinux functionality for compiling consumers of this project.
|
||||
Starting with v1.8.0, the `selinux` build tag is no longer needed.
|
||||
|
||||
For complete documentation, see [godoc](https://godoc.org/github.com/opencontainers/selinux).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Participation in the OpenContainers community is governed by [OpenContainer's Code of Conduct][code-of-conduct].
|
||||
|
||||
## Security
|
||||
|
||||
If you find an issue, please follow the [security][security] protocol to report it.
|
||||
|
||||
[security]: https://github.com/opencontainers/org/blob/master/SECURITY.md
|
||||
[code-of-conduct]: https://github.com/opencontainers/org/blob/master/CODE_OF_CONDUCT.md
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
Package selinux provides a high-level interface for interacting with selinux.
|
||||
|
||||
Usage:
|
||||
|
||||
import "github.com/opencontainers/selinux/go-selinux"
|
||||
|
||||
// Ensure that selinux is enforcing mode.
|
||||
if selinux.EnforceMode() != selinux.Enforcing {
|
||||
selinux.SetEnforceMode(selinux.Enforcing)
|
||||
}
|
||||
*/
|
||||
package selinux
|
||||
@@ -1,48 +0,0 @@
|
||||
package label
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/opencontainers/selinux/go-selinux"
|
||||
)
|
||||
|
||||
// Init initialises the labeling system
|
||||
func Init() {
|
||||
_ = selinux.GetEnabled()
|
||||
}
|
||||
|
||||
// FormatMountLabel returns a string to be used by the mount command. Using
|
||||
// the SELinux `context` mount option. Changing labels of files on mount
|
||||
// points with this option can never be changed.
|
||||
// FormatMountLabel returns a string to be used by the mount command.
|
||||
// The format of this string will be used to alter the labeling of the mountpoint.
|
||||
// The string returned is suitable to be used as the options field of the mount command.
|
||||
// If you need to have additional mount point options, you can pass them in as
|
||||
// the first parameter. Second parameter is the label that you wish to apply
|
||||
// to all content in the mount point.
|
||||
func FormatMountLabel(src, mountLabel string) string {
|
||||
return FormatMountLabelByType(src, mountLabel, "context")
|
||||
}
|
||||
|
||||
// FormatMountLabelByType returns a string to be used by the mount command.
|
||||
// Allow caller to specify the mount options. For example using the SELinux
|
||||
// `fscontext` mount option would allow certain container processes to change
|
||||
// labels of files created on the mount points, where as `context` option does
|
||||
// not.
|
||||
// FormatMountLabelByType returns a string to be used by the mount command.
|
||||
// The format of this string will be used to alter the labeling of the mountpoint.
|
||||
// The string returned is suitable to be used as the options field of the mount command.
|
||||
// If you need to have additional mount point options, you can pass them in as
|
||||
// the first parameter. Second parameter is the label that you wish to apply
|
||||
// to all content in the mount point.
|
||||
func FormatMountLabelByType(src, mountLabel, contextType string) string {
|
||||
if mountLabel != "" {
|
||||
switch src {
|
||||
case "":
|
||||
src = fmt.Sprintf("%s=%q", contextType, mountLabel)
|
||||
default:
|
||||
src = fmt.Sprintf("%s,%s=%q", src, contextType, mountLabel)
|
||||
}
|
||||
}
|
||||
return src
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
package label
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/opencontainers/selinux/go-selinux"
|
||||
)
|
||||
|
||||
// Valid Label Options
|
||||
var validOptions = map[string]bool{
|
||||
"disable": true,
|
||||
"type": true,
|
||||
"filetype": true,
|
||||
"user": true,
|
||||
"role": true,
|
||||
"level": true,
|
||||
}
|
||||
|
||||
var ErrIncompatibleLabel = errors.New("bad SELinux option: z and Z can not be used together")
|
||||
|
||||
// InitLabels returns the process label and file labels to be used within
|
||||
// the container. A list of options can be passed into this function to alter
|
||||
// the labels. The labels returned will include a random MCS String, that is
|
||||
// guaranteed to be unique.
|
||||
// If the disabled flag is passed in, the process label will not be set, but the mount label will be set
|
||||
// to the container_file label with the maximum category. This label is not usable by any confined label.
|
||||
func InitLabels(options []string) (plabel string, mlabel string, retErr error) {
|
||||
if !selinux.GetEnabled() {
|
||||
return "", "", nil
|
||||
}
|
||||
processLabel, mountLabel := selinux.ContainerLabels()
|
||||
if processLabel != "" {
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
selinux.ReleaseLabel(mountLabel)
|
||||
}
|
||||
}()
|
||||
pcon, err := selinux.NewContext(processLabel)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
mcsLevel := pcon["level"]
|
||||
mcon, err := selinux.NewContext(mountLabel)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, opt := range options {
|
||||
if opt == "disable" {
|
||||
selinux.ReleaseLabel(mountLabel)
|
||||
return "", selinux.PrivContainerMountLabel(), nil
|
||||
}
|
||||
if i := strings.Index(opt, ":"); i == -1 {
|
||||
return "", "", fmt.Errorf("bad label option %q, valid options 'disable' or \n'user, role, level, type, filetype' followed by ':' and a value", opt)
|
||||
}
|
||||
con := strings.SplitN(opt, ":", 2)
|
||||
if !validOptions[con[0]] {
|
||||
return "", "", fmt.Errorf("bad label option %q, valid options 'disable, user, role, level, type, filetype'", con[0])
|
||||
}
|
||||
if con[0] == "filetype" {
|
||||
mcon["type"] = con[1]
|
||||
continue
|
||||
}
|
||||
pcon[con[0]] = con[1]
|
||||
if con[0] == "level" || con[0] == "user" {
|
||||
mcon[con[0]] = con[1]
|
||||
}
|
||||
}
|
||||
if pcon.Get() != processLabel {
|
||||
if pcon["level"] != mcsLevel {
|
||||
selinux.ReleaseLabel(processLabel)
|
||||
}
|
||||
processLabel = pcon.Get()
|
||||
selinux.ReserveLabel(processLabel)
|
||||
}
|
||||
mountLabel = mcon.Get()
|
||||
}
|
||||
return processLabel, mountLabel, nil
|
||||
}
|
||||
|
||||
// SetFileLabel modifies the "path" label to the specified file label
|
||||
func SetFileLabel(path string, fileLabel string) error {
|
||||
if !selinux.GetEnabled() || fileLabel == "" {
|
||||
return nil
|
||||
}
|
||||
return selinux.SetFileLabel(path, fileLabel)
|
||||
}
|
||||
|
||||
// SetFileCreateLabel tells the kernel the label for all files to be created
|
||||
func SetFileCreateLabel(fileLabel string) error {
|
||||
if !selinux.GetEnabled() {
|
||||
return nil
|
||||
}
|
||||
return selinux.SetFSCreateLabel(fileLabel)
|
||||
}
|
||||
|
||||
// Relabel changes the label of path and all the entries beneath the path.
|
||||
// It changes the MCS label to s0 if shared is true.
|
||||
// This will allow all containers to share the content.
|
||||
//
|
||||
// The path itself is guaranteed to be relabeled last.
|
||||
func Relabel(path string, fileLabel string, shared bool) error {
|
||||
if !selinux.GetEnabled() || fileLabel == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if shared {
|
||||
c, err := selinux.NewContext(fileLabel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c["level"] = "s0"
|
||||
fileLabel = c.Get()
|
||||
}
|
||||
return selinux.Chcon(path, fileLabel, true)
|
||||
}
|
||||
|
||||
// Validate checks that the label does not include unexpected options
|
||||
func Validate(label string) error {
|
||||
if strings.Contains(label, "z") && strings.Contains(label, "Z") {
|
||||
return ErrIncompatibleLabel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RelabelNeeded checks whether the user requested a relabel
|
||||
func RelabelNeeded(label string) bool {
|
||||
return strings.Contains(label, "z") || strings.Contains(label, "Z")
|
||||
}
|
||||
|
||||
// IsShared checks that the label includes a "shared" mark
|
||||
func IsShared(label string) bool {
|
||||
return strings.Contains(label, "z")
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package label
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/selinux/go-selinux"
|
||||
)
|
||||
|
||||
func needSELinux(t *testing.T) {
|
||||
t.Helper()
|
||||
if !selinux.GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
needSELinux(t)
|
||||
|
||||
var testNull []string
|
||||
_, _, err := InitLabels(testNull)
|
||||
if err != nil {
|
||||
t.Fatalf("InitLabels failed: %v:", err)
|
||||
}
|
||||
testDisabled := []string{"disable"}
|
||||
if selinux.ROFileLabel() == "" {
|
||||
t.Fatal("selinux.ROFileLabel: empty")
|
||||
}
|
||||
plabel, mlabel, err := InitLabels(testDisabled)
|
||||
if err != nil {
|
||||
t.Fatalf("InitLabels(disabled) failed: %v", err)
|
||||
}
|
||||
if plabel != "" {
|
||||
t.Fatalf("InitLabels(disabled): %q not empty", plabel)
|
||||
}
|
||||
if mlabel != "system_u:object_r:container_file_t:s0:c1022,c1023" {
|
||||
t.Fatalf("InitLabels Disabled mlabel Failed, %s", mlabel)
|
||||
}
|
||||
|
||||
testUser := []string{"user:user_u", "role:user_r", "type:user_t", "level:s0:c1,c15"}
|
||||
plabel, mlabel, err = InitLabels(testUser)
|
||||
if err != nil {
|
||||
t.Fatalf("InitLabels(user) failed: %v", err)
|
||||
}
|
||||
if plabel != "user_u:user_r:user_t:s0:c1,c15" || (mlabel != "user_u:object_r:container_file_t:s0:c1,c15" && mlabel != "user_u:object_r:svirt_sandbox_file_t:s0:c1,c15") {
|
||||
t.Fatalf("InitLabels(user) failed (plabel=%q, mlabel=%q)", plabel, mlabel)
|
||||
}
|
||||
|
||||
testBadData := []string{"user", "role:user_r", "type:user_t", "level:s0:c1,c15"}
|
||||
if _, _, err = InitLabels(testBadData); err == nil {
|
||||
t.Fatal("InitLabels(bad): expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelabel(t *testing.T) {
|
||||
needSELinux(t)
|
||||
|
||||
testdir := t.TempDir()
|
||||
label := "system_u:object_r:container_file_t:s0:c1,c2"
|
||||
if err := Relabel(testdir, "", true); err != nil {
|
||||
t.Fatalf("Relabel with no label failed: %v", err)
|
||||
}
|
||||
if err := Relabel(testdir, label, true); err != nil {
|
||||
t.Fatalf("Relabel shared failed: %v", err)
|
||||
}
|
||||
if err := Relabel(testdir, label, false); err != nil {
|
||||
t.Fatalf("Relabel unshared failed: %v", err)
|
||||
}
|
||||
if err := Relabel("/etc", label, false); err == nil {
|
||||
t.Fatalf("Relabel /etc succeeded")
|
||||
}
|
||||
if err := Relabel("/", label, false); err == nil {
|
||||
t.Fatalf("Relabel / succeeded")
|
||||
}
|
||||
if err := Relabel("/usr", label, false); err == nil {
|
||||
t.Fatalf("Relabel /usr succeeded")
|
||||
}
|
||||
if err := Relabel("/usr/", label, false); err == nil {
|
||||
t.Fatalf("Relabel /usr/ succeeded")
|
||||
}
|
||||
if err := Relabel("/etc/passwd", label, false); err == nil {
|
||||
t.Fatalf("Relabel /etc/passwd succeeded")
|
||||
}
|
||||
if home := os.Getenv("HOME"); home != "" {
|
||||
if err := Relabel(home, label, false); err == nil {
|
||||
t.Fatalf("Relabel %s succeeded", home)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
if err := Validate("zZ"); !errors.Is(err, ErrIncompatibleLabel) {
|
||||
t.Fatalf("Expected incompatible error, got %v", err)
|
||||
}
|
||||
if err := Validate("Z"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Validate("z"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Validate(""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsShared(t *testing.T) {
|
||||
if shared := IsShared("Z"); shared {
|
||||
t.Fatalf("Expected label `Z` to not be shared, got %v", shared)
|
||||
}
|
||||
if shared := IsShared("z"); !shared {
|
||||
t.Fatalf("Expected label `z` to be shared, got %v", shared)
|
||||
}
|
||||
if shared := IsShared("Zz"); !shared {
|
||||
t.Fatalf("Expected label `Zz` to be shared, got %v", shared)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLabel(t *testing.T) {
|
||||
needSELinux(t)
|
||||
|
||||
testUser := []string{"filetype:test_file_t", "level:s0:c1,c15"}
|
||||
_, mlabel, err := InitLabels(testUser)
|
||||
if err != nil {
|
||||
t.Fatalf("InitLabels(user) failed: %v", err)
|
||||
}
|
||||
if mlabel != "system_u:object_r:test_file_t:s0:c1,c15" {
|
||||
t.Fatalf("InitLabels(filetype) failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package label
|
||||
|
||||
// InitLabels returns the process label and file labels to be used within
|
||||
// the container. A list of options can be passed into this function to alter
|
||||
// the labels.
|
||||
func InitLabels([]string) (string, string, error) {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func SetFileLabel(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetFileCreateLabel(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Relabel(string, string, bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisableSecOpt returns a security opt that can disable labeling
|
||||
// support for future container processes
|
||||
func DisableSecOpt() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks that the label does not include unexpected options
|
||||
func Validate(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RelabelNeeded checks whether the user requested a relabel
|
||||
func RelabelNeeded(string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsShared checks that the label includes a "shared" mark
|
||||
func IsShared(string) bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package label
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/opencontainers/selinux/go-selinux"
|
||||
)
|
||||
|
||||
const testLabel = "system_u:object_r:container_file_t:s0:c1,c2"
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
var testNull []string
|
||||
_, _, err := InitLabels(testNull)
|
||||
if err != nil {
|
||||
t.Log("InitLabels Failed")
|
||||
t.Fatal(err)
|
||||
}
|
||||
testDisabled := []string{"disable"}
|
||||
if selinux.ROFileLabel() != "" {
|
||||
t.Error("selinux.ROFileLabel Failed")
|
||||
}
|
||||
plabel, mlabel, err := InitLabels(testDisabled)
|
||||
if err != nil {
|
||||
t.Log("InitLabels Disabled Failed")
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plabel != "" {
|
||||
t.Fatal("InitLabels Disabled Failed")
|
||||
}
|
||||
if mlabel != "" {
|
||||
t.Fatal("InitLabels Disabled mlabel Failed")
|
||||
}
|
||||
testUser := []string{"user:user_u", "role:user_r", "type:user_t", "level:s0:c1,c15"}
|
||||
_, _, err = InitLabels(testUser)
|
||||
if err != nil {
|
||||
t.Log("InitLabels User Failed")
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelabel(t *testing.T) {
|
||||
if err := Relabel("/etc", testLabel, false); err != nil {
|
||||
t.Fatalf("Relabel /etc succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLabelCompile(t *testing.T) {
|
||||
if _, _, err := InitLabels(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
if err := SetFileLabel(tmpDir, "foobar"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := SetFileCreateLabel("foobar"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
DisableSecOpt()
|
||||
|
||||
if err := Validate("foobar"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if relabel := RelabelNeeded("foobar"); relabel {
|
||||
t.Fatal("Relabel failed")
|
||||
}
|
||||
if shared := IsShared("foobar"); shared {
|
||||
t.Fatal("isshared failed")
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package label
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFormatMountLabel(t *testing.T) {
|
||||
expected := `context="foobar"`
|
||||
if test := FormatMountLabel("", "foobar"); test != expected {
|
||||
t.Fatalf("Format failed. Expected %s, got %s", expected, test)
|
||||
}
|
||||
|
||||
expected = `src,context="foobar"`
|
||||
if test := FormatMountLabel("src", "foobar"); test != expected {
|
||||
t.Fatalf("Format failed. Expected %s, got %s", expected, test)
|
||||
}
|
||||
|
||||
expected = `src`
|
||||
if test := FormatMountLabel("src", ""); test != expected {
|
||||
t.Fatalf("Format failed. Expected %s, got %s", expected, test)
|
||||
}
|
||||
|
||||
expected = `fscontext="foobar"`
|
||||
if test := FormatMountLabelByType("", "foobar", "fscontext"); test != expected {
|
||||
t.Fatalf("Format failed. Expected %s, got %s", expected, test)
|
||||
}
|
||||
|
||||
expected = `src,fscontext="foobar"`
|
||||
if test := FormatMountLabelByType("src", "foobar", "fscontext"); test != expected {
|
||||
t.Fatalf("Format failed. Expected %s, got %s", expected, test)
|
||||
}
|
||||
|
||||
expected = `src`
|
||||
if test := FormatMountLabelByType("src", "", "rootcontext"); test != expected {
|
||||
t.Fatalf("Format failed. Expected %s, got %s", expected, test)
|
||||
}
|
||||
}
|
||||
-322
@@ -1,322 +0,0 @@
|
||||
package selinux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// Enforcing constant indicate SELinux is in enforcing mode
|
||||
Enforcing = 1
|
||||
// Permissive constant to indicate SELinux is in permissive mode
|
||||
Permissive = 0
|
||||
// Disabled constant to indicate SELinux is disabled
|
||||
Disabled = -1
|
||||
// maxCategory is the maximum number of categories used within containers
|
||||
maxCategory = 1024
|
||||
// DefaultCategoryRange is the upper bound on the category range
|
||||
DefaultCategoryRange = uint32(maxCategory)
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMCSAlreadyExists is returned when trying to allocate a duplicate MCS.
|
||||
ErrMCSAlreadyExists = errors.New("MCS label already exists")
|
||||
// ErrEmptyPath is returned when an empty path has been specified.
|
||||
ErrEmptyPath = errors.New("empty path")
|
||||
|
||||
// ErrInvalidLabel is returned when an invalid label is specified.
|
||||
ErrInvalidLabel = errors.New("invalid Label")
|
||||
|
||||
// InvalidLabel is returned when an invalid label is specified.
|
||||
//
|
||||
// Deprecated: use [ErrInvalidLabel].
|
||||
InvalidLabel = ErrInvalidLabel
|
||||
|
||||
// ErrIncomparable is returned two levels are not comparable
|
||||
ErrIncomparable = errors.New("incomparable levels")
|
||||
// ErrLevelSyntax is returned when a sensitivity or category do not have correct syntax in a level
|
||||
ErrLevelSyntax = errors.New("invalid level syntax")
|
||||
|
||||
// ErrContextMissing is returned if a requested context is not found in a file.
|
||||
ErrContextMissing = errors.New("context does not have a match")
|
||||
// ErrVerifierNil is returned when a context verifier function is nil.
|
||||
ErrVerifierNil = errors.New("verifier function is nil")
|
||||
|
||||
// ErrNotTGLeader is returned by [SetKeyLabel] if the calling thread
|
||||
// is not the thread group leader.
|
||||
ErrNotTGLeader = errors.New("calling thread is not the thread group leader")
|
||||
|
||||
// CategoryRange allows the upper bound on the category range to be adjusted
|
||||
CategoryRange = DefaultCategoryRange
|
||||
|
||||
privContainerMountLabel string
|
||||
)
|
||||
|
||||
// Context is a representation of the SELinux label broken into 4 parts
|
||||
type Context map[string]string
|
||||
|
||||
// SetDisabled disables SELinux support for the package
|
||||
func SetDisabled() {
|
||||
setDisabled()
|
||||
}
|
||||
|
||||
// GetEnabled returns whether SELinux is currently enabled.
|
||||
func GetEnabled() bool {
|
||||
return getEnabled()
|
||||
}
|
||||
|
||||
// ClassIndex returns the int index for an object class in the loaded policy,
|
||||
// or -1 and an error
|
||||
func ClassIndex(class string) (int, error) {
|
||||
return classIndex(class)
|
||||
}
|
||||
|
||||
// SetFileLabel sets the SELinux label for this path, following symlinks,
|
||||
// or returns an error.
|
||||
func SetFileLabel(fpath string, label string) error {
|
||||
return setFileLabel(fpath, label)
|
||||
}
|
||||
|
||||
// LsetFileLabel sets the SELinux label for this path, not following symlinks,
|
||||
// or returns an error.
|
||||
func LsetFileLabel(fpath string, label string) error {
|
||||
return lSetFileLabel(fpath, label)
|
||||
}
|
||||
|
||||
// FileLabel returns the SELinux label for this path, following symlinks,
|
||||
// or returns an error.
|
||||
func FileLabel(fpath string) (string, error) {
|
||||
return fileLabel(fpath)
|
||||
}
|
||||
|
||||
// LfileLabel returns the SELinux label for this path, not following symlinks,
|
||||
// or returns an error.
|
||||
func LfileLabel(fpath string) (string, error) {
|
||||
return lFileLabel(fpath)
|
||||
}
|
||||
|
||||
// SetFSCreateLabel tells the kernel what label to use for all file system objects
|
||||
// created by this task.
|
||||
// Set the label to an empty string to return to the default label. Calls to SetFSCreateLabel
|
||||
// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until file system
|
||||
// objects created by this task are finished to guarantee another goroutine does not migrate
|
||||
// to the current thread before execution is complete.
|
||||
func SetFSCreateLabel(label string) error {
|
||||
return setFSCreateLabel(label)
|
||||
}
|
||||
|
||||
// FSCreateLabel returns the default label the kernel which the kernel is using
|
||||
// for file system objects created by this task. "" indicates default.
|
||||
func FSCreateLabel() (string, error) {
|
||||
return fsCreateLabel()
|
||||
}
|
||||
|
||||
// CurrentLabel returns the SELinux label of the current process thread, or an error.
|
||||
func CurrentLabel() (string, error) {
|
||||
return currentLabel()
|
||||
}
|
||||
|
||||
// PidLabel returns the SELinux label of the given pid, or an error.
|
||||
func PidLabel(pid int) (string, error) {
|
||||
return pidLabel(pid)
|
||||
}
|
||||
|
||||
// ExecLabel returns the SELinux label that the kernel will use for any programs
|
||||
// that are executed by the current process thread, or an error.
|
||||
func ExecLabel() (string, error) {
|
||||
return execLabel()
|
||||
}
|
||||
|
||||
// CanonicalizeContext takes a context string and writes it to the kernel
|
||||
// the function then returns the context that the kernel will use. Use this
|
||||
// function to check if two contexts are equivalent
|
||||
func CanonicalizeContext(val string) (string, error) {
|
||||
return canonicalizeContext(val)
|
||||
}
|
||||
|
||||
// ComputeCreateContext requests the type transition from source to target for
|
||||
// class from the kernel.
|
||||
func ComputeCreateContext(source string, target string, class string) (string, error) {
|
||||
return computeCreateContext(source, target, class)
|
||||
}
|
||||
|
||||
// CalculateGlbLub computes the glb (greatest lower bound) and lub (least upper bound)
|
||||
// of a source and target range.
|
||||
// The glblub is calculated as the greater of the low sensitivities and
|
||||
// the lower of the high sensitivities and the and of each category bitset.
|
||||
func CalculateGlbLub(sourceRange, targetRange string) (string, error) {
|
||||
return calculateGlbLub(sourceRange, targetRange)
|
||||
}
|
||||
|
||||
// SetExecLabel sets the SELinux label that the kernel will use for any programs
|
||||
// that are executed by the current process thread, or an error. Calls to SetExecLabel
|
||||
// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until execution
|
||||
// of the program is finished to guarantee another goroutine does not migrate to the current
|
||||
// thread before execution is complete.
|
||||
func SetExecLabel(label string) error {
|
||||
return writeConThreadSelf("attr/exec", label)
|
||||
}
|
||||
|
||||
// SetTaskLabel sets the SELinux label for the current thread, or an error.
|
||||
// This requires the dyntransition permission. Calls to SetTaskLabel should
|
||||
// be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() to guarantee
|
||||
// the current thread does not run in a new mislabeled thread.
|
||||
func SetTaskLabel(label string) error {
|
||||
return writeConThreadSelf("attr/current", label)
|
||||
}
|
||||
|
||||
// SetSocketLabel takes a process label and tells the kernel to assign the
|
||||
// label to the next socket that gets created. Calls to SetSocketLabel
|
||||
// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until
|
||||
// the socket is created to guarantee another goroutine does not migrate
|
||||
// to the current thread before execution is complete.
|
||||
func SetSocketLabel(label string) error {
|
||||
return writeConThreadSelf("attr/sockcreate", label)
|
||||
}
|
||||
|
||||
// SocketLabel retrieves the current socket label setting
|
||||
func SocketLabel() (string, error) {
|
||||
return readConThreadSelf("attr/sockcreate")
|
||||
}
|
||||
|
||||
// PeerLabel retrieves the label of the client on the other side of a socket
|
||||
func PeerLabel(fd uintptr) (string, error) {
|
||||
return peerLabel(fd)
|
||||
}
|
||||
|
||||
// SetKeyLabel takes a process label and tells the kernel to assign the
|
||||
// label to the next kernel keyring that gets created.
|
||||
//
|
||||
// Calls to SetKeyLabel should be wrapped in
|
||||
// runtime.LockOSThread()/runtime.UnlockOSThread() until the kernel keyring is
|
||||
// created to guarantee another goroutine does not migrate to the current
|
||||
// thread before execution is complete.
|
||||
//
|
||||
// Only the thread group leader can set key label.
|
||||
func SetKeyLabel(label string) error {
|
||||
return setKeyLabel(label)
|
||||
}
|
||||
|
||||
// KeyLabel retrieves the current kernel keyring label setting
|
||||
func KeyLabel() (string, error) {
|
||||
return keyLabel()
|
||||
}
|
||||
|
||||
// Get returns the Context as a string
|
||||
func (c Context) Get() string {
|
||||
return c.get()
|
||||
}
|
||||
|
||||
// NewContext creates a new Context struct from the specified label
|
||||
func NewContext(label string) (Context, error) {
|
||||
return newContext(label)
|
||||
}
|
||||
|
||||
// ClearLabels clears all reserved labels
|
||||
func ClearLabels() {
|
||||
clearLabels()
|
||||
}
|
||||
|
||||
// ReserveLabel reserves the MLS/MCS level component of the specified label
|
||||
func ReserveLabel(label string) {
|
||||
reserveLabel(label)
|
||||
}
|
||||
|
||||
// MLSEnabled checks if MLS is enabled.
|
||||
func MLSEnabled() bool {
|
||||
return isMLSEnabled()
|
||||
}
|
||||
|
||||
// EnforceMode returns the current SELinux mode Enforcing, Permissive, Disabled
|
||||
func EnforceMode() int {
|
||||
return enforceMode()
|
||||
}
|
||||
|
||||
// SetEnforceMode sets the current SELinux mode Enforcing, Permissive.
|
||||
// Disabled is not valid, since this needs to be set at boot time.
|
||||
func SetEnforceMode(mode int) error {
|
||||
return setEnforceMode(mode)
|
||||
}
|
||||
|
||||
// DefaultEnforceMode returns the systems default SELinux mode Enforcing,
|
||||
// Permissive or Disabled. Note this is just the default at boot time.
|
||||
// EnforceMode tells you the systems current mode.
|
||||
func DefaultEnforceMode() int {
|
||||
return defaultEnforceMode()
|
||||
}
|
||||
|
||||
// ReleaseLabel un-reserves the MLS/MCS Level field of the specified label,
|
||||
// allowing it to be used by another process.
|
||||
func ReleaseLabel(label string) {
|
||||
releaseLabel(label)
|
||||
}
|
||||
|
||||
// ROFileLabel returns the specified SELinux readonly file label
|
||||
func ROFileLabel() string {
|
||||
return roFileLabel()
|
||||
}
|
||||
|
||||
// KVMContainerLabels returns the default processLabel and mountLabel to be used
|
||||
// for kvm containers by the calling process.
|
||||
func KVMContainerLabels() (string, string) {
|
||||
return kvmContainerLabels()
|
||||
}
|
||||
|
||||
// InitContainerLabels returns the default processLabel and file labels to be
|
||||
// used for containers running an init system like systemd by the calling process.
|
||||
func InitContainerLabels() (string, string) {
|
||||
return initContainerLabels()
|
||||
}
|
||||
|
||||
// ContainerLabels returns an allocated processLabel and fileLabel to be used for
|
||||
// container labeling by the calling process.
|
||||
func ContainerLabels() (processLabel string, fileLabel string) {
|
||||
return containerLabels()
|
||||
}
|
||||
|
||||
// SecurityCheckContext validates that the SELinux label is understood by the kernel
|
||||
func SecurityCheckContext(val string) error {
|
||||
return securityCheckContext(val)
|
||||
}
|
||||
|
||||
// CopyLevel returns a label with the MLS/MCS level from src label replaced on
|
||||
// the dest label.
|
||||
func CopyLevel(src, dest string) (string, error) {
|
||||
return copyLevel(src, dest)
|
||||
}
|
||||
|
||||
// Chcon changes the fpath file object to the SELinux label.
|
||||
// If fpath is a directory and recurse is true, then Chcon walks the
|
||||
// directory tree setting the label.
|
||||
//
|
||||
// The fpath itself is guaranteed to be relabeled last.
|
||||
func Chcon(fpath string, label string, recurse bool) error {
|
||||
return chcon(fpath, label, recurse)
|
||||
}
|
||||
|
||||
// DupSecOpt takes an SELinux process label and returns security options that
|
||||
// can be used to set the SELinux Type and Level for future container processes.
|
||||
func DupSecOpt(src string) ([]string, error) {
|
||||
return dupSecOpt(src)
|
||||
}
|
||||
|
||||
// DisableSecOpt returns a security opt that can be used to disable SELinux
|
||||
// labeling support for future container processes.
|
||||
func DisableSecOpt() []string {
|
||||
return []string{"disable"}
|
||||
}
|
||||
|
||||
// GetDefaultContextWithLevel gets a single context for the specified SELinux user
|
||||
// identity that is reachable from the specified scon context. The context is based
|
||||
// on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/<username> if it exists,
|
||||
// and falls back to the global /etc/selinux/{SELINUXTYPE}/contexts/default_contexts
|
||||
// file.
|
||||
func GetDefaultContextWithLevel(user, level, scon string) (string, error) {
|
||||
return getDefaultContextWithLevel(user, level, scon)
|
||||
}
|
||||
|
||||
// PrivContainerMountLabel returns mount label for privileged containers
|
||||
func PrivContainerMountLabel() string {
|
||||
// Make sure label is initialized.
|
||||
_ = label("")
|
||||
return privContainerMountLabel
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,711 +0,0 @@
|
||||
package selinux
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestSetFileLabel(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
const (
|
||||
tmpFile = "selinux_test"
|
||||
tmpLink = "selinux_test_link"
|
||||
con = "system_u:object_r:bin_t:s0:c1,c2"
|
||||
con2 = "system_u:object_r:bin_t:s0:c3,c4"
|
||||
)
|
||||
|
||||
_ = os.Remove(tmpFile)
|
||||
out, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out.Close()
|
||||
defer os.Remove(tmpFile)
|
||||
|
||||
_ = os.Remove(tmpLink)
|
||||
if err := os.Symlink(tmpFile, tmpLink); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpLink)
|
||||
|
||||
if err := SetFileLabel(tmpLink, con); err != nil {
|
||||
t.Fatalf("SetFileLabel failed: %s", err)
|
||||
}
|
||||
filelabel, err := FileLabel(tmpLink)
|
||||
if err != nil {
|
||||
t.Fatalf("FileLabel failed: %s", err)
|
||||
}
|
||||
if filelabel != con {
|
||||
t.Fatalf("FileLabel failed, returned %s expected %s", filelabel, con)
|
||||
}
|
||||
|
||||
// Using LfileLabel to verify that the symlink itself is not labeled.
|
||||
linkLabel, err := LfileLabel(tmpLink)
|
||||
if err != nil {
|
||||
t.Fatalf("LfileLabel failed: %s", err)
|
||||
}
|
||||
if linkLabel == con {
|
||||
t.Fatalf("Label on symlink should not be set, got: %q", linkLabel)
|
||||
}
|
||||
|
||||
// Use LsetFileLabel to set a label on the symlink itself.
|
||||
if err := LsetFileLabel(tmpLink, con2); err != nil {
|
||||
t.Fatalf("LsetFileLabel failed: %s", err)
|
||||
}
|
||||
filelabel, err = FileLabel(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("FileLabel failed: %s", err)
|
||||
}
|
||||
if filelabel != con {
|
||||
t.Fatalf("FileLabel was updated, returned %s expected %s", filelabel, con)
|
||||
}
|
||||
|
||||
linkLabel, err = LfileLabel(tmpLink)
|
||||
if err != nil {
|
||||
t.Fatalf("LfileLabel failed: %s", err)
|
||||
}
|
||||
if linkLabel != con2 {
|
||||
t.Fatalf("LfileLabel failed: returned %s expected %s", linkLabel, con2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVMLabels(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
plabel, flabel := KVMContainerLabels()
|
||||
if plabel == "" {
|
||||
t.Log("Failed to read kvm label")
|
||||
}
|
||||
t.Log(plabel)
|
||||
t.Log(flabel)
|
||||
if _, err := CanonicalizeContext(plabel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := CanonicalizeContext(flabel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ReleaseLabel(plabel)
|
||||
}
|
||||
|
||||
func TestInitLabels(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
plabel, flabel := InitContainerLabels()
|
||||
if plabel == "" {
|
||||
t.Log("Failed to read init label")
|
||||
}
|
||||
t.Log(plabel)
|
||||
t.Log(flabel)
|
||||
if _, err := CanonicalizeContext(plabel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := CanonicalizeContext(flabel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ReleaseLabel(plabel)
|
||||
}
|
||||
|
||||
func TestDuplicateLabel(t *testing.T) {
|
||||
secopt, err := DupSecOpt("system_u:system_r:container_t:s0:c1,c2")
|
||||
if err != nil {
|
||||
t.Fatalf("DupSecOpt: %v", err)
|
||||
}
|
||||
for _, opt := range secopt {
|
||||
con := strings.SplitN(opt, ":", 2)
|
||||
if con[0] == "user" {
|
||||
if con[1] != "system_u" {
|
||||
t.Errorf("DupSecOpt Failed user incorrect")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if con[0] == "role" {
|
||||
if con[1] != "system_r" {
|
||||
t.Errorf("DupSecOpt Failed role incorrect")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if con[0] == "type" {
|
||||
if con[1] != "container_t" {
|
||||
t.Errorf("DupSecOpt Failed type incorrect")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if con[0] == "level" {
|
||||
if con[1] != "s0:c1,c2" {
|
||||
t.Errorf("DupSecOpt Failed level incorrect")
|
||||
}
|
||||
continue
|
||||
}
|
||||
t.Errorf("DupSecOpt failed: invalid field %q", con[0])
|
||||
}
|
||||
secopt = DisableSecOpt()
|
||||
if secopt[0] != "disable" {
|
||||
t.Errorf(`DisableSecOpt failed: want "disable", got %q`, secopt[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSELinuxNoLevel(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
tlabel := "system_u:system_r:container_t"
|
||||
dup, err := DupSecOpt(tlabel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(dup) != 3 {
|
||||
t.Errorf("DupSecOpt failed on non mls label: want 3, got %d", len(dup))
|
||||
}
|
||||
con, err := NewContext(tlabel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if con.Get() != tlabel {
|
||||
t.Errorf("NewContext and con.Get() failed on non mls label: want %q, got %q", tlabel, con.Get())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocketLabel(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
// Ensure the thread stays the same for duration of the test.
|
||||
// Otherwise Go runtime can switch this to a different thread,
|
||||
// which results in EACCES in call to SetSocketLabel.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
label := "system_u:object_r:container_t:s0:c1,c2"
|
||||
if err := SetSocketLabel(label); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nlabel, err := SocketLabel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if label != nlabel {
|
||||
t.Errorf("SocketLabel %s != %s", nlabel, label)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyLabel(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
// Ensure the thread stays the same for duration of the test.
|
||||
// Otherwise Go runtime can switch this to a different thread,
|
||||
// which results in EACCES in call to SetKeyLabel.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
if unix.Getpid() != unix.Gettid() {
|
||||
t.Skip(ErrNotTGLeader)
|
||||
}
|
||||
|
||||
label := "system_u:object_r:container_t:s0:c1,c2"
|
||||
if err := SetKeyLabel(label); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nlabel, err := KeyLabel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if label != nlabel {
|
||||
t.Errorf("KeyLabel: want %q, got %q", label, nlabel)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContextGet(b *testing.B) {
|
||||
ctx, err := NewContext("system_u:object_r:container_file_t:s0:c1022,c1023")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
str := ""
|
||||
for i := 0; i < b.N; i++ {
|
||||
str = ctx.get()
|
||||
}
|
||||
b.Log(str)
|
||||
}
|
||||
|
||||
func TestSELinux(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
// Ensure the thread stays the same for duration of the test.
|
||||
// Otherwise Go runtime can switch this to a different thread,
|
||||
// which results in EACCES in call to SetFSCreateLabel.
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
var (
|
||||
err error
|
||||
plabel, flabel string
|
||||
)
|
||||
|
||||
plabel, flabel = ContainerLabels()
|
||||
t.Log(plabel)
|
||||
t.Log(flabel)
|
||||
plabel, flabel = ContainerLabels()
|
||||
t.Log(plabel)
|
||||
t.Log(flabel)
|
||||
ReleaseLabel(plabel)
|
||||
|
||||
plabel, flabel = ContainerLabels()
|
||||
t.Log(plabel)
|
||||
t.Log(flabel)
|
||||
ClearLabels()
|
||||
t.Log("ClearLabels")
|
||||
plabel, flabel = ContainerLabels()
|
||||
t.Log(plabel)
|
||||
t.Log(flabel)
|
||||
ReleaseLabel(plabel)
|
||||
|
||||
pid := os.Getpid()
|
||||
t.Logf("PID:%d MCS:%s", pid, intToMcs(pid, 1023))
|
||||
err = SetFSCreateLabel("unconfined_u:unconfined_r:unconfined_t:s0")
|
||||
if err != nil {
|
||||
t.Fatal("SetFSCreateLabel failed:", err)
|
||||
}
|
||||
t.Log(FSCreateLabel())
|
||||
err = SetFSCreateLabel("")
|
||||
if err != nil {
|
||||
t.Fatal("SetFSCreateLabel failed:", err)
|
||||
}
|
||||
t.Log(FSCreateLabel())
|
||||
t.Log(PidLabel(1))
|
||||
}
|
||||
|
||||
func TestSetEnforceMode(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("root required, skipping")
|
||||
}
|
||||
|
||||
t.Log("Enforcing Mode:", EnforceMode())
|
||||
mode := DefaultEnforceMode()
|
||||
t.Log("Default Enforce Mode:", mode)
|
||||
defer func() {
|
||||
_ = SetEnforceMode(mode)
|
||||
}()
|
||||
|
||||
if err := SetEnforceMode(Enforcing); err != nil {
|
||||
t.Fatalf("setting selinux mode to enforcing failed: %v", err)
|
||||
}
|
||||
if err := SetEnforceMode(Permissive); err != nil {
|
||||
t.Fatalf("setting selinux mode to permissive failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalizeContext(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
con := "system_u:object_r:bin_t:s0:c1,c2,c3"
|
||||
checkcon := "system_u:object_r:bin_t:s0:c1.c3"
|
||||
newcon, err := CanonicalizeContext(con)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if newcon != checkcon {
|
||||
t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon)
|
||||
}
|
||||
con = "system_u:object_r:bin_t:s0:c5,c2"
|
||||
checkcon = "system_u:object_r:bin_t:s0:c2,c5"
|
||||
newcon, err = CanonicalizeContext(con)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if newcon != checkcon {
|
||||
t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindSELinuxfsInMountinfo(t *testing.T) {
|
||||
//nolint:dupword // ignore duplicate words (sysfs sysfs)
|
||||
const mountinfo = `18 62 0:17 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw,seclabel
|
||||
19 62 0:3 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw
|
||||
20 62 0:5 / /dev rw,nosuid shared:2 - devtmpfs devtmpfs rw,seclabel,size=3995472k,nr_inodes=998868,mode=755
|
||||
21 18 0:16 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw
|
||||
22 20 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw,seclabel
|
||||
23 20 0:11 / /dev/pts rw,nosuid,noexec,relatime shared:4 - devpts devpts rw,seclabel,gid=5,mode=620,ptmxmode=000
|
||||
24 62 0:19 / /run rw,nosuid,nodev shared:23 - tmpfs tmpfs rw,seclabel,mode=755
|
||||
25 18 0:20 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:8 - tmpfs tmpfs ro,seclabel,mode=755
|
||||
26 25 0:21 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:9 - cgroup cgroup rw,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
|
||||
27 18 0:22 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:20 - pstore pstore rw
|
||||
28 25 0:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,perf_event
|
||||
29 25 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:11 - cgroup cgroup rw,devices
|
||||
30 25 0:25 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:12 - cgroup cgroup rw,cpuacct,cpu
|
||||
31 25 0:26 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,freezer
|
||||
32 25 0:27 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,net_prio,net_cls
|
||||
33 25 0:28 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,cpuset
|
||||
34 25 0:29 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,memory
|
||||
35 25 0:30 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,pids
|
||||
36 25 0:31 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,hugetlb
|
||||
37 25 0:32 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,blkio
|
||||
59 18 0:33 / /sys/kernel/config rw,relatime shared:21 - configfs configfs rw
|
||||
62 1 253:1 / / rw,relatime shared:1 - ext4 /dev/vda1 rw,seclabel,data=ordered
|
||||
38 18 0:15 / /sys/fs/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw
|
||||
39 19 0:35 / /proc/sys/fs/binfmt_misc rw,relatime shared:24 - autofs systemd-1 rw,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=11601
|
||||
40 20 0:36 / /dev/hugepages rw,relatime shared:25 - hugetlbfs hugetlbfs rw,seclabel
|
||||
41 20 0:14 / /dev/mqueue rw,relatime shared:26 - mqueue mqueue rw,seclabel
|
||||
42 18 0:6 / /sys/kernel/debug rw,relatime shared:27 - debugfs debugfs rw
|
||||
112 62 253:1 /var/lib/docker/plugins /var/lib/docker/plugins rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered
|
||||
115 62 253:1 /var/lib/docker/overlay2 /var/lib/docker/overlay2 rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered
|
||||
118 62 7:0 / /root/mnt rw,relatime shared:66 - ext4 /dev/loop0 rw,seclabel,data=ordered
|
||||
121 115 0:38 / /var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/merged rw,relatime - overlay overlay rw,seclabel,lowerdir=/var/lib/docker/overlay2/l/CPD4XI7UD4GGTGSJVPQSHWZKTK:/var/lib/docker/overlay2/l/NQKORR3IS7KNQDER35AZECLH4Z,upperdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/diff,workdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/work
|
||||
125 62 0:39 / /var/lib/docker/containers/5e3fce422957c291a5b502c2cf33d512fc1fcac424e4113136c808360e5b7215/shm rw,nosuid,nodev,noexec,relatime shared:68 - tmpfs shm rw,seclabel,size=65536k
|
||||
186 24 0:3 / /run/docker/netns/0a08e7496c6d rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw
|
||||
130 62 0:15 / /root/chroot/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw
|
||||
109 24 0:37 / /run/user/0 rw,nosuid,nodev,relatime shared:62 - tmpfs tmpfs rw,seclabel,size=801032k,mode=700
|
||||
`
|
||||
s := bufio.NewScanner(bytes.NewBuffer([]byte(mountinfo)))
|
||||
for _, expected := range []string{"/sys/fs/selinux", "/root/chroot/selinux", ""} {
|
||||
mnt := findSELinuxfsMount(s)
|
||||
t.Logf("found %q", mnt)
|
||||
if mnt != expected {
|
||||
t.Fatalf("expected %q, got %q", expected, mnt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityCheckContext(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
// check with valid context
|
||||
context, err := CurrentLabel()
|
||||
if err != nil {
|
||||
t.Fatalf("CurrentLabel() error: %v", err)
|
||||
}
|
||||
if context != "" {
|
||||
t.Logf("SecurityCheckContext(%q)", context)
|
||||
err = SecurityCheckContext(context)
|
||||
if err != nil {
|
||||
t.Errorf("SecurityCheckContext(%q) error: %v", context, err)
|
||||
}
|
||||
}
|
||||
|
||||
context = "not-syntactically-valid"
|
||||
err = SecurityCheckContext(context)
|
||||
if err == nil {
|
||||
t.Errorf("SecurityCheckContext(%q) succeeded, expected to fail", context)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassIndex(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
idx, err := ClassIndex("process")
|
||||
if err != nil {
|
||||
t.Errorf("Classindex error: %v", err)
|
||||
}
|
||||
// Every known policy has process as index 2, but it isn't guaranteed
|
||||
if idx != 2 {
|
||||
t.Errorf("ClassIndex unexpected answer %d, possibly not reference policy", idx)
|
||||
}
|
||||
|
||||
_, err = ClassIndex("foobar")
|
||||
if err == nil {
|
||||
t.Errorf("ClassIndex(\"foobar\") succeeded, expected to fail:")
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeCreateContext(t *testing.T) {
|
||||
if !GetEnabled() {
|
||||
t.Skip("SELinux not enabled, skipping.")
|
||||
}
|
||||
|
||||
// This may or may not be in the loaded policy but any refpolicy based policy should have it
|
||||
init := "system_u:system_r:init_t:s0"
|
||||
tmp := "system_u:object_r:tmp_t:s0"
|
||||
file := "file"
|
||||
t.Logf("ComputeCreateContext(%s, %s, %s)", init, tmp, file)
|
||||
context, err := ComputeCreateContext(init, tmp, file)
|
||||
if err != nil {
|
||||
t.Errorf("ComputeCreateContext error: %v", err)
|
||||
}
|
||||
if context != "system_u:object_r:init_tmp_t:s0" {
|
||||
t.Errorf("ComputeCreateContext unexpected answer %s, possibly not reference policy", context)
|
||||
}
|
||||
|
||||
badcon := "badcon"
|
||||
process := "process"
|
||||
// Test to ensure that a bad context returns an error
|
||||
t.Logf("ComputeCreateContext(%s, %s, %s)", badcon, tmp, process)
|
||||
_, err = ComputeCreateContext(badcon, tmp, process)
|
||||
if err == nil {
|
||||
t.Errorf("ComputeCreateContext(%s, %s, %s) succeeded, expected failure", badcon, tmp, process)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlbLub(t *testing.T) {
|
||||
tests := []struct {
|
||||
expectedErr error
|
||||
sourceRange string
|
||||
targetRange string
|
||||
expectedRange string
|
||||
}{
|
||||
{
|
||||
sourceRange: "s0:c0.c100-s10:c0.c150",
|
||||
targetRange: "s5:c50.c100-s15:c0.c149",
|
||||
expectedRange: "s5:c50.c100-s10:c0.c149",
|
||||
},
|
||||
{
|
||||
sourceRange: "s5:c50.c100-s15:c0.c149",
|
||||
targetRange: "s0:c0.c100-s10:c0.c150",
|
||||
expectedRange: "s5:c50.c100-s10:c0.c149",
|
||||
},
|
||||
{
|
||||
sourceRange: "s0:c0.c100-s10:c0.c150",
|
||||
targetRange: "s0",
|
||||
expectedRange: "s0",
|
||||
},
|
||||
{
|
||||
sourceRange: "s6:c0.c1023",
|
||||
targetRange: "s6:c0,c2,c11,c201.c429,c431.c511",
|
||||
expectedRange: "s6:c0,c2,c11,c201.c429,c431.c511",
|
||||
},
|
||||
{
|
||||
sourceRange: "s0-s15:c0.c1023",
|
||||
targetRange: "s6:c0,c2,c11,c201.c429,c431.c511",
|
||||
expectedRange: "s6-s6:c0,c2,c11,c201.c429,c431.c511",
|
||||
},
|
||||
{
|
||||
sourceRange: "s0:c0.c100,c125,c140,c150-s10",
|
||||
targetRange: "s4:c0.c50,c140",
|
||||
expectedRange: "s4:c0.c50,c140-s4",
|
||||
},
|
||||
{
|
||||
sourceRange: "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023",
|
||||
targetRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023",
|
||||
expectedRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023",
|
||||
},
|
||||
{
|
||||
sourceRange: "s5:c512.c540,c542,c543,c552.c1023-s5:c0.c550,c552.c1023",
|
||||
targetRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023",
|
||||
expectedRange: "s5:c512.c540,c542,c543,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023",
|
||||
},
|
||||
{
|
||||
sourceRange: "s5:c50.c100-s15:c0.c149",
|
||||
targetRange: "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023",
|
||||
expectedRange: "s5-s5:c0.c149",
|
||||
},
|
||||
{
|
||||
sourceRange: "s5-s15",
|
||||
targetRange: "s6-s7",
|
||||
expectedRange: "s6-s7",
|
||||
},
|
||||
{
|
||||
sourceRange: "s5:c50.c100-s15:c0.c149",
|
||||
targetRange: "s4-s4:c0.c1023",
|
||||
expectedErr: ErrIncomparable,
|
||||
},
|
||||
{
|
||||
sourceRange: "s4-s4:c0.c1023",
|
||||
targetRange: "s5:c50.c100-s15:c0.c149",
|
||||
expectedErr: ErrIncomparable,
|
||||
},
|
||||
{
|
||||
sourceRange: "s4-s4:c0.c1023.c10000",
|
||||
targetRange: "s5:c50.c100-s15:c0.c149",
|
||||
expectedErr: strconv.ErrSyntax,
|
||||
},
|
||||
{
|
||||
sourceRange: "s4-s4:c0.c1023.c10000-s4",
|
||||
targetRange: "s5:c50.c100-s15:c0.c149-s5",
|
||||
expectedErr: strconv.ErrSyntax,
|
||||
},
|
||||
{
|
||||
sourceRange: "4-4",
|
||||
targetRange: "s5:c50.c100-s15:c0.c149",
|
||||
expectedErr: ErrLevelSyntax,
|
||||
},
|
||||
{
|
||||
sourceRange: "t4-t4",
|
||||
targetRange: "s5:c50.c100-s15:c0.c149",
|
||||
expectedErr: ErrLevelSyntax,
|
||||
},
|
||||
{
|
||||
sourceRange: "s5:x50.x100-s15:c0.c149",
|
||||
targetRange: "s5:c50.c100-s15:c0.c149",
|
||||
expectedErr: ErrLevelSyntax,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := CalculateGlbLub(tt.sourceRange, tt.targetRange)
|
||||
if !errors.Is(err, tt.expectedErr) {
|
||||
// Go 1.13 strconv errors are not unwrappable,
|
||||
// so do that manually.
|
||||
// TODO remove this once we stop supporting Go 1.13.
|
||||
var numErr *strconv.NumError
|
||||
if errors.As(err, &numErr) && numErr.Err == tt.expectedErr { //nolint:errorlint // see above
|
||||
continue
|
||||
}
|
||||
t.Fatalf("want %q got %q: src: %q tgt: %q", tt.expectedErr, err, tt.sourceRange, tt.targetRange)
|
||||
}
|
||||
|
||||
if got != tt.expectedRange {
|
||||
t.Errorf("want %q got %q", tt.expectedRange, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWithLevel(t *testing.T) {
|
||||
want := "bob:sysadm_r:sysadm_t:SystemLow-SystemHigh"
|
||||
|
||||
goodDefaultBuff := `
|
||||
foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
|
||||
staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
|
||||
`
|
||||
|
||||
verifier := func(con string) error {
|
||||
if con != want {
|
||||
return fmt.Errorf("invalid context %s", con)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name, userBuff, defaultBuff string
|
||||
}{
|
||||
{
|
||||
name: "match exists in user context file",
|
||||
userBuff: `# COMMENT
|
||||
foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
|
||||
|
||||
staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
|
||||
`,
|
||||
defaultBuff: goodDefaultBuff,
|
||||
},
|
||||
{
|
||||
name: "match exists in default context file, but not in user file",
|
||||
userBuff: `# COMMENT
|
||||
foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
|
||||
fake_r:fake_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
|
||||
`,
|
||||
defaultBuff: goodDefaultBuff,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := defaultSECtx{
|
||||
user: "bob",
|
||||
level: "SystemLow-SystemHigh",
|
||||
scon: "system_u:staff_r:staff_t:s0",
|
||||
userRdr: bytes.NewBufferString(tt.userBuff),
|
||||
defaultRdr: bytes.NewBufferString(tt.defaultBuff),
|
||||
verifier: verifier,
|
||||
}
|
||||
|
||||
got, err := getDefaultContextFromReaders(&c)
|
||||
if err != nil {
|
||||
t.Fatalf("err should not exist but is: %v", err)
|
||||
}
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("got context: %q but expected %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("no match in user or default context files", func(t *testing.T) {
|
||||
badUserBuff := ""
|
||||
|
||||
badDefaultBuff := `
|
||||
foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
|
||||
dne_r:dne_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
|
||||
`
|
||||
c := defaultSECtx{
|
||||
user: "bob",
|
||||
level: "SystemLow-SystemHigh",
|
||||
scon: "system_u:staff_r:staff_t:s0",
|
||||
userRdr: bytes.NewBufferString(badUserBuff),
|
||||
defaultRdr: bytes.NewBufferString(badDefaultBuff),
|
||||
verifier: verifier,
|
||||
}
|
||||
|
||||
_, err := getDefaultContextFromReaders(&c)
|
||||
if err == nil {
|
||||
t.Fatalf("err was expected")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkChcon(b *testing.B) {
|
||||
file, err := filepath.Abs(os.Args[0])
|
||||
if err != nil {
|
||||
b.Fatalf("filepath.Abs: %v", err)
|
||||
}
|
||||
dir := filepath.Dir(file)
|
||||
con, err := FileLabel(file)
|
||||
if err != nil {
|
||||
b.Fatalf("FileLabel(%q): %v", file, err)
|
||||
}
|
||||
b.Logf("Chcon(%q, %q)", dir, con)
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
if err := Chcon(dir, con, true); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCurrentLabel(b *testing.B) {
|
||||
var (
|
||||
l string
|
||||
err error
|
||||
)
|
||||
for n := 0; n < b.N; n++ {
|
||||
l, err = CurrentLabel()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
b.Log(l)
|
||||
}
|
||||
|
||||
func BenchmarkReadConfig(b *testing.B) {
|
||||
str := ""
|
||||
for n := 0; n < b.N; n++ {
|
||||
str = readConfig(selinuxTypeTag)
|
||||
}
|
||||
b.Log(str)
|
||||
}
|
||||
|
||||
func BenchmarkLoadLabels(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
loadLabels()
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package selinux
|
||||
|
||||
func attrPath(string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func readConThreadSelf(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func writeConThreadSelf(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDisabled() {}
|
||||
|
||||
func getEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func classIndex(string) (int, error) {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func setFileLabel(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func lSetFileLabel(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileLabel(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func lFileLabel(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func setFSCreateLabel(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func fsCreateLabel() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func currentLabel() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func pidLabel(int) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func execLabel() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func canonicalizeContext(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func computeCreateContext(string, string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func calculateGlbLub(string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func peerLabel(uintptr) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func setKeyLabel(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func keyLabel() (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c Context) get() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func newContext(string) (Context, error) {
|
||||
return Context{}, nil
|
||||
}
|
||||
|
||||
func clearLabels() {
|
||||
}
|
||||
|
||||
func reserveLabel(string) {
|
||||
}
|
||||
|
||||
func isMLSEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func enforceMode() int {
|
||||
return Disabled
|
||||
}
|
||||
|
||||
func setEnforceMode(int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultEnforceMode() int {
|
||||
return Disabled
|
||||
}
|
||||
|
||||
func releaseLabel(string) {
|
||||
}
|
||||
|
||||
func roFileLabel() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func kvmContainerLabels() (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func initContainerLabels() (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func containerLabels() (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func securityCheckContext(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyLevel(string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func chcon(string, string, bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func dupSecOpt(string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getDefaultContextWithLevel(string, string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func label(_ string) string {
|
||||
return ""
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
package selinux
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testLabel = "foobar"
|
||||
|
||||
func TestSELinuxStubs(t *testing.T) {
|
||||
if GetEnabled() {
|
||||
t.Error("SELinux enabled on non-linux.")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
if _, err := FileLabel(tmpDir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := SetFileLabel(tmpDir, testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, err := LfileLabel(tmpDir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := LsetFileLabel(tmpDir, testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := SetFSCreateLabel(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, err := FSCreateLabel(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := CurrentLabel(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, err := PidLabel(0); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ClearLabels()
|
||||
|
||||
ReserveLabel(testLabel)
|
||||
ReleaseLabel(testLabel)
|
||||
if _, err := DupSecOpt(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if v := DisableSecOpt(); len(v) != 1 || v[0] != "disable" {
|
||||
t.Errorf(`expected "disabled", got %v`, v)
|
||||
}
|
||||
SetDisabled()
|
||||
if enabled := GetEnabled(); enabled {
|
||||
t.Error("Should not be enabled")
|
||||
}
|
||||
if err := SetExecLabel(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := SetTaskLabel(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := ExecLabel(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := CanonicalizeContext(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := ComputeCreateContext("foo", "bar", testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := SetSocketLabel(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := ClassIndex(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := SocketLabel(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := PeerLabel(0); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := SetKeyLabel(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := KeyLabel(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := SetExecLabel(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := ExecLabel(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
con, err := NewContext(testLabel)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
con.Get()
|
||||
if err = SetEnforceMode(1); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if v := DefaultEnforceMode(); v != Disabled {
|
||||
t.Errorf("expected %d, got %d", Disabled, v)
|
||||
}
|
||||
if v := EnforceMode(); v != Disabled {
|
||||
t.Errorf("expected %d, got %d", Disabled, v)
|
||||
}
|
||||
if v := ROFileLabel(); v != "" {
|
||||
t.Errorf(`expected "", got %q`, v)
|
||||
}
|
||||
if processLbl, fileLbl := ContainerLabels(); processLbl != "" || fileLbl != "" {
|
||||
t.Errorf(`expected fileLbl="", fileLbl="" got processLbl=%q, fileLbl=%q`, processLbl, fileLbl)
|
||||
}
|
||||
if err = SecurityCheckContext(testLabel); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err = CopyLevel("foo", "bar"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package selinux
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// lgetxattr returns a []byte slice containing the value of
|
||||
// an extended attribute attr set for path.
|
||||
func lgetxattr(path, attr string) ([]byte, error) {
|
||||
// Start with a 128 length byte array
|
||||
dest := make([]byte, 128)
|
||||
sz, errno := doLgetxattr(path, attr, dest)
|
||||
for errno == unix.ERANGE { //nolint:errorlint // unix errors are bare
|
||||
// Buffer too small, use zero-sized buffer to get the actual size
|
||||
sz, errno = doLgetxattr(path, attr, []byte{})
|
||||
if errno != nil {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
dest = make([]byte, sz)
|
||||
sz, errno = doLgetxattr(path, attr, dest)
|
||||
}
|
||||
if errno != nil {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
return dest[:sz], nil
|
||||
}
|
||||
|
||||
// doLgetxattr is a wrapper that retries on EINTR
|
||||
func doLgetxattr(path, attr string, dest []byte) (int, error) {
|
||||
for {
|
||||
sz, err := unix.Lgetxattr(path, attr, dest)
|
||||
if err != unix.EINTR {
|
||||
return sz, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getxattr returns a []byte slice containing the value of
|
||||
// an extended attribute attr set for path.
|
||||
func getxattr(path, attr string) ([]byte, error) {
|
||||
// Start with a 128 length byte array
|
||||
dest := make([]byte, 128)
|
||||
sz, errno := dogetxattr(path, attr, dest)
|
||||
for errno == unix.ERANGE { //nolint:errorlint // unix errors are bare
|
||||
// Buffer too small, use zero-sized buffer to get the actual size
|
||||
sz, errno = dogetxattr(path, attr, []byte{})
|
||||
if errno != nil {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
dest = make([]byte, sz)
|
||||
sz, errno = dogetxattr(path, attr, dest)
|
||||
}
|
||||
if errno != nil {
|
||||
return nil, errno
|
||||
}
|
||||
|
||||
return dest[:sz], nil
|
||||
}
|
||||
|
||||
// dogetxattr is a wrapper that retries on EINTR
|
||||
func dogetxattr(path, attr string, dest []byte) (int, error) {
|
||||
for {
|
||||
sz, err := unix.Getxattr(path, attr, dest)
|
||||
if err != unix.EINTR {
|
||||
return sz, err
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
module github.com/opencontainers/selinux
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/cyphar/filepath-securejoin v0.5.0
|
||||
golang.org/x/sys v0.18.0
|
||||
)
|
||||
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
|
||||
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
@@ -1,52 +0,0 @@
|
||||
## pwalk: parallel implementation of filepath.Walk
|
||||
|
||||
This is a wrapper for [filepath.Walk](https://pkg.go.dev/path/filepath?tab=doc#Walk)
|
||||
which may speed it up by calling multiple callback functions (WalkFunc) in parallel,
|
||||
utilizing goroutines.
|
||||
|
||||
By default, it utilizes 2\*runtime.NumCPU() goroutines for callbacks.
|
||||
This can be changed by using WalkN function which has the additional
|
||||
parameter, specifying the number of goroutines (concurrency).
|
||||
|
||||
### pwalk vs pwalkdir
|
||||
|
||||
This package is deprecated in favor of
|
||||
[pwalkdir](https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir),
|
||||
which is faster, but requires at least Go 1.16.
|
||||
|
||||
### Caveats
|
||||
|
||||
Please note the following limitations of this code:
|
||||
|
||||
* Unlike filepath.Walk, the order of calls is non-deterministic;
|
||||
|
||||
* Only primitive error handling is supported:
|
||||
|
||||
* filepath.SkipDir is not supported;
|
||||
|
||||
* ErrNotExist errors from filepath.Walk are silently ignored for any path
|
||||
except the top directory (Walk argument); any other error is returned to
|
||||
the caller of Walk;
|
||||
|
||||
* no errors are ever passed to WalkFunc;
|
||||
|
||||
* once any error is returned from any WalkFunc instance, no more new calls
|
||||
to WalkFunc are made, and the error is returned to the caller of Walk;
|
||||
|
||||
* if more than one walkFunc instance will return an error, only one
|
||||
of such errors will be propagated and returned by Walk, others
|
||||
will be silently discarded.
|
||||
|
||||
### Documentation
|
||||
|
||||
For the official documentation, see
|
||||
https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalk?tab=doc
|
||||
|
||||
### Benchmarks
|
||||
|
||||
For a WalkFunc that consists solely of the return statement, this
|
||||
implementation is about 10% slower than the standard library's
|
||||
filepath.Walk.
|
||||
|
||||
Otherwise (if a WalkFunc is doing something) this is usually faster,
|
||||
except when the WalkN(..., 1) is used.
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
package pwalk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// WalkFunc is the type of the function called by Walk to visit each
|
||||
// file or directory. It is an alias for [filepath.WalkFunc].
|
||||
//
|
||||
// Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir] and [fs.WalkDirFunc].
|
||||
type WalkFunc = filepath.WalkFunc
|
||||
|
||||
// Walk is a wrapper for filepath.Walk which can call multiple walkFn
|
||||
// in parallel, allowing to handle each item concurrently. A maximum of
|
||||
// twice the runtime.NumCPU() walkFn will be called at any one time.
|
||||
// If you want to change the maximum, use WalkN instead.
|
||||
//
|
||||
// The order of calls is non-deterministic.
|
||||
//
|
||||
// Note that this implementation only supports primitive error handling:
|
||||
//
|
||||
// - no errors are ever passed to walkFn;
|
||||
//
|
||||
// - once a walkFn returns any error, all further processing stops
|
||||
// and the error is returned to the caller of Walk;
|
||||
//
|
||||
// - filepath.SkipDir is not supported;
|
||||
//
|
||||
// - if more than one walkFn instance will return an error, only one
|
||||
// of such errors will be propagated and returned by Walk, others
|
||||
// will be silently discarded.
|
||||
//
|
||||
// Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir.Walk]
|
||||
func Walk(root string, walkFn WalkFunc) error {
|
||||
return WalkN(root, walkFn, runtime.NumCPU()*2)
|
||||
}
|
||||
|
||||
// WalkN is a wrapper for filepath.Walk which can call multiple walkFn
|
||||
// in parallel, allowing to handle each item concurrently. A maximum of
|
||||
// num walkFn will be called at any one time.
|
||||
//
|
||||
// Please see Walk documentation for caveats of using this function.
|
||||
//
|
||||
// Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir.WalkN]
|
||||
func WalkN(root string, walkFn WalkFunc, num int) error {
|
||||
// make sure limit is sensible
|
||||
if num < 1 {
|
||||
return fmt.Errorf("walk(%q): num must be > 0", root)
|
||||
}
|
||||
|
||||
files := make(chan *walkArgs, 2*num)
|
||||
errCh := make(chan error, 1) // get the first error, ignore others
|
||||
|
||||
// Start walking a tree asap
|
||||
var (
|
||||
err error
|
||||
wg sync.WaitGroup
|
||||
|
||||
rootLen = len(root)
|
||||
rootEntry *walkArgs
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// Walking a file tree can race with removal,
|
||||
// so ignore ENOENT, except for root.
|
||||
// https://github.com/opencontainers/selinux/issues/199.
|
||||
if errors.Is(err, os.ErrNotExist) && len(p) != rootLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
close(files)
|
||||
return err
|
||||
}
|
||||
if len(p) == rootLen {
|
||||
// Root entry is processed separately below.
|
||||
rootEntry = &walkArgs{path: p, info: &info}
|
||||
return nil
|
||||
}
|
||||
// add a file to the queue unless a callback sent an error
|
||||
select {
|
||||
case e := <-errCh:
|
||||
close(files)
|
||||
return e
|
||||
default:
|
||||
files <- &walkArgs{path: p, info: &info}
|
||||
return nil
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
close(files)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Add(num)
|
||||
for i := 0; i < num; i++ {
|
||||
go func() {
|
||||
for file := range files {
|
||||
if e := walkFn(file.path, *file.info, nil); e != nil {
|
||||
select {
|
||||
case errCh <- e: // sent ok
|
||||
default: // buffer full
|
||||
}
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if err == nil {
|
||||
err = walkFn(rootEntry.path, *rootEntry.info, nil)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// walkArgs holds the arguments that were passed to the Walk or WalkN
|
||||
// functions.
|
||||
type walkArgs struct {
|
||||
info *os.FileInfo
|
||||
path string
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
package pwalk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
var ac atomic.Uint32
|
||||
concurrency := runtime.NumCPU() * 2
|
||||
|
||||
dir, total := prepareTestSet(t, 3, 2, 1)
|
||||
|
||||
err := WalkN(dir,
|
||||
func(_ string, _ os.FileInfo, _ error) error {
|
||||
ac.Add(1)
|
||||
return nil
|
||||
},
|
||||
concurrency)
|
||||
if err != nil {
|
||||
t.Errorf("Walk failed: %v", err)
|
||||
}
|
||||
count := ac.Load()
|
||||
if count != total {
|
||||
t.Errorf("File count mismatch: found %d, expected %d", count, total)
|
||||
}
|
||||
|
||||
t.Logf("concurrency: %d, files found: %d", concurrency, count)
|
||||
}
|
||||
|
||||
func TestWalkTopLevelErrNotExistNotIgnored(t *testing.T) {
|
||||
if WalkN("non-existent-directory", cbEmpty, 8) == nil {
|
||||
t.Fatal("expected ErrNotExist, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/selinux/issues/199
|
||||
func TestWalkRaceWithRemoval(t *testing.T) {
|
||||
var ac atomic.Uint32
|
||||
concurrency := runtime.NumCPU() * 2
|
||||
// This test is still on a best-effort basis, meaning it can still pass
|
||||
// when there is a bug in the code, but the larger the test set is, the
|
||||
// higher the probability that this test fails (without a fix).
|
||||
//
|
||||
// With this set (4, 5, 6), and the fix commented out, it fails
|
||||
// 100 out of 100 runs on my machine.
|
||||
dir, total := prepareTestSet(t, 4, 5, 6)
|
||||
|
||||
// Race walk with removal.
|
||||
go os.RemoveAll(dir)
|
||||
err := WalkN(dir,
|
||||
func(_ string, _ os.FileInfo, _ error) error {
|
||||
ac.Add(1)
|
||||
return nil
|
||||
},
|
||||
concurrency)
|
||||
count := int(ac.Load())
|
||||
t.Logf("found %d of %d files", count, total)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirManyErrors(t *testing.T) {
|
||||
var ac atomic.Uint32
|
||||
|
||||
dir, total := prepareTestSet(t, 3, 3, 2)
|
||||
|
||||
maxFiles := total / 2
|
||||
e42 := errors.New("42")
|
||||
err := Walk(dir,
|
||||
func(_ string, _ os.FileInfo, _ error) error {
|
||||
if ac.Add(1) > maxFiles {
|
||||
return e42
|
||||
}
|
||||
return nil
|
||||
})
|
||||
count := ac.Load()
|
||||
t.Logf("found %d of %d files", count, total)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Walk succeeded, but error is expected")
|
||||
if count != total {
|
||||
t.Errorf("File count mismatch: found %d, expected %d", count, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeManyDirs(prefix string, levels, dirs, files int) (count uint32, err error) {
|
||||
for d := 0; d < dirs; d++ {
|
||||
var dir string
|
||||
dir, err = os.MkdirTemp(prefix, "d-")
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
for f := 0; f < files; f++ {
|
||||
var fi *os.File
|
||||
fi, err = os.CreateTemp(dir, "f-")
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
_ = fi.Close()
|
||||
count++
|
||||
}
|
||||
if levels == 0 {
|
||||
continue
|
||||
}
|
||||
var c uint32
|
||||
if c, err = makeManyDirs(dir, levels-1, dirs, files); err != nil {
|
||||
return count, err
|
||||
}
|
||||
count += c
|
||||
}
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// prepareTestSet() creates a directory tree of shallow files,
|
||||
// to be used for testing or benchmarking.
|
||||
//
|
||||
// Total dirs: dirs^levels + dirs^(levels-1) + ... + dirs^1
|
||||
// Total files: total_dirs * files
|
||||
func prepareTestSet(tb testing.TB, levels, dirs, files int) (dir string, total uint32) {
|
||||
tb.Helper()
|
||||
var err error
|
||||
|
||||
dir = tb.TempDir()
|
||||
total, err = makeManyDirs(dir, levels, dirs, files)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
total++ // this dir
|
||||
|
||||
return dir, total
|
||||
}
|
||||
|
||||
type walkerFunc func(root string, walkFn WalkFunc) error
|
||||
|
||||
func genWalkN(n int) walkerFunc {
|
||||
return func(root string, walkFn WalkFunc) error {
|
||||
return WalkN(root, walkFn, n)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWalk(b *testing.B) {
|
||||
const (
|
||||
levels = 5 // how deep
|
||||
dirs = 3 // dirs on each levels
|
||||
files = 8 // files on each levels
|
||||
)
|
||||
|
||||
benchmarks := []struct {
|
||||
walk filepath.WalkFunc
|
||||
name string
|
||||
}{
|
||||
{name: "Empty", walk: cbEmpty},
|
||||
{name: "ReadFile", walk: cbReadFile},
|
||||
{name: "ChownChmod", walk: cbChownChmod},
|
||||
{name: "RandomSleep", walk: cbRandomSleep},
|
||||
}
|
||||
|
||||
walkers := []struct {
|
||||
walker walkerFunc
|
||||
name string
|
||||
}{
|
||||
{name: "filepath.Walk", walker: filepath.Walk},
|
||||
{name: "pwalk.Walk", walker: Walk},
|
||||
// test WalkN with various values of N
|
||||
{name: "pwalk.Walk1", walker: genWalkN(1)},
|
||||
{name: "pwalk.Walk2", walker: genWalkN(2)},
|
||||
{name: "pwalk.Walk4", walker: genWalkN(4)},
|
||||
{name: "pwalk.Walk8", walker: genWalkN(8)},
|
||||
{name: "pwalk.Walk16", walker: genWalkN(16)},
|
||||
{name: "pwalk.Walk32", walker: genWalkN(32)},
|
||||
{name: "pwalk.Walk64", walker: genWalkN(64)},
|
||||
{name: "pwalk.Walk128", walker: genWalkN(128)},
|
||||
{name: "pwalk.Walk256", walker: genWalkN(256)},
|
||||
}
|
||||
|
||||
dir, total := prepareTestSet(b, levels, dirs, files)
|
||||
b.Logf("dataset: %d levels x %d dirs x %d files, total entries: %d", levels, dirs, files, total)
|
||||
|
||||
for _, bm := range benchmarks {
|
||||
for _, w := range walkers {
|
||||
walker := w.walker
|
||||
walkFn := bm.walk
|
||||
// preheat
|
||||
if err := w.walker(dir, bm.walk); err != nil {
|
||||
b.Errorf("walk failed: %v", err)
|
||||
}
|
||||
// benchmark
|
||||
b.Run(bm.name+"/"+w.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := walker(dir, walkFn); err != nil {
|
||||
b.Errorf("walk failed: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cbEmpty(_ string, _ os.FileInfo, _ error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func cbChownChmod(path string, info os.FileInfo, _ error) error {
|
||||
_ = os.Chown(path, 0, 0)
|
||||
mode := os.FileMode(0o644)
|
||||
if info.Mode().IsDir() {
|
||||
mode = os.FileMode(0o755)
|
||||
}
|
||||
_ = os.Chmod(path, mode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cbReadFile(path string, info os.FileInfo, _ error) error {
|
||||
var err error
|
||||
if info.Mode().IsRegular() {
|
||||
_, err = os.ReadFile(path)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cbRandomSleep(_ string, _ os.FileInfo, _ error) error {
|
||||
time.Sleep(time.Duration(rand.Intn(500)) * time.Microsecond) //nolint:gosec // ignore G404: Use of weak random number generator
|
||||
return nil
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
## pwalkdir: parallel implementation of filepath.WalkDir
|
||||
|
||||
This is a wrapper for [filepath.WalkDir](https://pkg.go.dev/path/filepath#WalkDir)
|
||||
which may speed it up by calling multiple callback functions (WalkDirFunc)
|
||||
in parallel, utilizing goroutines.
|
||||
|
||||
By default, it utilizes 2\*runtime.NumCPU() goroutines for callbacks.
|
||||
This can be changed by using WalkN function which has the additional
|
||||
parameter, specifying the number of goroutines (concurrency).
|
||||
|
||||
### pwalk vs pwalkdir
|
||||
|
||||
This package is very similar to
|
||||
[pwalk](https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir),
|
||||
but utilizes `filepath.WalkDir` (added to Go 1.16), which does not call stat(2)
|
||||
on every entry and is therefore faster (up to 3x, depending on usage scenario).
|
||||
|
||||
Users who are OK with requiring Go 1.16+ should switch to this
|
||||
implementation.
|
||||
|
||||
### Caveats
|
||||
|
||||
Please note the following limitations of this code:
|
||||
|
||||
* Unlike filepath.WalkDir, the order of calls is non-deterministic;
|
||||
|
||||
* Only primitive error handling is supported:
|
||||
|
||||
* fs.SkipDir is not supported;
|
||||
|
||||
* ErrNotExist errors from filepath.WalkDir are silently ignored for any path
|
||||
except the top directory (WalkDir argument); any other error is returned to
|
||||
the caller of WalkDir;
|
||||
|
||||
* once any error is returned from any walkDirFunc instance, no more calls
|
||||
to WalkDirFunc are made, and the error is returned to the caller of WalkDir;
|
||||
|
||||
* if more than one WalkDirFunc instance will return an error, only one
|
||||
of such errors will be propagated to and returned by WalkDir, others
|
||||
will be silently discarded.
|
||||
|
||||
### Documentation
|
||||
|
||||
For the official documentation, see
|
||||
https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir
|
||||
|
||||
### Benchmarks
|
||||
|
||||
For a WalkDirFunc that consists solely of the return statement, this
|
||||
implementation is about 15% slower than the standard library's
|
||||
filepath.WalkDir.
|
||||
|
||||
Otherwise (if a WalkDirFunc is actually doing something) this is usually
|
||||
faster, except when the WalkDirN(..., 1) is used. Run `go test -bench .`
|
||||
to see how different operations can benefit from it, as well as how the
|
||||
level of parallelism affects the speed.
|
||||
@@ -1,123 +0,0 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package pwalkdir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Walk is a wrapper for filepath.WalkDir which can call multiple walkFn
|
||||
// in parallel, allowing to handle each item concurrently. A maximum of
|
||||
// twice the runtime.NumCPU() walkFn will be called at any one time.
|
||||
// If you want to change the maximum, use WalkN instead.
|
||||
//
|
||||
// The order of calls is non-deterministic.
|
||||
//
|
||||
// Note that this implementation only supports primitive error handling:
|
||||
//
|
||||
// - no errors are ever passed to walkFn;
|
||||
//
|
||||
// - once a walkFn returns any error, all further processing stops
|
||||
// and the error is returned to the caller of Walk;
|
||||
//
|
||||
// - filepath.SkipDir is not supported;
|
||||
//
|
||||
// - if more than one walkFn instance will return an error, only one
|
||||
// of such errors will be propagated and returned by Walk, others
|
||||
// will be silently discarded.
|
||||
func Walk(root string, walkFn fs.WalkDirFunc) error {
|
||||
return WalkN(root, walkFn, runtime.NumCPU()*2)
|
||||
}
|
||||
|
||||
// WalkN is a wrapper for filepath.WalkDir which can call multiple walkFn
|
||||
// in parallel, allowing to handle each item concurrently. A maximum of
|
||||
// num walkFn will be called at any one time.
|
||||
//
|
||||
// Please see Walk documentation for caveats of using this function.
|
||||
func WalkN(root string, walkFn fs.WalkDirFunc, num int) error {
|
||||
// make sure limit is sensible
|
||||
if num < 1 {
|
||||
return fmt.Errorf("walk(%q): num must be > 0", root)
|
||||
}
|
||||
|
||||
files := make(chan *walkArgs, 2*num)
|
||||
errCh := make(chan error, 1) // Get the first error, ignore others.
|
||||
|
||||
// Start walking a tree asap.
|
||||
var (
|
||||
err error
|
||||
wg sync.WaitGroup
|
||||
|
||||
rootLen = len(root)
|
||||
rootEntry *walkArgs
|
||||
)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = filepath.WalkDir(root, func(p string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
// Walking a file tree can race with removal,
|
||||
// so ignore ENOENT, except for root.
|
||||
// https://github.com/opencontainers/selinux/issues/199.
|
||||
if errors.Is(err, fs.ErrNotExist) && len(p) != rootLen {
|
||||
return nil
|
||||
}
|
||||
close(files)
|
||||
return err
|
||||
}
|
||||
if len(p) == rootLen {
|
||||
// Root entry is processed separately below.
|
||||
rootEntry = &walkArgs{path: p, entry: entry}
|
||||
return nil
|
||||
}
|
||||
// Add a file to the queue unless a callback sent an error.
|
||||
select {
|
||||
case e := <-errCh:
|
||||
close(files)
|
||||
return e
|
||||
default:
|
||||
files <- &walkArgs{path: p, entry: entry}
|
||||
return nil
|
||||
}
|
||||
})
|
||||
if err == nil {
|
||||
close(files)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Add(num)
|
||||
for i := 0; i < num; i++ {
|
||||
go func() {
|
||||
for file := range files {
|
||||
if e := walkFn(file.path, file.entry, nil); e != nil {
|
||||
select {
|
||||
case errCh <- e: // sent ok
|
||||
default: // buffer full
|
||||
}
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if err == nil {
|
||||
err = walkFn(rootEntry.path, rootEntry.entry, nil)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// walkArgs holds the arguments that were passed to the Walk or WalkN
|
||||
// functions.
|
||||
type walkArgs struct {
|
||||
entry fs.DirEntry
|
||||
path string
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
//go:build go1.16
|
||||
// +build go1.16
|
||||
|
||||
package pwalkdir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWalkDir(t *testing.T) {
|
||||
var ac atomic.Uint32
|
||||
concurrency := runtime.NumCPU() * 2
|
||||
dir, total := prepareTestSet(t, 3, 2, 1)
|
||||
|
||||
err := WalkN(dir,
|
||||
func(_ string, _ fs.DirEntry, _ error) error {
|
||||
ac.Add(1)
|
||||
return nil
|
||||
},
|
||||
concurrency)
|
||||
if err != nil {
|
||||
t.Errorf("Walk failed: %v", err)
|
||||
}
|
||||
count := ac.Load()
|
||||
if count != total {
|
||||
t.Errorf("File count mismatch: found %d, expected %d", count, total)
|
||||
}
|
||||
|
||||
t.Logf("concurrency: %d, files found: %d", concurrency, count)
|
||||
}
|
||||
|
||||
func TestWalkDirTopLevelErrNotExistNotIgnored(t *testing.T) {
|
||||
err := WalkN("non-existent-directory", cbEmpty, 8)
|
||||
if err == nil {
|
||||
t.Fatal("expected ErrNotExist, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/selinux/issues/199
|
||||
func TestWalkDirRaceWithRemoval(t *testing.T) {
|
||||
var ac atomic.Uint32
|
||||
concurrency := runtime.NumCPU() * 2
|
||||
// This test is still on a best-effort basis, meaning it can still pass
|
||||
// when there is a bug in the code, but the larger the test set is, the
|
||||
// higher the probability that this test fails (without a fix).
|
||||
//
|
||||
// With this set (4, 5, 6), and the fix commented out, it fails
|
||||
// about 90 out of 100 runs on my machine.
|
||||
dir, total := prepareTestSet(t, 4, 5, 6)
|
||||
|
||||
// Make walk race with removal.
|
||||
go os.RemoveAll(dir)
|
||||
err := WalkN(dir,
|
||||
func(_ string, _ fs.DirEntry, _ error) error {
|
||||
ac.Add(1)
|
||||
return nil
|
||||
},
|
||||
concurrency)
|
||||
count := ac.Load()
|
||||
t.Logf("found %d of %d files", count, total)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirManyErrors(t *testing.T) {
|
||||
var ac atomic.Uint32
|
||||
dir, total := prepareTestSet(t, 3, 3, 2)
|
||||
|
||||
maxFiles := total / 2
|
||||
e42 := errors.New("42")
|
||||
err := Walk(dir,
|
||||
func(_ string, _ fs.DirEntry, _ error) error {
|
||||
if ac.Add(1) > maxFiles {
|
||||
return e42
|
||||
}
|
||||
return nil
|
||||
})
|
||||
count := ac.Load()
|
||||
t.Logf("found %d of %d files", count, total)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Walk succeeded, but error is expected")
|
||||
if count != total {
|
||||
t.Errorf("File count mismatch: found %d, expected %d", count, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeManyDirs(prefix string, levels, dirs, files int) (count uint32, err error) {
|
||||
for d := 0; d < dirs; d++ {
|
||||
var dir string
|
||||
dir, err = os.MkdirTemp(prefix, "d-")
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
for f := 0; f < files; f++ {
|
||||
var fi *os.File
|
||||
fi, err = os.CreateTemp(dir, "f-")
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
fi.Close()
|
||||
count++
|
||||
}
|
||||
if levels == 0 {
|
||||
continue
|
||||
}
|
||||
var c uint32
|
||||
if c, err = makeManyDirs(dir, levels-1, dirs, files); err != nil {
|
||||
return count, err
|
||||
}
|
||||
count += c
|
||||
}
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// prepareTestSet() creates a directory tree of shallow files,
|
||||
// to be used for testing or benchmarking.
|
||||
//
|
||||
// Total dirs: dirs^levels + dirs^(levels-1) + ... + dirs^1
|
||||
// Total files: total_dirs * files
|
||||
func prepareTestSet(tb testing.TB, levels, dirs, files int) (dir string, total uint32) {
|
||||
tb.Helper()
|
||||
var err error
|
||||
|
||||
dir = tb.TempDir()
|
||||
total, err = makeManyDirs(dir, levels, dirs, files)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
total++ // this dir
|
||||
|
||||
return dir, total
|
||||
}
|
||||
|
||||
type walkerFunc func(root string, walkFn fs.WalkDirFunc) error
|
||||
|
||||
func genWalkN(n int) walkerFunc {
|
||||
return func(root string, walkFn fs.WalkDirFunc) error {
|
||||
return WalkN(root, walkFn, n)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWalk(b *testing.B) {
|
||||
const (
|
||||
levels = 5 // how deep
|
||||
dirs = 3 // dirs on each levels
|
||||
files = 8 // files on each levels
|
||||
)
|
||||
|
||||
benchmarks := []struct {
|
||||
walk fs.WalkDirFunc
|
||||
name string
|
||||
}{
|
||||
{name: "Empty", walk: cbEmpty},
|
||||
{name: "ReadFile", walk: cbReadFile},
|
||||
{name: "ChownChmod", walk: cbChownChmod},
|
||||
{name: "RandomSleep", walk: cbRandomSleep},
|
||||
}
|
||||
|
||||
walkers := []struct {
|
||||
walker walkerFunc
|
||||
name string
|
||||
}{
|
||||
{name: "filepath.WalkDir", walker: filepath.WalkDir},
|
||||
{name: "pwalkdir.Walk", walker: Walk},
|
||||
// test WalkN with various values of N
|
||||
{name: "pwalkdir.Walk1", walker: genWalkN(1)},
|
||||
{name: "pwalkdir.Walk2", walker: genWalkN(2)},
|
||||
{name: "pwalkdir.Walk4", walker: genWalkN(4)},
|
||||
{name: "pwalkdir.Walk8", walker: genWalkN(8)},
|
||||
{name: "pwalkdir.Walk16", walker: genWalkN(16)},
|
||||
{name: "pwalkdir.Walk32", walker: genWalkN(32)},
|
||||
{name: "pwalkdir.Walk64", walker: genWalkN(64)},
|
||||
{name: "pwalkdir.Walk128", walker: genWalkN(128)},
|
||||
{name: "pwalkdir.Walk256", walker: genWalkN(256)},
|
||||
}
|
||||
|
||||
dir, total := prepareTestSet(b, levels, dirs, files)
|
||||
b.Logf("dataset: %d levels x %d dirs x %d files, total entries: %d", levels, dirs, files, total)
|
||||
|
||||
for _, bm := range benchmarks {
|
||||
for _, w := range walkers {
|
||||
walker := w.walker
|
||||
walkFn := bm.walk
|
||||
// preheat
|
||||
if err := w.walker(dir, bm.walk); err != nil {
|
||||
b.Errorf("walk failed: %v", err)
|
||||
}
|
||||
// benchmark
|
||||
b.Run(bm.name+"/"+w.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := walker(dir, walkFn); err != nil {
|
||||
b.Errorf("walk failed: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cbEmpty(_ string, _ fs.DirEntry, _ error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func cbChownChmod(path string, e fs.DirEntry, _ error) error {
|
||||
_ = os.Chown(path, 0, 0)
|
||||
mode := os.FileMode(0o644)
|
||||
if e.IsDir() {
|
||||
mode = os.FileMode(0o755)
|
||||
}
|
||||
_ = os.Chmod(path, mode)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cbReadFile(path string, e fs.DirEntry, _ error) error {
|
||||
var err error
|
||||
if e.Type().IsRegular() {
|
||||
_, err = os.ReadFile(path)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cbRandomSleep(_ string, _ fs.DirEntry, _ error) error {
|
||||
time.Sleep(time.Duration(rand.Intn(500)) * time.Microsecond) //nolint:gosec // ignore G404: Use of weak random number generator
|
||||
return nil
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# 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/.
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- bidichk
|
||||
- cyclop
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- goconst
|
||||
- godot
|
||||
- gomoddirectives
|
||||
- gosec
|
||||
- mirror
|
||||
- misspell
|
||||
- mnd
|
||||
- nilerr
|
||||
- nilnil
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- reassign
|
||||
- revive
|
||||
- unconvert
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
- goimports
|
||||
settings:
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- cyphar.com/go-pathrs
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
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/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// 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 provides bindings for libpathrs, a library for safe path
|
||||
// resolution on Linux.
|
||||
package pathrs
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
//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 (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"cyphar.com/go-pathrs/internal/fdutils"
|
||||
"cyphar.com/go-pathrs/internal/libpathrs"
|
||||
)
|
||||
|
||||
// Handle is a handle for a path within a given [Root]. This handle references
|
||||
// an already-resolved path which can be used for only one purpose -- to
|
||||
// "re-open" the handle and get an actual [os.File] which can be used for
|
||||
// ordinary operations.
|
||||
//
|
||||
// If you wish to open a file without having an intermediate [Handle] object,
|
||||
// you can try to use [Root.Open] or [Root.OpenFile].
|
||||
//
|
||||
// It is critical that perform all relevant operations through this [Handle]
|
||||
// (rather than fetching the file descriptor yourself with [Handle.IntoRaw]),
|
||||
// because the security properties of libpathrs depend on users doing all
|
||||
// relevant filesystem operations through libpathrs.
|
||||
//
|
||||
// [os.File]: https://pkg.go.dev/os#File
|
||||
type Handle struct {
|
||||
inner *os.File
|
||||
}
|
||||
|
||||
// HandleFromFile creates a new [Handle] from an existing file handle. The
|
||||
// handle will be copied by this method, so the original handle should still be
|
||||
// freed by the caller.
|
||||
//
|
||||
// This is effectively the inverse operation of [Handle.IntoRaw], and is used
|
||||
// for "deserialising" pathrs root handles.
|
||||
func HandleFromFile(file *os.File) (*Handle, error) {
|
||||
newFile, err := fdutils.DupFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("duplicate handle fd: %w", err)
|
||||
}
|
||||
return &Handle{inner: newFile}, nil
|
||||
}
|
||||
|
||||
// Open creates an "upgraded" file handle to the file referenced by the
|
||||
// [Handle]. Note that the original [Handle] is not consumed by this operation,
|
||||
// and can be opened multiple times.
|
||||
//
|
||||
// The handle returned is only usable for reading, and this is method is
|
||||
// shorthand for [Handle.OpenFile] with os.O_RDONLY.
|
||||
//
|
||||
// TODO: Rename these to "Reopen" or something.
|
||||
func (h *Handle) Open() (*os.File, error) {
|
||||
return h.OpenFile(os.O_RDONLY)
|
||||
}
|
||||
|
||||
// OpenFile creates an "upgraded" file handle to the file referenced by the
|
||||
// [Handle]. Note that the original [Handle] is not consumed by this operation,
|
||||
// and can be opened multiple times.
|
||||
//
|
||||
// The provided flags indicate which open(2) flags are used to create the new
|
||||
// handle.
|
||||
//
|
||||
// TODO: Rename these to "Reopen" or something.
|
||||
func (h *Handle) OpenFile(flags int) (*os.File, error) {
|
||||
return fdutils.WithFileFd(h.inner, func(fd uintptr) (*os.File, error) {
|
||||
newFd, err := libpathrs.Reopen(fd, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.NewFile(newFd, h.inner.Name()), nil
|
||||
})
|
||||
}
|
||||
|
||||
// IntoFile unwraps the [Handle] into its underlying [os.File].
|
||||
//
|
||||
// You almost certainly want to use [Handle.OpenFile] to get a non-O_PATH
|
||||
// version of this [Handle].
|
||||
//
|
||||
// This operation returns the internal [os.File] of the [Handle] directly, so
|
||||
// calling [Handle.Close] will also close any copies of the returned [os.File].
|
||||
// If you want to get an independent copy, use [Handle.Clone] followed by
|
||||
// [Handle.IntoFile] on the cloned [Handle].
|
||||
//
|
||||
// [os.File]: https://pkg.go.dev/os#File
|
||||
func (h *Handle) 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 h.inner
|
||||
}
|
||||
|
||||
// Clone creates a copy of a [Handle], such that it has a separate lifetime to
|
||||
// the original (while referring to the same underlying file).
|
||||
func (h *Handle) Clone() (*Handle, error) {
|
||||
return HandleFromFile(h.inner)
|
||||
}
|
||||
|
||||
// Close frees all of the resources used by the [Handle].
|
||||
func (h *Handle) Close() error {
|
||||
return h.inner.Close()
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
//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 fdutils contains a few helper methods when dealing with *os.File and
|
||||
// file descriptors.
|
||||
package fdutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"cyphar.com/go-pathrs/internal/libpathrs"
|
||||
)
|
||||
|
||||
// DupFd makes a duplicate of the given fd.
|
||||
func DupFd(fd uintptr, name string) (*os.File, error) {
|
||||
newFd, err := unix.FcntlInt(fd, unix.F_DUPFD_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fcntl(F_DUPFD_CLOEXEC): %w", err)
|
||||
}
|
||||
return os.NewFile(uintptr(newFd), name), nil
|
||||
}
|
||||
|
||||
// WithFileFd is a more ergonomic wrapper around file.SyscallConn().Control().
|
||||
func WithFileFd[T any](file *os.File, fn func(fd uintptr) (T, error)) (T, error) {
|
||||
conn, err := file.SyscallConn()
|
||||
if err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
var (
|
||||
ret T
|
||||
innerErr error
|
||||
)
|
||||
if err := conn.Control(func(fd uintptr) {
|
||||
ret, innerErr = fn(fd)
|
||||
}); err != nil {
|
||||
return *new(T), err
|
||||
}
|
||||
return ret, innerErr
|
||||
}
|
||||
|
||||
// DupFile makes a duplicate of the given file.
|
||||
func DupFile(file *os.File) (*os.File, error) {
|
||||
return WithFileFd(file, func(fd uintptr) (*os.File, error) {
|
||||
return DupFd(fd, file.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// MkFile creates a new *os.File from the provided file descriptor. However,
|
||||
// unlike os.NewFile, the file's Name is based on the real path (provided by
|
||||
// /proc/self/fd/$n).
|
||||
func MkFile(fd uintptr) (*os.File, error) {
|
||||
fdPath := fmt.Sprintf("fd/%d", fd)
|
||||
fdName, err := libpathrs.ProcReadlinkat(libpathrs.ProcDefaultRootFd, libpathrs.ProcThreadSelf, fdPath)
|
||||
if err != nil {
|
||||
_ = unix.Close(int(fd))
|
||||
return nil, fmt.Errorf("failed to fetch real name of fd %d: %w", fd, err)
|
||||
}
|
||||
// TODO: Maybe we should prefix this name with something to indicate to
|
||||
// users that they must not use this path as a "safe" path. Something like
|
||||
// "//pathrs-handle:/foo/bar"?
|
||||
return os.NewFile(fd, fdName), nil
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
//go:build linux
|
||||
|
||||
// TODO: Use "go:build unix" once we bump the minimum Go version 1.19.
|
||||
|
||||
// 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 libpathrs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Error represents an underlying libpathrs error.
|
||||
type Error struct {
|
||||
description string
|
||||
errno syscall.Errno
|
||||
}
|
||||
|
||||
// Error returns a textual description of the error.
|
||||
func (err *Error) Error() string {
|
||||
return err.description
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error which was wrapped by this error (if
|
||||
// applicable).
|
||||
func (err *Error) Unwrap() error {
|
||||
if err.errno != 0 {
|
||||
return err.errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
//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 libpathrs is an internal thin wrapper around the libpathrs C API.
|
||||
package libpathrs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
// TODO: Figure out if we need to add support for linking against libpathrs
|
||||
// statically even if in dynamically linked builds in order to make
|
||||
// packaging a bit easier (using "-Wl,-Bstatic -lpathrs -Wl,-Bdynamic" or
|
||||
// "-l:pathrs.a").
|
||||
#cgo pkg-config: pathrs
|
||||
#include <pathrs.h>
|
||||
|
||||
// This is a workaround for unsafe.Pointer() not working for non-void pointers.
|
||||
char *cast_ptr(void *ptr) { return ptr; }
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func fetchError(errID C.int) error {
|
||||
if errID >= C.__PATHRS_MAX_ERR_VALUE {
|
||||
return nil
|
||||
}
|
||||
cErr := C.pathrs_errorinfo(errID)
|
||||
defer C.pathrs_errorinfo_free(cErr)
|
||||
|
||||
var err error
|
||||
if cErr != nil {
|
||||
err = &Error{
|
||||
errno: syscall.Errno(cErr.saved_errno),
|
||||
description: C.GoString(cErr.description),
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// OpenRoot wraps pathrs_open_root.
|
||||
func OpenRoot(path string) (uintptr, error) {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
fd := C.pathrs_open_root(cPath)
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
|
||||
// Reopen wraps pathrs_reopen.
|
||||
func Reopen(fd uintptr, flags int) (uintptr, error) {
|
||||
newFd := C.pathrs_reopen(C.int(fd), C.int(flags))
|
||||
return uintptr(newFd), fetchError(newFd)
|
||||
}
|
||||
|
||||
// InRootResolve wraps pathrs_inroot_resolve.
|
||||
func InRootResolve(rootFd uintptr, path string) (uintptr, error) {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
fd := C.pathrs_inroot_resolve(C.int(rootFd), cPath)
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
|
||||
// InRootResolveNoFollow wraps pathrs_inroot_resolve_nofollow.
|
||||
func InRootResolveNoFollow(rootFd uintptr, path string) (uintptr, error) {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
fd := C.pathrs_inroot_resolve_nofollow(C.int(rootFd), cPath)
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
|
||||
// InRootOpen wraps pathrs_inroot_open.
|
||||
func InRootOpen(rootFd uintptr, path string, flags int) (uintptr, error) {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
fd := C.pathrs_inroot_open(C.int(rootFd), cPath, C.int(flags))
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
|
||||
// InRootReadlink wraps pathrs_inroot_readlink.
|
||||
func InRootReadlink(rootFd uintptr, path string) (string, error) {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
size := 128
|
||||
for {
|
||||
linkBuf := make([]byte, size)
|
||||
n := C.pathrs_inroot_readlink(C.int(rootFd), cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf)))
|
||||
switch {
|
||||
case int(n) < C.__PATHRS_MAX_ERR_VALUE:
|
||||
return "", fetchError(n)
|
||||
case int(n) <= len(linkBuf):
|
||||
return string(linkBuf[:int(n)]), nil
|
||||
default:
|
||||
// The contents were truncated. Unlike readlinkat, pathrs returns
|
||||
// the size of the link when it checked. So use the returned size
|
||||
// as a basis for the reallocated size (but in order to avoid a DoS
|
||||
// where a magic-link is growing by a single byte each iteration,
|
||||
// make sure we are a fair bit larger).
|
||||
size += int(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InRootRmdir wraps pathrs_inroot_rmdir.
|
||||
func InRootRmdir(rootFd uintptr, path string) error {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
err := C.pathrs_inroot_rmdir(C.int(rootFd), cPath)
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// InRootUnlink wraps pathrs_inroot_unlink.
|
||||
func InRootUnlink(rootFd uintptr, path string) error {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
err := C.pathrs_inroot_unlink(C.int(rootFd), cPath)
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// InRootRemoveAll wraps pathrs_inroot_remove_all.
|
||||
func InRootRemoveAll(rootFd uintptr, path string) error {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
err := C.pathrs_inroot_remove_all(C.int(rootFd), cPath)
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// InRootCreat wraps pathrs_inroot_creat.
|
||||
func InRootCreat(rootFd uintptr, path string, flags int, mode uint32) (uintptr, error) {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
fd := C.pathrs_inroot_creat(C.int(rootFd), cPath, C.int(flags), C.uint(mode))
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
|
||||
// InRootRename wraps pathrs_inroot_rename.
|
||||
func InRootRename(rootFd uintptr, src, dst string, flags uint) error {
|
||||
cSrc := C.CString(src)
|
||||
defer C.free(unsafe.Pointer(cSrc))
|
||||
|
||||
cDst := C.CString(dst)
|
||||
defer C.free(unsafe.Pointer(cDst))
|
||||
|
||||
err := C.pathrs_inroot_rename(C.int(rootFd), cSrc, cDst, C.uint(flags))
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// InRootMkdir wraps pathrs_inroot_mkdir.
|
||||
func InRootMkdir(rootFd uintptr, path string, mode uint32) error {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
err := C.pathrs_inroot_mkdir(C.int(rootFd), cPath, C.uint(mode))
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// InRootMkdirAll wraps pathrs_inroot_mkdir_all.
|
||||
func InRootMkdirAll(rootFd uintptr, path string, mode uint32) (uintptr, error) {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
fd := C.pathrs_inroot_mkdir_all(C.int(rootFd), cPath, C.uint(mode))
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
|
||||
// InRootMknod wraps pathrs_inroot_mknod.
|
||||
func InRootMknod(rootFd uintptr, path string, mode uint32, dev uint64) error {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
err := C.pathrs_inroot_mknod(C.int(rootFd), cPath, C.uint(mode), C.dev_t(dev))
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// InRootSymlink wraps pathrs_inroot_symlink.
|
||||
func InRootSymlink(rootFd uintptr, path, target string) error {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
cTarget := C.CString(target)
|
||||
defer C.free(unsafe.Pointer(cTarget))
|
||||
|
||||
err := C.pathrs_inroot_symlink(C.int(rootFd), cPath, cTarget)
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// InRootHardlink wraps pathrs_inroot_hardlink.
|
||||
func InRootHardlink(rootFd uintptr, path, target string) error {
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
cTarget := C.CString(target)
|
||||
defer C.free(unsafe.Pointer(cTarget))
|
||||
|
||||
err := C.pathrs_inroot_hardlink(C.int(rootFd), cPath, cTarget)
|
||||
return fetchError(err)
|
||||
}
|
||||
|
||||
// ProcBase is pathrs_proc_base_t (uint64_t).
|
||||
type ProcBase C.pathrs_proc_base_t
|
||||
|
||||
// FIXME: We need to open-code the constants because CGo unfortunately will
|
||||
// implicitly convert any non-literal constants (i.e. those resolved using gcc)
|
||||
// to signed integers. See <https://github.com/golang/go/issues/39136> for some
|
||||
// more information on the underlying issue (though.
|
||||
const (
|
||||
// ProcRoot is PATHRS_PROC_ROOT.
|
||||
ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT
|
||||
// ProcSelf is PATHRS_PROC_SELF.
|
||||
ProcSelf ProcBase = 0xFFFF_FFFE_091D_5E1F // C.PATHRS_PROC_SELF
|
||||
// ProcThreadSelf is PATHRS_PROC_THREAD_SELF.
|
||||
ProcThreadSelf ProcBase = 0xFFFF_FFFE_3EAD_5E1F // C.PATHRS_PROC_THREAD_SELF
|
||||
|
||||
// ProcBaseTypeMask is __PATHRS_PROC_TYPE_MASK.
|
||||
ProcBaseTypeMask ProcBase = 0xFFFF_FFFF_0000_0000 // C.__PATHRS_PROC_TYPE_MASK
|
||||
// ProcBaseTypePid is __PATHRS_PROC_TYPE_PID.
|
||||
ProcBaseTypePid ProcBase = 0x8000_0000_0000_0000 // C.__PATHRS_PROC_TYPE_PID
|
||||
|
||||
// ProcDefaultRootFd is PATHRS_PROC_DEFAULT_ROOTFD.
|
||||
ProcDefaultRootFd = -int(syscall.EBADF) // C.PATHRS_PROC_DEFAULT_ROOTFD
|
||||
)
|
||||
|
||||
func assertEqual[T comparable](a, b T, msg string) {
|
||||
if a != b {
|
||||
panic(fmt.Sprintf("%s ((%T) %#v != (%T) %#v)", msg, a, a, b, b))
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the values above match the actual C values. Unfortunately, Go
|
||||
// only allows us to forcefully cast int64 to uint64 if you use a temporary
|
||||
// variable, which means we cannot do it in a const context and thus need to do
|
||||
// it at runtime (even though it is a check that fundamentally could be done at
|
||||
// compile-time)...
|
||||
func init() {
|
||||
var (
|
||||
actualProcRoot int64 = C.PATHRS_PROC_ROOT
|
||||
actualProcSelf int64 = C.PATHRS_PROC_SELF
|
||||
actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF
|
||||
)
|
||||
|
||||
assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT")
|
||||
assertEqual(ProcSelf, ProcBase(actualProcSelf), "PATHRS_PROC_SELF")
|
||||
assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF")
|
||||
|
||||
var (
|
||||
actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK
|
||||
actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID
|
||||
)
|
||||
|
||||
assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK")
|
||||
assertEqual(ProcBaseTypePid, ProcBase(actualProcBaseTypePid), "__PATHRS_PROC_TYPE_PID")
|
||||
|
||||
assertEqual(ProcDefaultRootFd, int(C.PATHRS_PROC_DEFAULT_ROOTFD), "PATHRS_PROC_DEFAULT_ROOTFD")
|
||||
}
|
||||
|
||||
// ProcPid reimplements the PROC_PID(x) conversion.
|
||||
func ProcPid(pid uint32) ProcBase { return ProcBaseTypePid | ProcBase(pid) }
|
||||
|
||||
// ProcOpenat wraps pathrs_proc_openat.
|
||||
func ProcOpenat(procRootFd int, base ProcBase, path string, flags int) (uintptr, error) {
|
||||
cBase := C.pathrs_proc_base_t(base)
|
||||
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
fd := C.pathrs_proc_openat(C.int(procRootFd), cBase, cPath, C.int(flags))
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
|
||||
// ProcReadlinkat wraps pathrs_proc_readlinkat.
|
||||
func ProcReadlinkat(procRootFd int, base ProcBase, path string) (string, error) {
|
||||
// TODO: See if we can unify this code with InRootReadlink.
|
||||
|
||||
cBase := C.pathrs_proc_base_t(base)
|
||||
|
||||
cPath := C.CString(path)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
size := 128
|
||||
for {
|
||||
linkBuf := make([]byte, size)
|
||||
n := C.pathrs_proc_readlinkat(
|
||||
C.int(procRootFd), cBase, cPath,
|
||||
C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf)))
|
||||
switch {
|
||||
case int(n) < C.__PATHRS_MAX_ERR_VALUE:
|
||||
return "", fetchError(n)
|
||||
case int(n) <= len(linkBuf):
|
||||
return string(linkBuf[:int(n)]), nil
|
||||
default:
|
||||
// The contents were truncated. Unlike readlinkat, pathrs returns
|
||||
// the size of the link when it checked. So use the returned size
|
||||
// as a basis for the reallocated size (but in order to avoid a DoS
|
||||
// where a magic-link is growing by a single byte each iteration,
|
||||
// make sure we are a fair bit larger).
|
||||
size += int(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ProcfsOpenHow is pathrs_procfs_open_how (struct).
|
||||
type ProcfsOpenHow C.pathrs_procfs_open_how
|
||||
|
||||
const (
|
||||
// ProcfsNewUnmasked is PATHRS_PROCFS_NEW_UNMASKED.
|
||||
ProcfsNewUnmasked = C.PATHRS_PROCFS_NEW_UNMASKED
|
||||
)
|
||||
|
||||
// Flags returns a pointer to the internal flags field to allow other packages
|
||||
// to modify structure fields that are internal due to Go's visibility model.
|
||||
func (how *ProcfsOpenHow) Flags() *C.uint64_t { return &how.flags }
|
||||
|
||||
// ProcfsOpen is pathrs_procfs_open (sizeof(*how) is passed automatically).
|
||||
func ProcfsOpen(how *ProcfsOpenHow) (uintptr, error) {
|
||||
fd := C.pathrs_procfs_open((*C.pathrs_procfs_open_how)(how), C.size_t(unsafe.Sizeof(*how)))
|
||||
return uintptr(fd), fetchError(fd)
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
//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 procfs provides a safe API for operating on /proc on Linux.
|
||||
package procfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"cyphar.com/go-pathrs/internal/fdutils"
|
||||
"cyphar.com/go-pathrs/internal/libpathrs"
|
||||
)
|
||||
|
||||
// ProcBase is used with [ProcReadlink] and related functions to indicate what
|
||||
// /proc subpath path operations should be done relative to.
|
||||
type ProcBase struct {
|
||||
inner libpathrs.ProcBase
|
||||
}
|
||||
|
||||
var (
|
||||
// ProcRoot indicates to use /proc. Note that this mode may be more
|
||||
// expensive because we have to take steps to try to avoid leaking unmasked
|
||||
// procfs handles, so you should use [ProcBaseSelf] if you can.
|
||||
ProcRoot = ProcBase{inner: libpathrs.ProcRoot}
|
||||
// ProcSelf indicates to use /proc/self. For most programs, this is the
|
||||
// standard choice.
|
||||
ProcSelf = ProcBase{inner: libpathrs.ProcSelf}
|
||||
// ProcThreadSelf indicates to use /proc/thread-self. In multi-threaded
|
||||
// programs where one thread has a different CLONE_FS, it is possible for
|
||||
// /proc/self to point the wrong thread and so /proc/thread-self may be
|
||||
// necessary.
|
||||
ProcThreadSelf = ProcBase{inner: libpathrs.ProcThreadSelf}
|
||||
)
|
||||
|
||||
// ProcPid returns a ProcBase which indicates to use /proc/$pid for the given
|
||||
// PID (or TID). Be aware that due to PID recycling, using this is generally
|
||||
// not safe except in certain circumstances. Namely:
|
||||
//
|
||||
// - PID 1 (the init process), as that PID cannot ever get recycled.
|
||||
// - Your current PID (though you should just use [ProcBaseSelf]).
|
||||
// - Your current TID if you have used [runtime.LockOSThread] (though you
|
||||
// should just use [ProcBaseThreadSelf]).
|
||||
// - PIDs of child processes (as long as you are sure that no other part of
|
||||
// your program incorrectly catches or ignores SIGCHLD, and that you do it
|
||||
// *before* you call wait(2)or any equivalent method that could reap
|
||||
// zombies).
|
||||
func ProcPid(pid int) ProcBase {
|
||||
if pid < 0 || pid >= 1<<31 {
|
||||
panic("invalid ProcBasePid value") // TODO: should this be an error?
|
||||
}
|
||||
return ProcBase{inner: libpathrs.ProcPid(uint32(pid))}
|
||||
}
|
||||
|
||||
// ThreadCloser is a callback that needs to be called when you are done
|
||||
// operating on an [os.File] fetched using [Handle.OpenThreadSelf].
|
||||
//
|
||||
// [os.File]: https://pkg.go.dev/os#File
|
||||
type ThreadCloser func()
|
||||
|
||||
// Handle is a wrapper around an *os.File handle to "/proc", which can be
|
||||
// used to do further procfs-related operations in a safe way.
|
||||
type Handle struct {
|
||||
inner *os.File
|
||||
}
|
||||
|
||||
// Close releases all internal resources for this [Handle].
|
||||
//
|
||||
// Note that if the handle is actually the global cached handle, this operation
|
||||
// is a no-op.
|
||||
func (proc *Handle) Close() error {
|
||||
var err error
|
||||
if proc.inner != nil {
|
||||
err = proc.inner.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// OpenOption is a configuration function passed as an argument to [Open].
|
||||
type OpenOption func(*libpathrs.ProcfsOpenHow) error
|
||||
|
||||
// UnmaskedProcRoot can be passed to [Open] to request an unmasked procfs
|
||||
// handle be created.
|
||||
//
|
||||
// procfs, err := procfs.OpenRoot(procfs.UnmaskedProcRoot)
|
||||
func UnmaskedProcRoot(how *libpathrs.ProcfsOpenHow) error {
|
||||
*how.Flags() |= libpathrs.ProcfsNewUnmasked
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open creates a new [Handle] to a safe "/proc", based on the passed
|
||||
// configuration options (in the form of a series of [OpenOption]s).
|
||||
func Open(opts ...OpenOption) (*Handle, error) {
|
||||
var how libpathrs.ProcfsOpenHow
|
||||
for _, opt := range opts {
|
||||
if err := opt(&how); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fd, err := libpathrs.ProcfsOpen(&how)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var procFile *os.File
|
||||
if int(fd) >= 0 {
|
||||
procFile = os.NewFile(fd, "/proc")
|
||||
}
|
||||
// TODO: Check that fd == PATHRS_PROC_DEFAULT_ROOTFD in the <0 case?
|
||||
return &Handle{inner: procFile}, nil
|
||||
}
|
||||
|
||||
// TODO: Switch to something fdutils.WithFileFd-like.
|
||||
func (proc *Handle) fd() int {
|
||||
if proc.inner != nil {
|
||||
return int(proc.inner.Fd())
|
||||
}
|
||||
return libpathrs.ProcDefaultRootFd
|
||||
}
|
||||
|
||||
// TODO: Should we expose open?
|
||||
func (proc *Handle) open(base ProcBase, path string, flags int) (_ *os.File, Closer ThreadCloser, Err error) {
|
||||
var closer ThreadCloser
|
||||
if base == ProcThreadSelf {
|
||||
runtime.LockOSThread()
|
||||
closer = runtime.UnlockOSThread
|
||||
}
|
||||
defer func() {
|
||||
if closer != nil && Err != nil {
|
||||
closer()
|
||||
Closer = nil
|
||||
}
|
||||
}()
|
||||
|
||||
fd, err := libpathrs.ProcOpenat(proc.fd(), base.inner, path, flags)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
file, err := fdutils.MkFile(fd)
|
||||
return file, closer, err
|
||||
}
|
||||
|
||||
// OpenRoot safely opens a given path from inside /proc/.
|
||||
//
|
||||
// This function must only be used for accessing global information from procfs
|
||||
// (such as /proc/cpuinfo) or information about other processes (such as
|
||||
// /proc/1). Accessing your own process information should be done using
|
||||
// [Handle.OpenSelf] or [Handle.OpenThreadSelf].
|
||||
func (proc *Handle) OpenRoot(path string, flags int) (*os.File, error) {
|
||||
file, closer, err := proc.open(ProcRoot, path, flags)
|
||||
if closer != nil {
|
||||
// should not happen
|
||||
panic("non-zero closer returned from procOpen(ProcRoot)")
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
// OpenSelf safely opens a given path from inside /proc/self/.
|
||||
//
|
||||
// This method is recommend for getting process information about the current
|
||||
// process for almost all Go processes *except* for cases where there are
|
||||
// [runtime.LockOSThread] threads that have changed some aspect of their state
|
||||
// (such as through unshare(CLONE_FS) or changing namespaces).
|
||||
//
|
||||
// For such non-heterogeneous processes, /proc/self may reference to a task
|
||||
// that has different state from the current goroutine and so it may be
|
||||
// preferable to use [Handle.OpenThreadSelf]. The same is true if a user
|
||||
// really wants to inspect the current OS thread's information (such as
|
||||
// /proc/thread-self/stack or /proc/thread-self/status which is always uniquely
|
||||
// per-thread).
|
||||
//
|
||||
// Unlike [Handle.OpenThreadSelf], this method does not involve locking
|
||||
// the goroutine to the current OS thread and so is simpler to use and
|
||||
// theoretically has slightly less overhead.
|
||||
//
|
||||
// [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread
|
||||
func (proc *Handle) OpenSelf(path string, flags int) (*os.File, error) {
|
||||
file, closer, err := proc.open(ProcSelf, path, flags)
|
||||
if closer != nil {
|
||||
// should not happen
|
||||
panic("non-zero closer returned from procOpen(ProcSelf)")
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
// OpenPid safely opens a given path from inside /proc/$pid/, where pid can be
|
||||
// either a PID or TID.
|
||||
//
|
||||
// This is effectively equivalent to calling [Handle.OpenRoot] with the
|
||||
// pid prefixed to the subpath.
|
||||
//
|
||||
// Be aware that due to PID recycling, using this is generally not safe except
|
||||
// in certain circumstances. See the documentation of [ProcPid] for more
|
||||
// details.
|
||||
func (proc *Handle) OpenPid(pid int, path string, flags int) (*os.File, error) {
|
||||
file, closer, err := proc.open(ProcPid(pid), path, flags)
|
||||
if closer != nil {
|
||||
// should not happen
|
||||
panic("non-zero closer returned from procOpen(ProcPidOpen)")
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
// OpenThreadSelf safely opens a given path from inside /proc/thread-self/.
|
||||
//
|
||||
// Most Go processes have heterogeneous threads (all threads have most of the
|
||||
// same kernel state such as CLONE_FS) and so [Handle.OpenSelf] is
|
||||
// preferable for most users.
|
||||
//
|
||||
// For non-heterogeneous threads, or users that actually want thread-specific
|
||||
// information (such as /proc/thread-self/stack or /proc/thread-self/status),
|
||||
// this method is necessary.
|
||||
//
|
||||
// Because Go can change the running OS thread of your goroutine without notice
|
||||
// (and then subsequently kill the old thread), this method will lock the
|
||||
// current goroutine to the OS thread (with [runtime.LockOSThread]) and the
|
||||
// caller is responsible for unlocking the the OS thread with the
|
||||
// [ThreadCloser] callback once they are done using the returned file. This
|
||||
// callback MUST be called AFTER you have finished using the returned
|
||||
// [os.File]. This callback is completely separate to [os.File.Close], so it
|
||||
// must be called regardless of how you close the handle.
|
||||
//
|
||||
// [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread
|
||||
// [os.File]: https://pkg.go.dev/os#File
|
||||
// [os.File.Close]: https://pkg.go.dev/os#File.Close
|
||||
func (proc *Handle) OpenThreadSelf(path string, flags int) (*os.File, ThreadCloser, error) {
|
||||
return proc.open(ProcThreadSelf, path, flags)
|
||||
}
|
||||
|
||||
// Readlink safely reads the contents of a symlink from the given procfs base.
|
||||
//
|
||||
// This is effectively equivalent to doing an Open*(O_PATH|O_NOFOLLOW) of the
|
||||
// path and then doing unix.Readlinkat(fd, ""), but with the benefit that
|
||||
// thread locking is not necessary for [ProcThreadSelf].
|
||||
func (proc *Handle) Readlink(base ProcBase, path string) (string, error) {
|
||||
return libpathrs.ProcReadlinkat(proc.fd(), base.inner, path)
|
||||
}
|
||||
+367
@@ -0,0 +1,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()
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
//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 (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
//nolint:cyclop // this function needs to handle a lot of cases
|
||||
func toUnixMode(mode os.FileMode, needsType bool) (uint32, error) {
|
||||
sysMode := uint32(mode.Perm())
|
||||
switch mode & os.ModeType { //nolint:exhaustive // we only care about ModeType bits
|
||||
case 0:
|
||||
if needsType {
|
||||
sysMode |= unix.S_IFREG
|
||||
}
|
||||
case os.ModeDir:
|
||||
sysMode |= unix.S_IFDIR
|
||||
case os.ModeSymlink:
|
||||
sysMode |= unix.S_IFLNK
|
||||
case os.ModeCharDevice | os.ModeDevice:
|
||||
sysMode |= unix.S_IFCHR
|
||||
case os.ModeDevice:
|
||||
sysMode |= unix.S_IFBLK
|
||||
case os.ModeNamedPipe:
|
||||
sysMode |= unix.S_IFIFO
|
||||
case os.ModeSocket:
|
||||
sysMode |= unix.S_IFSOCK
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid mode filetype %+o", mode)
|
||||
}
|
||||
if mode&os.ModeSetuid != 0 {
|
||||
sysMode |= unix.S_ISUID
|
||||
}
|
||||
if mode&os.ModeSetgid != 0 {
|
||||
sysMode |= unix.S_ISGID
|
||||
}
|
||||
if mode&os.ModeSticky != 0 {
|
||||
sysMode |= unix.S_ISVTX
|
||||
}
|
||||
return sysMode, nil
|
||||
}
|
||||
+4
@@ -9,6 +9,10 @@
|
||||
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
build-tags:
|
||||
- libpathrs
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- asasalint
|
||||
|
||||
+60
-2
@@ -4,7 +4,64 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased 0.5.z] ##
|
||||
## [Unreleased] ##
|
||||
|
||||
## [0.6.0] - 2025-11-03 ##
|
||||
|
||||
> By the Power of Greyskull!
|
||||
|
||||
While quite small code-wise, this release marks a very key point in the
|
||||
development of filepath-securejoin.
|
||||
|
||||
filepath-securejoin was originally intended (back in 2017) to simply be a
|
||||
single-purpose library that would take some common code used in container
|
||||
runtimes (specifically, Docker's `FollowSymlinksInScope`) and make it more
|
||||
general-purpose (with the eventual goals of it ending up in the Go stdlib).
|
||||
|
||||
Of course, I quickly discovered that this problem was actually far more
|
||||
complicated to solve when dealing with racing attackers, which lead to me
|
||||
developing `openat2(2)` and [libpathrs][]. I had originally planned for
|
||||
libpathrs to completely replace filepath-securejoin "once it was ready" but in
|
||||
the interim we needed to fix several race attacks in runc as part of security
|
||||
advisories. Obviously we couldn't require the usage of a pre-0.1 Rust library
|
||||
in runc so it was necessary to port bits of libpathrs into filepath-securejoin.
|
||||
(Ironically the first prototypes of libpathrs were originally written in Go and
|
||||
then rewritten to Rust, so the code in filepath-securejoin is actually Go code
|
||||
that was rewritten to Rust then re-rewritten to Go.)
|
||||
|
||||
It then became clear that pure-Go libraries will likely not be willing to
|
||||
require CGo for all of their builds, so it was necessary to accept that
|
||||
filepath-securejoin will need to stay. As such, in v0.5.0 we provided more
|
||||
pure-Go implementations of features from libpathrs but moved them into
|
||||
`pathrs-lite` subpackage to clarify what purpose these helpers serve.
|
||||
|
||||
This release finally closes the loop and makes it so that pathrs-lite can
|
||||
transparently use libpathrs (via a `libpathrs` build-tag). This means that
|
||||
upstream libraries can use the pure Go version if they prefer, but downstreams
|
||||
(either downstream library users or even downstream distributions) are able to
|
||||
migrate to libpathrs for all usages of pathrs-lite in an entire Go binary.
|
||||
|
||||
I should make it clear that I do not plan to port the rest of libpathrs to Go,
|
||||
as I do not wish to maintain two copies of the same codebase. pathrs-lite
|
||||
already provides the core essentials necessary to operate on paths safely for
|
||||
most modern systems. Users who want additional hardening or more ergonomic APIs
|
||||
are free to use [`cyphar.com/go-pathrs`][go-pathrs] (libpathrs's Go bindings).
|
||||
|
||||
[libpathrs]: https://github.com/cyphar/libpathrs
|
||||
[go-pathrs]: https://cyphar.com/go-pathrs
|
||||
|
||||
### Breaking ###
|
||||
- The deprecated `MkdirAll`, `MkdirAllHandle`, `OpenInRoot`, `OpenatInRoot` and
|
||||
`Reopen` wrappers have been removed. Please switch to using `pathrs-lite`
|
||||
directly.
|
||||
|
||||
### Added ###
|
||||
- `pathrs-lite` now has support for using [libpathrs][libpathrs] as a backend.
|
||||
This is opt-in and can be enabled at build time with the `libpathrs` build
|
||||
tag. The intention is to allow for downstream libraries and other projects to
|
||||
make use of the pure-Go `github.com/cyphar/filepath-securejoin/pathrs-lite`
|
||||
package and distributors can then opt-in to using `libpathrs` for the entire
|
||||
binary if they wish.
|
||||
|
||||
## [0.5.1] - 2025-10-31 ##
|
||||
|
||||
@@ -383,7 +440,8 @@ This is our first release of `github.com/cyphar/filepath-securejoin`,
|
||||
containing a full implementation with a coverage of 93.5% (the only missing
|
||||
cases are the error cases, which are hard to mocktest at the moment).
|
||||
|
||||
[Unreleased 0.5.z]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...release-0.5
|
||||
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.6.0...HEAD
|
||||
[0.6.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.1...v0.6.0
|
||||
[0.5.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0
|
||||
[0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
0.5.1
|
||||
0.6.0
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 securejoin
|
||||
|
||||
import (
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite"
|
||||
)
|
||||
|
||||
var (
|
||||
// MkdirAll is a wrapper around [pathrs.MkdirAll].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.MkdirAll] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
MkdirAll = pathrs.MkdirAll
|
||||
|
||||
// MkdirAllHandle is a wrapper around [pathrs.MkdirAllHandle].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.MkdirAllHandle] directly instead.
|
||||
// This wrapper will be removed in filepath-securejoin v0.6.
|
||||
MkdirAllHandle = pathrs.MkdirAllHandle
|
||||
|
||||
// OpenInRoot is a wrapper around [pathrs.OpenInRoot].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.OpenInRoot] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
OpenInRoot = pathrs.OpenInRoot
|
||||
|
||||
// OpenatInRoot is a wrapper around [pathrs.OpenatInRoot].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.OpenatInRoot] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
OpenatInRoot = pathrs.OpenatInRoot
|
||||
|
||||
// Reopen is a wrapper around [pathrs.Reopen].
|
||||
//
|
||||
// Deprecated: You should use [pathrs.Reopen] directly instead. This
|
||||
// wrapper will be removed in filepath-securejoin v0.6.
|
||||
Reopen = pathrs.Reopen
|
||||
)
|
||||
+7
-5
@@ -5,11 +5,13 @@ Go** implementation of the core bits of [libpathrs][]. This is not intended to
|
||||
be a complete replacement for libpathrs, instead it is mainly intended to be
|
||||
useful as a transition tool for existing Go projects.
|
||||
|
||||
The long-term plan for `pathrs-lite` is to provide a build tag that will cause
|
||||
all `pathrs-lite` operations to call into libpathrs directly, thus removing
|
||||
code duplication for projects that wish to make use of libpathrs (and providing
|
||||
the ability for software packagers to opt-in to libpathrs support without
|
||||
needing to patch upstream).
|
||||
`pathrs-lite` also provides a very easy way to switch to `libpathrs` (even for
|
||||
downstreams where `pathrs-lite` is being used in a third-party package and is
|
||||
not interested in using CGo). At build time, if you use the `libpathrs` build
|
||||
tag then `pathrs-lite` will use `libpathrs` directly instead of the pure Go
|
||||
implementation. The two backends are functionally equivalent (and we have
|
||||
integration tests to verify this), so this migration should be very easy with
|
||||
no user-visible impact.
|
||||
|
||||
[libpathrs]: https://github.com/cyphar/libpathrs
|
||||
|
||||
|
||||
+2
@@ -11,4 +11,6 @@
|
||||
|
||||
// Package pathrs (pathrs-lite) is a less complete pure Go implementation of
|
||||
// some of the APIs provided by [libpathrs].
|
||||
//
|
||||
// [libpathrs]: https://github.com/cyphar/libpathrs
|
||||
package pathrs
|
||||
|
||||
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 gopathrs is a less complete pure Go implementation of some of the
|
||||
// APIs provided by [libpathrs].
|
||||
//
|
||||
// [libpathrs]: https://github.com/cyphar/libpathrs
|
||||
package gopathrs
|
||||
Generated
Vendored
+3
-3
@@ -9,7 +9,7 @@
|
||||
// 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
|
||||
package gopathrs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -166,11 +166,11 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
|
||||
return tailEntry.dir, tailEntry.remainingPath, true
|
||||
}
|
||||
|
||||
// partialLookupInRoot tries to lookup as much of the request path as possible
|
||||
// PartialLookupInRoot tries to lookup as much of the request path as possible
|
||||
// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
|
||||
// component of the requested path, returning a file handle to the final
|
||||
// existing component and a string containing the remaining path components.
|
||||
func partialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) {
|
||||
func PartialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) {
|
||||
return lookupInRoot(root, unsafePath, true)
|
||||
}
|
||||
|
||||
Generated
Vendored
+10
-44
@@ -9,7 +9,7 @@
|
||||
// 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
|
||||
package gopathrs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -23,9 +23,12 @@ import (
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
||||
)
|
||||
|
||||
var errInvalidMode = errors.New("invalid permission mode")
|
||||
// ErrInvalidMode is returned from [MkdirAll] when the requested mode is
|
||||
// invalid.
|
||||
var ErrInvalidMode = errors.New("invalid permission mode")
|
||||
|
||||
// modePermExt is like os.ModePerm except that it also includes the set[ug]id
|
||||
// and sticky bits.
|
||||
@@ -45,11 +48,11 @@ func toUnixMode(mode os.FileMode) (uint32, error) {
|
||||
}
|
||||
// We don't allow file type bits.
|
||||
if mode&os.ModeType != 0 {
|
||||
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", errInvalidMode, mode, mode)
|
||||
return 0, fmt.Errorf("%w %+.3o (%s): type bits not permitted", ErrInvalidMode, mode, mode)
|
||||
}
|
||||
// We don't allow other unknown modes.
|
||||
if mode&^modePermExt != 0 || sysMode&unix.S_IFMT != 0 {
|
||||
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", errInvalidMode, mode, mode)
|
||||
return 0, fmt.Errorf("%w %+.3o (%s): unknown mode bits", ErrInvalidMode, mode, mode)
|
||||
}
|
||||
return sysMode, nil
|
||||
}
|
||||
@@ -84,11 +87,11 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||
// users it seems more prudent to return an error so users notice that
|
||||
// these bits will not be set.
|
||||
if unixMode&^0o1777 != 0 {
|
||||
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
|
||||
return nil, fmt.Errorf("%w for mkdir %+.3o: suid and sgid are ignored by mkdir", ErrInvalidMode, mode)
|
||||
}
|
||||
|
||||
// Try to open as much of the path as possible.
|
||||
currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath)
|
||||
currentDir, remainingPath, err := PartialLookupInRoot(root, unsafePath)
|
||||
defer func() {
|
||||
if Err != nil {
|
||||
_ = currentDir.Close()
|
||||
@@ -117,7 +120,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||
// Re-open the path to match the O_DIRECTORY reopen loop later (so that we
|
||||
// always return a non-O_PATH handle). We also check that we actually got a
|
||||
// directory.
|
||||
if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
|
||||
if reopenDir, err := procfs.ReopenFd(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
|
||||
return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
|
||||
@@ -207,40 +210,3 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||
}
|
||||
return currentDir, nil
|
||||
}
|
||||
|
||||
// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
|
||||
// where the new directory is guaranteed to be within the root directory (if an
|
||||
// attacker can move directories from inside the root to outside the root, the
|
||||
// created directory tree might be outside of the root but the key constraint
|
||||
// is that at no point will we walk outside of the directory tree we are
|
||||
// creating).
|
||||
//
|
||||
// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
|
||||
//
|
||||
// path, _ := securejoin.SecureJoin(root, unsafePath)
|
||||
// err := os.MkdirAll(path, mode)
|
||||
//
|
||||
// But is much safer. The above implementation is unsafe because if an attacker
|
||||
// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
|
||||
// possible for MkdirAll to resolve unsafe symlink components and create
|
||||
// directories outside of the root.
|
||||
//
|
||||
// If you plan to open the directory after you have created it or want to use
|
||||
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
|
||||
// This function is a wrapper around [MkdirAllHandle].
|
||||
//
|
||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
||||
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
|
||||
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = f.Close()
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 gopathrs
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
||||
// using an *[os.File] handle, to ensure that the correct root directory is used.
|
||||
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||
handle, err := completeLookupInRoot(root, unsafePath)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
Generated
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
||||
// 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
|
||||
package gopathrs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
|
||||
// where the new directory is guaranteed to be within the root directory (if an
|
||||
// attacker can move directories from inside the root to outside the root, the
|
||||
// created directory tree might be outside of the root but the key constraint
|
||||
// is that at no point will we walk outside of the directory tree we are
|
||||
// creating).
|
||||
//
|
||||
// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
|
||||
//
|
||||
// path, _ := securejoin.SecureJoin(root, unsafePath)
|
||||
// err := os.MkdirAll(path, mode)
|
||||
//
|
||||
// But is much safer. The above implementation is unsafe because if an attacker
|
||||
// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
|
||||
// possible for MkdirAll to resolve unsafe symlink components and create
|
||||
// directories outside of the root.
|
||||
//
|
||||
// If you plan to open the directory after you have created it or want to use
|
||||
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
|
||||
// This function is a wrapper around [MkdirAllHandle].
|
||||
//
|
||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
||||
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
|
||||
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = f.Close()
|
||||
return nil
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build libpathrs
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 (
|
||||
"os"
|
||||
|
||||
"cyphar.com/go-pathrs"
|
||||
)
|
||||
|
||||
// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
|
||||
// in two respects:
|
||||
//
|
||||
// - The caller provides the root directory as an *[os.File] (preferably O_PATH)
|
||||
// handle. This means that the caller can be sure which root directory is
|
||||
// being used. Note that this can be emulated by using /proc/self/fd/... as
|
||||
// the root path with [os.MkdirAll].
|
||||
//
|
||||
// - Once all of the directories have been created, an *[os.File] O_PATH handle
|
||||
// to the directory at unsafePath is returned to the caller. This is done in
|
||||
// an effectively-race-free way (an attacker would only be able to swap the
|
||||
// final directory component), which is not possible to emulate with
|
||||
// [MkdirAll].
|
||||
//
|
||||
// In addition, the returned handle is obtained far more efficiently than doing
|
||||
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
|
||||
// doing [MkdirAll]. If you intend to open the directory after creating it, you
|
||||
// should use MkdirAllHandle.
|
||||
//
|
||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
||||
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error) {
|
||||
rootRef, err := pathrs.RootFromFile(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rootRef.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
handle, err := rootRef.MkdirAll(unsafePath, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handle.IntoFile(), nil
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux && !libpathrs
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 (
|
||||
"os"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs"
|
||||
)
|
||||
|
||||
// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
|
||||
// in two respects:
|
||||
//
|
||||
// - The caller provides the root directory as an *[os.File] (preferably O_PATH)
|
||||
// handle. This means that the caller can be sure which root directory is
|
||||
// being used. Note that this can be emulated by using /proc/self/fd/... as
|
||||
// the root path with [os.MkdirAll].
|
||||
//
|
||||
// - Once all of the directories have been created, an *[os.File] O_PATH handle
|
||||
// to the directory at unsafePath is returned to the caller. This is done in
|
||||
// an effectively-race-free way (an attacker would only be able to swap the
|
||||
// final directory component), which is not possible to emulate with
|
||||
// [MkdirAll].
|
||||
//
|
||||
// In addition, the returned handle is obtained far more efficiently than doing
|
||||
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
|
||||
// doing [MkdirAll]. If you intend to open the directory after creating it, you
|
||||
// should use MkdirAllHandle.
|
||||
//
|
||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
||||
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (*os.File, error) {
|
||||
return gopathrs.MkdirAllHandle(root, unsafePath, mode)
|
||||
}
|
||||
Generated
Vendored
-29
@@ -15,20 +15,8 @@ import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
||||
)
|
||||
|
||||
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
||||
// using an *[os.File] handle, to ensure that the correct root directory is used.
|
||||
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||
handle, err := completeLookupInRoot(root, unsafePath)
|
||||
if err != nil {
|
||||
return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// OpenInRoot safely opens the provided unsafePath within the root.
|
||||
// Effectively, OpenInRoot(root, unsafePath) is equivalent to
|
||||
//
|
||||
@@ -55,20 +43,3 @@ func OpenInRoot(root, unsafePath string) (*os.File, error) {
|
||||
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
|
||||
return OpenatInRoot(rootDir, unsafePath)
|
||||
}
|
||||
|
||||
// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
|
||||
// Reopen(file, flags) is effectively equivalent to
|
||||
//
|
||||
// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
|
||||
// os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
|
||||
//
|
||||
// But with some extra hardenings to ensure that we are not tricked by a
|
||||
// maliciously-configured /proc mount. While this attack scenario is not
|
||||
// common, in container runtimes it is possible for higher-level runtimes to be
|
||||
// tricked into configuring an unsafe /proc that can be used to attack file
|
||||
// operations. See [CVE-2019-19921] for more details.
|
||||
//
|
||||
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
|
||||
func Reopen(handle *os.File, flags int) (*os.File, error) {
|
||||
return procfs.ReopenFd(handle, flags)
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build libpathrs
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 (
|
||||
"os"
|
||||
|
||||
"cyphar.com/go-pathrs"
|
||||
)
|
||||
|
||||
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
||||
// using an *[os.File] handle, to ensure that the correct root directory is used.
|
||||
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||
rootRef, err := pathrs.RootFromFile(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rootRef.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
handle, err := rootRef.Resolve(unsafePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return handle.IntoFile(), nil
|
||||
}
|
||||
|
||||
// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
|
||||
// Reopen(file, flags) is effectively equivalent to
|
||||
//
|
||||
// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
|
||||
// os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
|
||||
//
|
||||
// But with some extra hardenings to ensure that we are not tricked by a
|
||||
// maliciously-configured /proc mount. While this attack scenario is not
|
||||
// common, in container runtimes it is possible for higher-level runtimes to be
|
||||
// tricked into configuring an unsafe /proc that can be used to attack file
|
||||
// operations. See [CVE-2019-19921] for more details.
|
||||
//
|
||||
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
|
||||
func Reopen(file *os.File, flags int) (*os.File, error) {
|
||||
handle, err := pathrs.HandleFromFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer handle.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
return handle.OpenFile(flags)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux && !libpathrs
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 (
|
||||
"os"
|
||||
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs"
|
||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
||||
)
|
||||
|
||||
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
||||
// using an *[os.File] handle, to ensure that the correct root directory is used.
|
||||
func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||
return gopathrs.OpenatInRoot(root, unsafePath)
|
||||
}
|
||||
|
||||
// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
|
||||
// Reopen(file, flags) is effectively equivalent to
|
||||
//
|
||||
// fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
|
||||
// os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
|
||||
//
|
||||
// But with some extra hardenings to ensure that we are not tricked by a
|
||||
// maliciously-configured /proc mount. While this attack scenario is not
|
||||
// common, in container runtimes it is possible for higher-level runtimes to be
|
||||
// tricked into configuring an unsafe /proc that can be used to attack file
|
||||
// operations. See [CVE-2019-19921] for more details.
|
||||
//
|
||||
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
|
||||
func Reopen(handle *os.File, flags int) (*os.File, error) {
|
||||
return procfs.ReopenFd(handle, flags)
|
||||
}
|
||||
Generated
Vendored
+161
@@ -0,0 +1,161 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build libpathrs
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-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 procfs provides a safe API for operating on /proc on Linux.
|
||||
package procfs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"cyphar.com/go-pathrs/procfs"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ProcThreadSelfCloser is a callback that needs to be called when you are done
|
||||
// operating on an [os.File] fetched using [Handle.OpenThreadSelf].
|
||||
//
|
||||
// [os.File]: https://pkg.go.dev/os#File
|
||||
type ProcThreadSelfCloser = procfs.ThreadCloser
|
||||
|
||||
// Handle is a wrapper around an *os.File handle to "/proc", which can be used
|
||||
// to do further procfs-related operations in a safe way.
|
||||
type Handle struct {
|
||||
inner *procfs.Handle
|
||||
}
|
||||
|
||||
// Close close the resources associated with this [Handle]. Note that if this
|
||||
// [Handle] was created with [OpenProcRoot], on some kernels the underlying
|
||||
// procfs handle is cached and so this Close operation may be a no-op. However,
|
||||
// you should always call Close on [Handle]s once you are done with them.
|
||||
func (proc *Handle) Close() error { return proc.inner.Close() }
|
||||
|
||||
// OpenProcRoot tries to open a "safer" handle to "/proc" (i.e., one with the
|
||||
// "subset=pid" mount option applied, available from Linux 5.8). Unless you
|
||||
// plan to do many [Handle.OpenRoot] operations, users should prefer to use
|
||||
// this over [OpenUnsafeProcRoot] which is far more dangerous to keep open.
|
||||
//
|
||||
// If a safe handle cannot be opened, OpenProcRoot will fall back to opening a
|
||||
// regular "/proc" handle.
|
||||
//
|
||||
// Note that using [Handle.OpenRoot] will still work with handles returned by
|
||||
// this function. If a subpath cannot be operated on with a safe "/proc"
|
||||
// handle, then [OpenUnsafeProcRoot] will be called internally and a temporary
|
||||
// unsafe handle will be used.
|
||||
func OpenProcRoot() (*Handle, error) {
|
||||
proc, err := procfs.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Handle{inner: proc}, nil
|
||||
}
|
||||
|
||||
// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or
|
||||
// masked paths. You must be extremely careful to make sure this handle is
|
||||
// never leaked to a container and that you program cannot be tricked into
|
||||
// writing to arbitrary paths within it.
|
||||
//
|
||||
// This is not necessary if you just wish to use [Handle.OpenRoot], as handles
|
||||
// returned by [OpenProcRoot] will fall back to using a *temporary* unsafe
|
||||
// handle in that case. You should only really use this if you need to do many
|
||||
// operations with [Handle.OpenRoot] and the performance overhead of making
|
||||
// many procfs handles is an issue. If you do use OpenUnsafeProcRoot, you
|
||||
// should make sure to close the handle as soon as possible to avoid
|
||||
// known-fd-number attacks.
|
||||
func OpenUnsafeProcRoot() (*Handle, error) {
|
||||
proc, err := procfs.Open(procfs.UnmaskedProcRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Handle{inner: proc}, nil
|
||||
}
|
||||
|
||||
// OpenThreadSelf returns a handle to "/proc/thread-self/<subpath>" (or an
|
||||
// equivalent handle on older kernels where "/proc/thread-self" doesn't exist).
|
||||
// Once finished with the handle, you must call the returned closer function
|
||||
// ([runtime.UnlockOSThread]). You must not pass the returned *os.File to other
|
||||
// Go threads or use the handle after calling the closer.
|
||||
//
|
||||
// [runtime.UnlockOSThread]: https://pkg.go.dev/runtime#UnlockOSThread
|
||||
func (proc *Handle) OpenThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) {
|
||||
return proc.inner.OpenThreadSelf(subpath, unix.O_PATH|unix.O_NOFOLLOW)
|
||||
}
|
||||
|
||||
// OpenSelf returns a handle to /proc/self/<subpath>.
|
||||
//
|
||||
// Note that in Go programs with non-homogenous threads, this may result in
|
||||
// spurious errors. If you are monkeying around with APIs that are
|
||||
// thread-specific, you probably want to use [Handle.OpenThreadSelf] instead
|
||||
// which will guarantee that the handle refers to the same thread as the caller
|
||||
// is executing on.
|
||||
func (proc *Handle) OpenSelf(subpath string) (*os.File, error) {
|
||||
return proc.inner.OpenSelf(subpath, unix.O_PATH|unix.O_NOFOLLOW)
|
||||
}
|
||||
|
||||
// OpenRoot returns a handle to /proc/<subpath>.
|
||||
//
|
||||
// You should only use this when you need to operate on global procfs files
|
||||
// (such as sysctls in /proc/sys). Unlike [Handle.OpenThreadSelf],
|
||||
// [Handle.OpenSelf], and [Handle.OpenPid], the procfs handle used internally
|
||||
// for this operation will never use "subset=pid", which makes it a more juicy
|
||||
// target for [CVE-2024-21626]-style attacks (and doing something like opening
|
||||
// a directory with OpenRoot effectively leaks [OpenUnsafeProcRoot] as long as
|
||||
// the file descriptor is open).
|
||||
//
|
||||
// [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
|
||||
func (proc *Handle) OpenRoot(subpath string) (*os.File, error) {
|
||||
return proc.inner.OpenRoot(subpath, unix.O_PATH|unix.O_NOFOLLOW)
|
||||
}
|
||||
|
||||
// OpenPid returns a handle to /proc/$pid/<subpath> (pid can be a pid or tid).
|
||||
// This is mainly intended for usage when operating on other processes.
|
||||
//
|
||||
// You should not use this for the current thread, as special handling is
|
||||
// needed for /proc/thread-self (or /proc/self/task/<tid>) when dealing with
|
||||
// goroutine scheduling -- use [Handle.OpenThreadSelf] instead.
|
||||
//
|
||||
// To refer to the current thread-group, you should use prefer
|
||||
// [Handle.OpenSelf] to passing os.Getpid as the pid argument.
|
||||
func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) {
|
||||
return proc.inner.OpenPid(pid, subpath, unix.O_PATH|unix.O_NOFOLLOW)
|
||||
}
|
||||
|
||||
// ProcSelfFdReadlink gets the real path of the given file by looking at
|
||||
// /proc/self/fd/<fd> with [readlink]. It is effectively just shorthand for
|
||||
// something along the lines of:
|
||||
//
|
||||
// proc, err := procfs.OpenProcRoot()
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// link, err := proc.OpenThreadSelf(fmt.Sprintf("fd/%d", f.Fd()))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer link.Close()
|
||||
// var buf [4096]byte
|
||||
// n, err := unix.Readlinkat(int(link.Fd()), "", buf[:])
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// pathname := buf[:n]
|
||||
//
|
||||
// [readlink]: https://pkg.go.dev/golang.org/x/sys/unix#Readlinkat
|
||||
func ProcSelfFdReadlink(f *os.File) (string, error) {
|
||||
proc, err := procfs.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer proc.Close() //nolint:errcheck // close failures aren't critical here
|
||||
|
||||
fdPath := "fd/" + strconv.Itoa(int(f.Fd()))
|
||||
return proc.Readlink(procfs.ProcThreadSelf, fdPath)
|
||||
}
|
||||
Generated
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build linux
|
||||
//go:build linux && !libpathrs
|
||||
|
||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
||||
// Copyright (C) 2024-2025 SUSE LLC
|
||||
+12
-16
@@ -273,8 +273,8 @@ func writeConFd(out *os.File, val string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// openProcThreadSelf is a small wrapper around [OpenThreadSelf] and
|
||||
// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
|
||||
// openProcThreadSelf is a small wrapper around [procfs.Handle.OpenThreadSelf]
|
||||
// and [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
|
||||
// provided mode must be os.O_* flags to indicate what mode the returned file
|
||||
// should be opened with (flags like os.O_CREAT and os.O_EXCL are not
|
||||
// supported).
|
||||
@@ -283,8 +283,6 @@ func writeConFd(out *os.File, val string) error {
|
||||
// /proc/thread-self/<subpath> with no tricky mounts or symlinks causing you to
|
||||
// operate on an unexpected path (with some caveats on pre-openat2 or
|
||||
// pre-fsopen kernels).
|
||||
//
|
||||
// [OpenThreadSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenThreadSelf
|
||||
func openProcThreadSelf(subpath string, mode int) (*os.File, procfs.ProcThreadSelfCloser, error) {
|
||||
if subpath == "" {
|
||||
return nil, nil, ErrEmptyPath
|
||||
@@ -340,17 +338,16 @@ func writeConThreadSelf(fpath, val string) error {
|
||||
return writeConFd(out, val)
|
||||
}
|
||||
|
||||
// openProcSelf is a small wrapper around [OpenSelf] and [pathrs.Reopen] to
|
||||
// make "one-shot opens" slightly more ergonomic. The provided mode must be
|
||||
// os.O_* flags to indicate what mode the returned file should be opened with
|
||||
// (flags like os.O_CREAT and os.O_EXCL are not supported).
|
||||
// openProcSelf is a small wrapper around [procfs.Handle.OpenSelf] and
|
||||
// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
|
||||
// provided mode must be os.O_* flags to indicate what mode the returned file
|
||||
// should be opened with (flags like os.O_CREAT and os.O_EXCL are not
|
||||
// supported).
|
||||
//
|
||||
// If no error occurred, the returned handle is guaranteed to be exactly
|
||||
// /proc/self/<subpath> with no tricky mounts or symlinks causing you to
|
||||
// operate on an unexpected path (with some caveats on pre-openat2 or
|
||||
// pre-fsopen kernels).
|
||||
//
|
||||
// [OpenSelf]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenSelf
|
||||
func openProcSelf(subpath string, mode int) (*os.File, error) {
|
||||
if subpath == "" {
|
||||
return nil, ErrEmptyPath
|
||||
@@ -403,17 +400,16 @@ func writeConSelf(fpath, val string) error {
|
||||
return writeConFd(out, val)
|
||||
}
|
||||
|
||||
// openProcPid is a small wrapper around [OpenPid] and [pathrs.Reopen] to make
|
||||
// "one-shot opens" slightly more ergonomic. The provided mode must be os.O_*
|
||||
// flags to indicate what mode the returned file should be opened with (flags
|
||||
// like os.O_CREAT and os.O_EXCL are not supported).
|
||||
// openProcPid is a small wrapper around [procfs.Handle.OpenPid] and
|
||||
// [pathrs.Reopen] to make "one-shot opens" slightly more ergonomic. The
|
||||
// provided mode must be os.O_* flags to indicate what mode the returned file
|
||||
// should be opened with (flags like os.O_CREAT and os.O_EXCL are not
|
||||
// supported).
|
||||
//
|
||||
// If no error occurred, the returned handle is guaranteed to be exactly
|
||||
// /proc/self/<subpath> with no tricky mounts or symlinks causing you to
|
||||
// operate on an unexpected path (with some caveats on pre-openat2 or
|
||||
// pre-fsopen kernels).
|
||||
//
|
||||
// [OpenPid]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle.OpenPid
|
||||
func openProcPid(pid int, subpath string, mode int) (*os.File, error) {
|
||||
if subpath == "" {
|
||||
return nil, ErrEmptyPath
|
||||
|
||||
-4
@@ -3,10 +3,6 @@
|
||||
|
||||
package selinux
|
||||
|
||||
func attrPath(string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func readConThreadSelf(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
Vendored
+9
-3
@@ -1,3 +1,9 @@
|
||||
# cyphar.com/go-pathrs v0.2.1
|
||||
## explicit; go 1.18
|
||||
cyphar.com/go-pathrs
|
||||
cyphar.com/go-pathrs/internal/fdutils
|
||||
cyphar.com/go-pathrs/internal/libpathrs
|
||||
cyphar.com/go-pathrs/procfs
|
||||
# github.com/checkpoint-restore/go-criu/v6 v6.3.0
|
||||
## explicit; go 1.16
|
||||
github.com/checkpoint-restore/go-criu/v6
|
||||
@@ -27,7 +33,7 @@ github.com/coreos/go-systemd/v22/dbus
|
||||
# github.com/cpuguy83/go-md2man/v2 v2.0.5
|
||||
## explicit; go 1.11
|
||||
github.com/cpuguy83/go-md2man/v2/md2man
|
||||
# github.com/cyphar/filepath-securejoin v0.5.1
|
||||
# github.com/cyphar/filepath-securejoin v0.6.0
|
||||
## explicit; go 1.18
|
||||
github.com/cyphar/filepath-securejoin
|
||||
github.com/cyphar/filepath-securejoin/internal/consts
|
||||
@@ -36,6 +42,7 @@ github.com/cyphar/filepath-securejoin/pathrs-lite/internal
|
||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert
|
||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd
|
||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat
|
||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gopathrs
|
||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion
|
||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux
|
||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs
|
||||
@@ -76,7 +83,7 @@ github.com/opencontainers/cgroups/systemd
|
||||
## explicit
|
||||
github.com/opencontainers/runtime-spec/specs-go
|
||||
github.com/opencontainers/runtime-spec/specs-go/features
|
||||
# github.com/opencontainers/selinux v1.12.0 => ./internal/third_party/selinux
|
||||
# github.com/opencontainers/selinux v1.13.0
|
||||
## explicit; go 1.19
|
||||
github.com/opencontainers/selinux/go-selinux
|
||||
github.com/opencontainers/selinux/go-selinux/label
|
||||
@@ -137,4 +144,3 @@ google.golang.org/protobuf/reflect/protoreflect
|
||||
google.golang.org/protobuf/reflect/protoregistry
|
||||
google.golang.org/protobuf/runtime/protoiface
|
||||
google.golang.org/protobuf/runtime/protoimpl
|
||||
# github.com/opencontainers/selinux => ./internal/third_party/selinux
|
||||
|
||||
Reference in New Issue
Block a user