1
0
mirror of https://github.com/opencontainers/runc.git synced 2026-02-06 03:45:41 +01:00

selinux: use safe procfs API for labels

Due to the sensitive nature of these fixes, it was not possible to
submit these upstream and vendor the upstream library. Instead, this
patch uses a fork of github.com/opencontainers/selinux, branched at
commit opencontainers/selinux@879a755db5.

In order to permit downstreams to build with this patched version, a
snapshot of the forked version has been included in
internal/third_party/selinux. Note that since we use "go mod vendor",
the patched code is usable even without being "go get"-able. Once the
embargo for this issue is lifted we can submit the patches upstream and
switch back to a proper upstream go.mod entry.

Also, this requires us to temporarily disable the CI job we have that
disallows "replace" directives.

Fixes: GHSA-cgrx-mc8f-2prm CVE-2025-52881
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
This commit is contained in:
Aleksa Sarai
2025-10-07 22:48:50 +11:00
parent d40b3439a9
commit ed6b1693b8
40 changed files with 4964 additions and 113 deletions

View File

@@ -152,9 +152,12 @@ 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
- 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
# 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
commit:

5
go.mod
View File

@@ -32,3 +32,8 @@ require (
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

2
go.sum
View File

@@ -48,8 +48,6 @@ 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.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8=
github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U=
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=

View File

@@ -0,0 +1,2 @@
[codespell]
skip = ./.git,./go.sum,./go-selinux/testdata

View File

@@ -0,0 +1,10 @@
# 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"

View File

@@ -0,0 +1,163 @@
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"

View File

@@ -0,0 +1 @@
build

View File

@@ -0,0 +1,44 @@
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

View File

@@ -0,0 +1 @@
* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah

View File

@@ -0,0 +1,119 @@
## 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
internal/third_party/selinux/LICENSE vendored Normal file
View File

@@ -0,0 +1,201 @@
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.

View File

@@ -0,0 +1,5 @@
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
internal/third_party/selinux/Makefile vendored Normal file
View File

@@ -0,0 +1,37 @@
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
internal/third_party/selinux/README.md vendored Normal file
View File

@@ -0,0 +1,23 @@
# 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

View File

@@ -0,0 +1,13 @@
/*
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

View File

@@ -0,0 +1,48 @@
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
}

View File

@@ -0,0 +1,136 @@
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")
}

View File

@@ -0,0 +1,130 @@
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)
}
}

View File

@@ -0,0 +1,44 @@
//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
}

View File

@@ -0,0 +1,76 @@
//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")
}
}

View File

@@ -0,0 +1,35 @@
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)
}
}

View File

@@ -0,0 +1,322 @@
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

View File

@@ -0,0 +1,711 @@
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()
}
}

View File

@@ -0,0 +1,159 @@
//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 ""
}

View File

@@ -0,0 +1,127 @@
//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)
}
}

View File

@@ -0,0 +1,71 @@
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
internal/third_party/selinux/go.mod vendored Normal file
View File

@@ -0,0 +1,8 @@
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
internal/third_party/selinux/go.sum vendored Normal file
View File

@@ -0,0 +1,8 @@
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=

View File

@@ -0,0 +1,52 @@
## 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.

View File

@@ -0,0 +1,131 @@
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
}

View File

@@ -0,0 +1,236 @@
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
}

View File

@@ -0,0 +1,56 @@
## 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.

View File

@@ -0,0 +1,123 @@
//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
}

View File

@@ -0,0 +1,239 @@
//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
}

View File

@@ -18,7 +18,7 @@ var validOptions = map[string]bool{
"level": true,
}
var ErrIncompatibleLabel = errors.New("Bad SELinux option z and Z can not be used together")
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
@@ -52,11 +52,11 @@ func InitLabels(options []string) (plabel string, mlabel string, retErr error) {
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)
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])
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]

View File

@@ -153,7 +153,7 @@ func CalculateGlbLub(sourceRange, targetRange string) (string, error) {
// 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 writeCon(attrPath("exec"), label)
return writeConThreadSelf("attr/exec", label)
}
// SetTaskLabel sets the SELinux label for the current thread, or an error.
@@ -161,7 +161,7 @@ func SetExecLabel(label string) error {
// 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 writeCon(attrPath("current"), label)
return writeConThreadSelf("attr/current", label)
}
// SetSocketLabel takes a process label and tells the kernel to assign the
@@ -170,12 +170,12 @@ func SetTaskLabel(label string) error {
// 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 writeCon(attrPath("sockcreate"), label)
return writeConThreadSelf("attr/sockcreate", label)
}
// SocketLabel retrieves the current socket label setting
func SocketLabel() (string, error) {
return readCon(attrPath("sockcreate"))
return readConThreadSelf("attr/sockcreate")
}
// PeerLabel retrieves the label of the client on the other side of a socket
@@ -198,7 +198,7 @@ func SetKeyLabel(label string) error {
// KeyLabel retrieves the current kernel keyring label setting
func KeyLabel() (string, error) {
return readCon("/proc/self/attr/keycreate")
return keyLabel()
}
// Get returns the Context as a string

View File

@@ -17,8 +17,11 @@ import (
"strings"
"sync"
"github.com/opencontainers/selinux/pkg/pwalkdir"
"github.com/cyphar/filepath-securejoin/pathrs-lite"
"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
"golang.org/x/sys/unix"
"github.com/opencontainers/selinux/pkg/pwalkdir"
)
const (
@@ -73,10 +76,6 @@ var (
mcsList: make(map[string]bool),
}
// for attrPath()
attrPathOnce sync.Once
haveThreadSelf bool
// for policyRoot()
policyRootOnce sync.Once
policyRootVal string
@@ -256,42 +255,6 @@ func readConfig(target string) string {
return ""
}
func isProcHandle(fh *os.File) error {
var buf unix.Statfs_t
for {
err := unix.Fstatfs(int(fh.Fd()), &buf)
if err == nil {
break
}
if err != unix.EINTR {
return &os.PathError{Op: "fstatfs", Path: fh.Name(), Err: err}
}
}
if buf.Type != unix.PROC_SUPER_MAGIC {
return fmt.Errorf("file %q is not on procfs", fh.Name())
}
return nil
}
func readCon(fpath string) (string, error) {
if fpath == "" {
return "", ErrEmptyPath
}
in, err := os.Open(fpath)
if err != nil {
return "", err
}
defer in.Close()
if err := isProcHandle(in); err != nil {
return "", err
}
return readConFd(in)
}
func readConFd(in *os.File) (string, error) {
data, err := io.ReadAll(in)
if err != nil {
@@ -300,6 +263,181 @@ func readConFd(in *os.File) (string, error) {
return string(bytes.TrimSuffix(data, []byte{0})), nil
}
func writeConFd(out *os.File, val string) error {
var err error
if val != "" {
_, err = out.Write([]byte(val))
} else {
_, err = out.Write(nil)
}
return err
}
// openProcThreadSelf is a small wrapper around [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).
//
// If no error occurred, the returned handle is guaranteed to be exactly
// /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
}
proc, err := procfs.OpenProcRoot()
if err != nil {
return nil, nil, err
}
defer proc.Close()
handle, closer, err := proc.OpenThreadSelf(subpath)
if err != nil {
return nil, nil, fmt.Errorf("open /proc/thread-self/%s handle: %w", subpath, err)
}
defer handle.Close() // we will return a re-opened handle
file, err := pathrs.Reopen(handle, mode)
if err != nil {
closer()
return nil, nil, fmt.Errorf("reopen /proc/thread-self/%s handle (%#x): %w", subpath, mode, err)
}
return file, closer, nil
}
// Read the contents of /proc/thread-self/<fpath>.
func readConThreadSelf(fpath string) (string, error) {
in, closer, err := openProcThreadSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC)
if err != nil {
return "", err
}
defer closer()
defer in.Close()
return readConFd(in)
}
// Write <val> to /proc/thread-self/<fpath>.
func writeConThreadSelf(fpath, val string) error {
if val == "" {
if !getEnabled() {
return nil
}
}
out, closer, err := openProcThreadSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC)
if err != nil {
return err
}
defer closer()
defer out.Close()
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).
//
// 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
}
proc, err := procfs.OpenProcRoot()
if err != nil {
return nil, err
}
defer proc.Close()
handle, err := proc.OpenSelf(subpath)
if err != nil {
return nil, fmt.Errorf("open /proc/self/%s handle: %w", subpath, err)
}
defer handle.Close() // we will return a re-opened handle
file, err := pathrs.Reopen(handle, mode)
if err != nil {
return nil, fmt.Errorf("reopen /proc/self/%s handle (%#x): %w", subpath, mode, err)
}
return file, nil
}
// Read the contents of /proc/self/<fpath>.
func readConSelf(fpath string) (string, error) {
in, err := openProcSelf(fpath, os.O_RDONLY|unix.O_CLOEXEC)
if err != nil {
return "", err
}
defer in.Close()
return readConFd(in)
}
// Write <val> to /proc/self/<fpath>.
func writeConSelf(fpath, val string) error {
if val == "" {
if !getEnabled() {
return nil
}
}
out, err := openProcSelf(fpath, os.O_WRONLY|unix.O_CLOEXEC)
if err != nil {
return err
}
defer out.Close()
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).
//
// 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
}
proc, err := procfs.OpenProcRoot()
if err != nil {
return nil, err
}
defer proc.Close()
handle, err := proc.OpenPid(pid, subpath)
if err != nil {
return nil, fmt.Errorf("open /proc/%d/%s handle: %w", pid, subpath, err)
}
defer handle.Close() // we will return a re-opened handle
file, err := pathrs.Reopen(handle, mode)
if err != nil {
return nil, fmt.Errorf("reopen /proc/%d/%s handle (%#x): %w", pid, subpath, mode, err)
}
return file, nil
}
// classIndex returns the int index for an object class in the loaded policy,
// or -1 and an error
func classIndex(class string) (int, error) {
@@ -393,78 +531,34 @@ func lFileLabel(fpath string) (string, error) {
}
func setFSCreateLabel(label string) error {
return writeCon(attrPath("fscreate"), label)
return writeConThreadSelf("attr/fscreate", 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 readCon(attrPath("fscreate"))
return readConThreadSelf("attr/fscreate")
}
// currentLabel returns the SELinux label of the current process thread, or an error.
func currentLabel() (string, error) {
return readCon(attrPath("current"))
return readConThreadSelf("attr/current")
}
// pidLabel returns the SELinux label of the given pid, or an error.
func pidLabel(pid int) (string, error) {
return readCon(fmt.Sprintf("/proc/%d/attr/current", pid))
it, err := openProcPid(pid, "attr/current", os.O_RDONLY|unix.O_CLOEXEC)
if err != nil {
return "", nil
}
defer it.Close()
return readConFd(it)
}
// 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 readCon(attrPath("exec"))
}
func writeCon(fpath, val string) error {
if fpath == "" {
return ErrEmptyPath
}
if val == "" {
if !getEnabled() {
return nil
}
}
out, err := os.OpenFile(fpath, os.O_WRONLY, 0)
if err != nil {
return err
}
defer out.Close()
if err := isProcHandle(out); err != nil {
return err
}
if val != "" {
_, err = out.Write([]byte(val))
} else {
_, err = out.Write(nil)
}
if err != nil {
return err
}
return nil
}
func attrPath(attr string) string {
// Linux >= 3.17 provides this
const threadSelfPrefix = "/proc/thread-self/attr"
attrPathOnce.Do(func() {
st, err := os.Stat(threadSelfPrefix)
if err == nil && st.Mode().IsDir() {
haveThreadSelf = true
}
})
if haveThreadSelf {
return filepath.Join(threadSelfPrefix, attr)
}
return filepath.Join("/proc/self/task", strconv.Itoa(unix.Gettid()), "attr", attr)
return readConThreadSelf("exec")
}
// canonicalizeContext takes a context string and writes it to the kernel
@@ -728,19 +822,29 @@ func peerLabel(fd uintptr) (string, error) {
// setKeyLabel takes a process label and tells the kernel to assign the
// label to the next kernel keyring that gets created
func setKeyLabel(label string) error {
err := writeCon("/proc/self/attr/keycreate", label)
// Rather than using /proc/thread-self, we want to use /proc/self to
// operate on the thread-group leader.
err := writeConSelf("attr/keycreate", label)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if label == "" && errors.Is(err, os.ErrPermission) {
return nil
}
if errors.Is(err, unix.EACCES) && unix.Getuid() != unix.Gettid() {
if errors.Is(err, unix.EACCES) && unix.Getpid() != unix.Gettid() {
return ErrNotTGLeader
}
return err
}
// KeyLabel retrieves the current kernel keyring label setting for this
// thread-group.
func keyLabel() (string, error) {
// Rather than using /proc/thread-self, we want to use /proc/self to
// operate on the thread-group leader.
return readConSelf("attr/keycreate")
}
// get returns the Context as a string
func (c Context) get() string {
if l := c["level"]; l != "" {

View File

@@ -7,11 +7,11 @@ func attrPath(string) string {
return ""
}
func readCon(string) (string, error) {
func readConThreadSelf(string) (string, error) {
return "", nil
}
func writeCon(string, string) error {
func writeConThreadSelf(string, string) error {
return nil
}
@@ -81,6 +81,10 @@ func setKeyLabel(string) error {
return nil
}
func keyLabel() (string, error) {
return "", nil
}
func (c Context) get() string {
return ""
}

3
vendor/modules.txt vendored
View File

@@ -76,7 +76,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
# github.com/opencontainers/selinux v1.12.0 => ./internal/third_party/selinux
## explicit; go 1.19
github.com/opencontainers/selinux/go-selinux
github.com/opencontainers/selinux/go-selinux/label
@@ -137,3 +137,4 @@ 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