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 commit
ed6b1693b8 ("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>
This commit is contained in:
Aleksa Sarai
2025-11-08 02:10:48 +11:00
parent 846835cce9
commit 96f1962f91
67 changed files with 2240 additions and 4909 deletions
+3 -6
View File
@@ -153,12 +153,9 @@ jobs:
- name: no toolchain in go.mod # See https://github.com/opencontainers/runc/pull/4717, https://github.com/dependabot/dependabot-core/issues/11933.
run: |
if grep -q '^toolchain ' go.mod; then echo "Error: go.mod must not have toolchain directive, please fix"; exit 1; fi
# FIXME: This check needed to be disabled for the go-selinux patch addded
# when patching CVE-2025-52881. This needs to be removed as soon as
# the embargo is lifted, along with the replace directive in go.mod.
#- name: no exclude nor replace in go.mod
# run: |
# if grep -Eq '^\s*(exclude|replace) ' go.mod; then echo "Error: go.mod must not have exclude/replace directive, it breaks go install. Please fix"; exit 1; fi
- name: no exclude nor replace in go.mod
run: |
if grep -Eq '^\s*(exclude|replace) ' go.mod; then echo "Error: go.mod must not have exclude/replace directive, it breaks go install. Please fix"; exit 1; fi
commit:
+3 -7
View File
@@ -6,7 +6,7 @@ require (
github.com/checkpoint-restore/go-criu/v7 v7.2.0
github.com/containerd/console v1.0.5
github.com/coreos/go-systemd/v22 v22.6.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.5
github.com/opencontainers/runtime-spec v1.2.2-0.20250818071321-383cadbf08c0
github.com/opencontainers/selinux v1.12.0
github.com/opencontainers/selinux v1.13.0
github.com/seccomp/libseccomp-golang v0.11.1
github.com/sirupsen/logrus v1.9.3
github.com/urfave/cli v1.22.17
@@ -28,12 +28,8 @@ 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.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // 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
+8 -3
View File
@@ -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.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/checkpoint-restore/go-criu/v7 v7.2.0 h1:qGiWA4App1gGlEfIJ68WR9jbezV9J7yZdjzglezcqKo=
github.com/checkpoint-restore/go-criu/v7 v7.2.0/go.mod h1:u0LCWLg0w4yqqu14aXhiB4YD3a1qd8EcCEg7vda5dwo=
@@ -9,8 +11,8 @@ github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5z
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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=
@@ -48,6 +50,8 @@ github.com/opencontainers/cgroups v0.0.5 h1:DRITAqcOnY0uSBzIpt1RYWLjh5DPDiqUs4fY
github.com/opencontainers/cgroups v0.0.5/go.mod h1:oWVzJsKK0gG9SCRBfTpnn16WcGEqDI8PAcpMGbqWxcs=
github.com/opencontainers/runtime-spec v1.2.2-0.20250818071321-383cadbf08c0 h1:RLn0YfUWkiqPGtgUANvJrcjIkCHGRl3jcz/c557M28M=
github.com/opencontainers/runtime-spec v1.2.2-0.20250818071321-383cadbf08c0/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=
@@ -66,8 +70,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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.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.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
-2
View File
@@ -1,2 +0,0 @@
[codespell]
skip = ./.git,./go.sum,./go-selinux/testdata
-10
View File
@@ -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
View File
@@ -1 +0,0 @@
build
-44
View File
@@ -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
View File
@@ -1 +0,0 @@
* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah
-119
View File
@@ -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`.
-201
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,23 +0,0 @@
# selinux
[![GoDoc](https://godoc.org/github.com/opencontainers/selinux?status.svg)](https://godoc.org/github.com/opencontainers/selinux) [![Go Report Card](https://goreportcard.com/badge/github.com/opencontainers/selinux)](https://goreportcard.com/report/github.com/opencontainers/selinux) [![Build Status](https://travis-ci.org/opencontainers/selinux.svg?branch=master)](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
View File
@@ -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
-48
View File
@@ -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
View File
@@ -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()
}
}
-159
View File
@@ -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)
}
}
-71
View File
@@ -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
}
}
}
-8
View File
@@ -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
)
-8
View File
@@ -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=
-52
View File
@@ -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
View File
@@ -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
}
-236
View File
@@ -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
}
-56
View File
@@ -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.
-123
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -9,6 +9,10 @@
version: "2"
run:
build-tags:
- libpathrs
linters:
enable:
- asasalint
+60 -2
View File
@@ -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
View File
@@ -1 +1 @@
0.5.1
0.6.0
-48
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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
View File
@@ -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
}
@@ -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
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
@@ -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
View File
@@ -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
View File
@@ -3,10 +3,6 @@
package selinux
func attrPath(string) string {
return ""
}
func readConThreadSelf(string) (string, error) {
return "", nil
}
+9 -3
View File
@@ -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/v7 v7.2.0
## explicit; go 1.20
github.com/checkpoint-restore/go-criu/v7
@@ -27,7 +33,7 @@ github.com/coreos/go-systemd/v22/dbus
# github.com/cpuguy83/go-md2man/v2 v2.0.7
## explicit; go 1.12
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