diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c085a9193..971705fe0 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -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: diff --git a/go.mod b/go.mod index 723c66db4..ee82b16c0 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 3a0158440..f417a8278 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/third_party/selinux/.codespellrc b/internal/third_party/selinux/.codespellrc new file mode 100644 index 000000000..8f0866a3e --- /dev/null +++ b/internal/third_party/selinux/.codespellrc @@ -0,0 +1,2 @@ +[codespell] +skip = ./.git,./go.sum,./go-selinux/testdata diff --git a/internal/third_party/selinux/.github/dependabot.yml b/internal/third_party/selinux/.github/dependabot.yml new file mode 100644 index 000000000..b534a2b9f --- /dev/null +++ b/internal/third_party/selinux/.github/dependabot.yml @@ -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" diff --git a/internal/third_party/selinux/.github/workflows/validate.yml b/internal/third_party/selinux/.github/workflows/validate.yml new file mode 100644 index 000000000..fab1cb494 --- /dev/null +++ b/internal/third_party/selinux/.github/workflows/validate.yml @@ -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" diff --git a/internal/third_party/selinux/.gitignore b/internal/third_party/selinux/.gitignore new file mode 100644 index 000000000..378eac25d --- /dev/null +++ b/internal/third_party/selinux/.gitignore @@ -0,0 +1 @@ +build diff --git a/internal/third_party/selinux/.golangci.yml b/internal/third_party/selinux/.golangci.yml new file mode 100644 index 000000000..b1b989251 --- /dev/null +++ b/internal/third_party/selinux/.golangci.yml @@ -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 diff --git a/internal/third_party/selinux/CODEOWNERS b/internal/third_party/selinux/CODEOWNERS new file mode 100644 index 000000000..14392178a --- /dev/null +++ b/internal/third_party/selinux/CODEOWNERS @@ -0,0 +1 @@ +* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah diff --git a/internal/third_party/selinux/CONTRIBUTING.md b/internal/third_party/selinux/CONTRIBUTING.md new file mode 100644 index 000000000..dc3ff6a51 --- /dev/null +++ b/internal/third_party/selinux/CONTRIBUTING.md @@ -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 + +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`. diff --git a/internal/third_party/selinux/LICENSE b/internal/third_party/selinux/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/internal/third_party/selinux/LICENSE @@ -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. diff --git a/internal/third_party/selinux/MAINTAINERS b/internal/third_party/selinux/MAINTAINERS new file mode 100644 index 000000000..748c18b4c --- /dev/null +++ b/internal/third_party/selinux/MAINTAINERS @@ -0,0 +1,5 @@ +Antonio Murdaca (@runcom) +Daniel J Walsh (@rhatdan) +Mrunal Patel (@mrunalp) +Sebastiaan van Stijn (@thaJeztah) +Kirill Kolyshikin (@kolyshkin) diff --git a/internal/third_party/selinux/Makefile b/internal/third_party/selinux/Makefile new file mode 100644 index 000000000..f7b9c3dac --- /dev/null +++ b/internal/third_party/selinux/Makefile @@ -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 diff --git a/internal/third_party/selinux/README.md b/internal/third_party/selinux/README.md new file mode 100644 index 000000000..cd6a60f80 --- /dev/null +++ b/internal/third_party/selinux/README.md @@ -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 diff --git a/internal/third_party/selinux/go-selinux/doc.go b/internal/third_party/selinux/go-selinux/doc.go new file mode 100644 index 000000000..57a15c9a1 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/doc.go @@ -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 diff --git a/internal/third_party/selinux/go-selinux/label/label.go b/internal/third_party/selinux/go-selinux/label/label.go new file mode 100644 index 000000000..884a8b805 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/label/label.go @@ -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 +} diff --git a/internal/third_party/selinux/go-selinux/label/label_linux.go b/internal/third_party/selinux/go-selinux/label/label_linux.go new file mode 100644 index 000000000..95f29e21f --- /dev/null +++ b/internal/third_party/selinux/go-selinux/label/label_linux.go @@ -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") +} diff --git a/internal/third_party/selinux/go-selinux/label/label_linux_test.go b/internal/third_party/selinux/go-selinux/label/label_linux_test.go new file mode 100644 index 000000000..e25ead795 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/label/label_linux_test.go @@ -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) + } +} diff --git a/internal/third_party/selinux/go-selinux/label/label_stub.go b/internal/third_party/selinux/go-selinux/label/label_stub.go new file mode 100644 index 000000000..7a54afc5e --- /dev/null +++ b/internal/third_party/selinux/go-selinux/label/label_stub.go @@ -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 +} diff --git a/internal/third_party/selinux/go-selinux/label/label_stub_test.go b/internal/third_party/selinux/go-selinux/label/label_stub_test.go new file mode 100644 index 000000000..e92cc8b94 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/label/label_stub_test.go @@ -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") + } +} diff --git a/internal/third_party/selinux/go-selinux/label/label_test.go b/internal/third_party/selinux/go-selinux/label/label_test.go new file mode 100644 index 000000000..fb172f3ff --- /dev/null +++ b/internal/third_party/selinux/go-selinux/label/label_test.go @@ -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) + } +} diff --git a/internal/third_party/selinux/go-selinux/selinux.go b/internal/third_party/selinux/go-selinux/selinux.go new file mode 100644 index 000000000..15150d475 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/selinux.go @@ -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/ 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 +} diff --git a/internal/third_party/selinux/go-selinux/selinux_linux.go b/internal/third_party/selinux/go-selinux/selinux_linux.go new file mode 100644 index 000000000..70392d989 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/selinux_linux.go @@ -0,0 +1,1405 @@ +package selinux + +import ( + "bufio" + "bytes" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "io/fs" + "math/big" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + "sync" + + "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 ( + minSensLen = 2 + contextFile = "/usr/share/containers/selinux/contexts" + selinuxDir = "/etc/selinux/" + selinuxUsersDir = "contexts/users" + defaultContexts = "contexts/default_contexts" + selinuxConfig = selinuxDir + "config" + selinuxfsMount = "/sys/fs/selinux" + selinuxTypeTag = "SELINUXTYPE" + selinuxTag = "SELINUX" + xattrNameSelinux = "security.selinux" +) + +type selinuxState struct { + mcsList map[string]bool + selinuxfs string + selinuxfsOnce sync.Once + enabledSet bool + enabled bool + sync.Mutex +} + +type level struct { + cats *big.Int + sens int +} + +type mlsRange struct { + low *level + high *level +} + +type defaultSECtx struct { + userRdr io.Reader + verifier func(string) error + defaultRdr io.Reader + user, level, scon string +} + +type levelItem byte + +const ( + sensitivity levelItem = 's' + category levelItem = 'c' +) + +var ( + readOnlyFileLabel string + state = selinuxState{ + mcsList: make(map[string]bool), + } + + // for policyRoot() + policyRootOnce sync.Once + policyRootVal string + + // for label() + loadLabelsOnce sync.Once + labels map[string]string +) + +func policyRoot() string { + policyRootOnce.Do(func() { + policyRootVal = filepath.Join(selinuxDir, readConfig(selinuxTypeTag)) + }) + + return policyRootVal +} + +func (s *selinuxState) setEnable(enabled bool) bool { + s.Lock() + defer s.Unlock() + s.enabledSet = true + s.enabled = enabled + return s.enabled +} + +func (s *selinuxState) getEnabled() bool { + s.Lock() + enabled := s.enabled + enabledSet := s.enabledSet + s.Unlock() + if enabledSet { + return enabled + } + + enabled = false + if fs := getSelinuxMountPoint(); fs != "" { + if con, _ := CurrentLabel(); con != "kernel" { + enabled = true + } + } + return s.setEnable(enabled) +} + +// setDisabled disables SELinux support for the package +func setDisabled() { + state.setEnable(false) +} + +func verifySELinuxfsMount(mnt string) bool { + var buf unix.Statfs_t + for { + err := unix.Statfs(mnt, &buf) + if err == nil { + break + } + if err == unix.EAGAIN || err == unix.EINTR { + continue + } + return false + } + + //#nosec G115 -- there is no overflow here. + if uint32(buf.Type) != uint32(unix.SELINUX_MAGIC) { + return false + } + if (buf.Flags & unix.ST_RDONLY) != 0 { + return false + } + + return true +} + +func findSELinuxfs() string { + // fast path: check the default mount first + if verifySELinuxfsMount(selinuxfsMount) { + return selinuxfsMount + } + + // check if selinuxfs is available before going the slow path + fs, err := os.ReadFile("/proc/filesystems") + if err != nil { + return "" + } + if !bytes.Contains(fs, []byte("\tselinuxfs\n")) { + return "" + } + + // slow path: try to find among the mounts + f, err := os.Open("/proc/self/mountinfo") + if err != nil { + return "" + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for { + mnt := findSELinuxfsMount(scanner) + if mnt == "" { // error or not found + return "" + } + if verifySELinuxfsMount(mnt) { + return mnt + } + } +} + +// findSELinuxfsMount returns a next selinuxfs mount point found, +// if there is one, or an empty string in case of EOF or error. +func findSELinuxfsMount(s *bufio.Scanner) string { + for s.Scan() { + txt := s.Bytes() + // The first field after - is fs type. + // Safe as spaces in mountpoints are encoded as \040 + if !bytes.Contains(txt, []byte(" - selinuxfs ")) { + continue + } + const mPos = 5 // mount point is 5th field + fields := bytes.SplitN(txt, []byte(" "), mPos+1) + if len(fields) < mPos+1 { + continue + } + return string(fields[mPos-1]) + } + + return "" +} + +func (s *selinuxState) getSELinuxfs() string { + s.selinuxfsOnce.Do(func() { + s.selinuxfs = findSELinuxfs() + }) + + return s.selinuxfs +} + +// getSelinuxMountPoint returns the path to the mountpoint of an selinuxfs +// filesystem or an empty string if no mountpoint is found. Selinuxfs is +// a proc-like pseudo-filesystem that exposes the SELinux policy API to +// processes. The existence of an selinuxfs mount is used to determine +// whether SELinux is currently enabled or not. +func getSelinuxMountPoint() string { + return state.getSELinuxfs() +} + +// getEnabled returns whether SELinux is currently enabled. +func getEnabled() bool { + return state.getEnabled() +} + +func readConfig(target string) string { + in, err := os.Open(selinuxConfig) + if err != nil { + return "" + } + defer in.Close() + + scanner := bufio.NewScanner(in) + + for scanner.Scan() { + line := bytes.TrimSpace(scanner.Bytes()) + if len(line) == 0 { + // Skip blank lines + continue + } + if line[0] == ';' || line[0] == '#' { + // Skip comments + continue + } + fields := bytes.SplitN(line, []byte{'='}, 2) + if len(fields) != 2 { + continue + } + if bytes.Equal(fields[0], []byte(target)) { + return string(bytes.Trim(fields[1], `"`)) + } + } + return "" +} + +func readConFd(in *os.File) (string, error) { + data, err := io.ReadAll(in) + if err != nil { + return "", err + } + 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/ 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/. +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 to /proc/thread-self/. +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/ 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/. +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 to /proc/self/. +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/ 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) { + permpath := fmt.Sprintf("class/%s/index", class) + indexpath := filepath.Join(getSelinuxMountPoint(), permpath) + + indexB, err := os.ReadFile(indexpath) + if err != nil { + return -1, err + } + index, err := strconv.Atoi(string(indexB)) + if err != nil { + return -1, err + } + + return index, nil +} + +// lSetFileLabel sets the SELinux label for this path, not following symlinks, +// or returns an error. +func lSetFileLabel(fpath string, label string) error { + if fpath == "" { + return ErrEmptyPath + } + for { + err := unix.Lsetxattr(fpath, xattrNameSelinux, []byte(label), 0) + if err == nil { + break + } + if err != unix.EINTR { + return &os.PathError{Op: fmt.Sprintf("lsetxattr(label=%s)", label), Path: fpath, Err: err} + } + } + + return nil +} + +// setFileLabel sets the SELinux label for this path, following symlinks, +// or returns an error. +func setFileLabel(fpath string, label string) error { + if fpath == "" { + return ErrEmptyPath + } + for { + err := unix.Setxattr(fpath, xattrNameSelinux, []byte(label), 0) + if err == nil { + break + } + if err != unix.EINTR { + return &os.PathError{Op: fmt.Sprintf("setxattr(label=%s)", label), Path: fpath, Err: err} + } + } + + return nil +} + +// fileLabel returns the SELinux label for this path, following symlinks, +// or returns an error. +func fileLabel(fpath string) (string, error) { + if fpath == "" { + return "", ErrEmptyPath + } + + label, err := getxattr(fpath, xattrNameSelinux) + if err != nil { + return "", &os.PathError{Op: "getxattr", Path: fpath, Err: err} + } + // Trim the NUL byte at the end of the byte buffer, if present. + if len(label) > 0 && label[len(label)-1] == '\x00' { + label = label[:len(label)-1] + } + return string(label), nil +} + +// lFileLabel returns the SELinux label for this path, not following symlinks, +// or returns an error. +func lFileLabel(fpath string) (string, error) { + if fpath == "" { + return "", ErrEmptyPath + } + + label, err := lgetxattr(fpath, xattrNameSelinux) + if err != nil { + return "", &os.PathError{Op: "lgetxattr", Path: fpath, Err: err} + } + // Trim the NUL byte at the end of the byte buffer, if present. + if len(label) > 0 && label[len(label)-1] == '\x00' { + label = label[:len(label)-1] + } + return string(label), nil +} + +func setFSCreateLabel(label string) error { + 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 readConThreadSelf("attr/fscreate") +} + +// currentLabel returns the SELinux label of the current process thread, or an error. +func currentLabel() (string, error) { + return readConThreadSelf("attr/current") +} + +// pidLabel returns the SELinux label of the given pid, or an error. +func pidLabel(pid int) (string, error) { + 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 readConThreadSelf("exec") +} + +// 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 readWriteCon(filepath.Join(getSelinuxMountPoint(), "context"), 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) { + classidx, err := classIndex(class) + if err != nil { + return "", err + } + + return readWriteCon(filepath.Join(getSelinuxMountPoint(), "create"), fmt.Sprintf("%s %s %d", source, target, classidx)) +} + +// catsToBitset stores categories in a bitset. +func catsToBitset(cats string) (*big.Int, error) { + bitset := new(big.Int) + + catlist := strings.Split(cats, ",") + for _, r := range catlist { + ranges := strings.SplitN(r, ".", 2) + if len(ranges) > 1 { + catstart, err := parseLevelItem(ranges[0], category) + if err != nil { + return nil, err + } + catend, err := parseLevelItem(ranges[1], category) + if err != nil { + return nil, err + } + for i := catstart; i <= catend; i++ { + bitset.SetBit(bitset, i, 1) + } + } else { + cat, err := parseLevelItem(ranges[0], category) + if err != nil { + return nil, err + } + bitset.SetBit(bitset, cat, 1) + } + } + + return bitset, nil +} + +// parseLevelItem parses and verifies that a sensitivity or category are valid +func parseLevelItem(s string, sep levelItem) (int, error) { + if len(s) < minSensLen || levelItem(s[0]) != sep { + return 0, ErrLevelSyntax + } + const bitSize = 31 // Make sure the result fits into signed int32. + val, err := strconv.ParseUint(s[1:], 10, bitSize) + if err != nil { + return 0, err + } + + return int(val), nil +} + +// parseLevel fills a level from a string that contains +// a sensitivity and categories +func (l *level) parseLevel(levelStr string) error { + lvl := strings.SplitN(levelStr, ":", 2) + sens, err := parseLevelItem(lvl[0], sensitivity) + if err != nil { + return fmt.Errorf("failed to parse sensitivity: %w", err) + } + l.sens = sens + if len(lvl) > 1 { + cats, err := catsToBitset(lvl[1]) + if err != nil { + return fmt.Errorf("failed to parse categories: %w", err) + } + l.cats = cats + } + + return nil +} + +// rangeStrToMLSRange marshals a string representation of a range. +func rangeStrToMLSRange(rangeStr string) (*mlsRange, error) { + r := &mlsRange{} + l := strings.SplitN(rangeStr, "-", 2) + + switch len(l) { + // rangeStr that has a low and a high level, e.g. s4:c0.c1023-s6:c0.c1023 + case 2: + r.high = &level{} + if err := r.high.parseLevel(l[1]); err != nil { + return nil, fmt.Errorf("failed to parse high level %q: %w", l[1], err) + } + fallthrough + // rangeStr that is single level, e.g. s6:c0,c3,c5,c30.c1023 + case 1: + r.low = &level{} + if err := r.low.parseLevel(l[0]); err != nil { + return nil, fmt.Errorf("failed to parse low level %q: %w", l[0], err) + } + } + + if r.high == nil { + r.high = r.low + } + + return r, nil +} + +// bitsetToStr takes a category bitset and returns it in the +// canonical selinux syntax +func bitsetToStr(c *big.Int) string { + var str string + + length := 0 + i0 := int(c.TrailingZeroBits()) //#nosec G115 -- don't expect TralingZeroBits to return values with highest bit set. + for i := i0; i < c.BitLen(); i++ { + if c.Bit(i) == 0 { + continue + } + if length == 0 { + if str != "" { + str += "," + } + str += "c" + strconv.Itoa(i) + } + if c.Bit(i+1) == 1 { + length++ + continue + } + if length == 1 { + str += ",c" + strconv.Itoa(i) + } else if length > 1 { + str += ".c" + strconv.Itoa(i) + } + length = 0 + } + + return str +} + +func (l *level) equal(l2 *level) bool { + if l2 == nil || l == nil { + return l == l2 + } + if l2.sens != l.sens { + return false + } + if l2.cats == nil || l.cats == nil { + return l2.cats == l.cats + } + return l.cats.Cmp(l2.cats) == 0 +} + +// String returns an mlsRange as a string. +func (m mlsRange) String() string { + low := "s" + strconv.Itoa(m.low.sens) + if m.low.cats != nil && m.low.cats.BitLen() > 0 { + low += ":" + bitsetToStr(m.low.cats) + } + + if m.low.equal(m.high) { + return low + } + + high := "s" + strconv.Itoa(m.high.sens) + if m.high.cats != nil && m.high.cats.BitLen() > 0 { + high += ":" + bitsetToStr(m.high.cats) + } + + return low + "-" + high +} + +// TODO: remove these in favor of built-in min/max +// once we stop supporting Go < 1.21. +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func minInt(a, b int) int { + if a < b { + return a + } + return b +} + +// 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) { + s, err := rangeStrToMLSRange(sourceRange) + if err != nil { + return "", err + } + t, err := rangeStrToMLSRange(targetRange) + if err != nil { + return "", err + } + + if s.high.sens < t.low.sens || t.high.sens < s.low.sens { + /* these ranges have no common sensitivities */ + return "", ErrIncomparable + } + + outrange := &mlsRange{low: &level{}, high: &level{}} + + /* take the greatest of the low */ + outrange.low.sens = maxInt(s.low.sens, t.low.sens) + + /* take the least of the high */ + outrange.high.sens = minInt(s.high.sens, t.high.sens) + + /* find the intersecting categories */ + if s.low.cats != nil && t.low.cats != nil { + outrange.low.cats = new(big.Int) + outrange.low.cats.And(s.low.cats, t.low.cats) + } + if s.high.cats != nil && t.high.cats != nil { + outrange.high.cats = new(big.Int) + outrange.high.cats.And(s.high.cats, t.high.cats) + } + + return outrange.String(), nil +} + +func readWriteCon(fpath string, val string) (string, error) { + if fpath == "" { + return "", ErrEmptyPath + } + f, err := os.OpenFile(fpath, os.O_RDWR, 0) + if err != nil { + return "", err + } + defer f.Close() + + _, err = f.Write([]byte(val)) + if err != nil { + return "", err + } + + return readConFd(f) +} + +// peerLabel retrieves the label of the client on the other side of a socket +func peerLabel(fd uintptr) (string, error) { + l, err := unix.GetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_PEERSEC) + if err != nil { + return "", &os.PathError{Op: "getsockopt", Path: "fd " + strconv.Itoa(int(fd)), Err: err} + } + return l, nil +} + +// 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 { + // 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.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 != "" { + return c["user"] + ":" + c["role"] + ":" + c["type"] + ":" + l + } + return c["user"] + ":" + c["role"] + ":" + c["type"] +} + +// newContext creates a new Context struct from the specified label +func newContext(label string) (Context, error) { + c := make(Context) + + if len(label) != 0 { + con := strings.SplitN(label, ":", 4) + if len(con) < 3 { + return c, ErrInvalidLabel + } + c["user"] = con[0] + c["role"] = con[1] + c["type"] = con[2] + if len(con) > 3 { + c["level"] = con[3] + } + } + return c, nil +} + +// clearLabels clears all reserved labels +func clearLabels() { + state.Lock() + state.mcsList = make(map[string]bool) + state.Unlock() +} + +// reserveLabel reserves the MLS/MCS level component of the specified label +func reserveLabel(label string) { + if len(label) != 0 { + con := strings.SplitN(label, ":", 4) + if len(con) > 3 { + _ = mcsAdd(con[3]) + } + } +} + +func selinuxEnforcePath() string { + return filepath.Join(getSelinuxMountPoint(), "enforce") +} + +// isMLSEnabled checks if MLS is enabled. +func isMLSEnabled() bool { + enabledB, err := os.ReadFile(filepath.Join(getSelinuxMountPoint(), "mls")) + if err != nil { + return false + } + return bytes.Equal(enabledB, []byte{'1'}) +} + +// enforceMode returns the current SELinux mode Enforcing, Permissive, Disabled +func enforceMode() int { + var enforce int + + enforceB, err := os.ReadFile(selinuxEnforcePath()) + if err != nil { + return -1 + } + enforce, err = strconv.Atoi(string(enforceB)) + if err != nil { + return -1 + } + return enforce +} + +// 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 os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0) +} + +// 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 { + switch readConfig(selinuxTag) { + case "enforcing": + return Enforcing + case "permissive": + return Permissive + } + return Disabled +} + +func mcsAdd(mcs string) error { + if mcs == "" { + return nil + } + state.Lock() + defer state.Unlock() + if state.mcsList[mcs] { + return ErrMCSAlreadyExists + } + state.mcsList[mcs] = true + return nil +} + +func mcsDelete(mcs string) { + if mcs == "" { + return + } + state.Lock() + defer state.Unlock() + state.mcsList[mcs] = false +} + +func intToMcs(id int, catRange uint32) string { + var ( + SETSIZE = int(catRange) + TIER = SETSIZE + ORD = id + ) + + if id < 1 || id > 523776 { + return "" + } + + for ORD > TIER { + ORD -= TIER + TIER-- + } + TIER = SETSIZE - TIER + ORD += TIER + return fmt.Sprintf("s0:c%d,c%d", TIER, ORD) +} + +func uniqMcs(catRange uint32) string { + var ( + n uint32 + c1, c2 uint32 + mcs string + ) + + for { + _ = binary.Read(rand.Reader, binary.LittleEndian, &n) + c1 = n % catRange + _ = binary.Read(rand.Reader, binary.LittleEndian, &n) + c2 = n % catRange + if c1 == c2 { + continue + } else if c1 > c2 { + c1, c2 = c2, c1 + } + mcs = fmt.Sprintf("s0:c%d,c%d", c1, c2) + if err := mcsAdd(mcs); err != nil { + continue + } + break + } + return mcs +} + +// releaseLabel un-reserves the MLS/MCS Level field of the specified label, +// allowing it to be used by another process. +func releaseLabel(label string) { + if len(label) != 0 { + con := strings.SplitN(label, ":", 4) + if len(con) > 3 { + mcsDelete(con[3]) + } + } +} + +// roFileLabel returns the specified SELinux readonly file label +func roFileLabel() string { + return readOnlyFileLabel +} + +func openContextFile() (*os.File, error) { + if f, err := os.Open(contextFile); err == nil { + return f, nil + } + return os.Open(filepath.Join(policyRoot(), "contexts", "lxc_contexts")) +} + +func loadLabels() { + labels = make(map[string]string) + in, err := openContextFile() + if err != nil { + return + } + defer in.Close() + + scanner := bufio.NewScanner(in) + + for scanner.Scan() { + line := bytes.TrimSpace(scanner.Bytes()) + if len(line) == 0 { + // Skip blank lines + continue + } + if line[0] == ';' || line[0] == '#' { + // Skip comments + continue + } + fields := bytes.SplitN(line, []byte{'='}, 2) + if len(fields) != 2 { + continue + } + key, val := bytes.TrimSpace(fields[0]), bytes.TrimSpace(fields[1]) + labels[string(key)] = string(bytes.Trim(val, `"`)) + } + + con, _ := NewContext(labels["file"]) + con["level"] = fmt.Sprintf("s0:c%d,c%d", maxCategory-2, maxCategory-1) + privContainerMountLabel = con.get() + reserveLabel(privContainerMountLabel) +} + +func label(key string) string { + loadLabelsOnce.Do(func() { + loadLabels() + }) + return labels[key] +} + +// kvmContainerLabels returns the default processLabel and mountLabel to be used +// for kvm containers by the calling process. +func kvmContainerLabels() (string, string) { + processLabel := label("kvm_process") + if processLabel == "" { + processLabel = label("process") + } + + return addMcs(processLabel, label("file")) +} + +// 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) { + processLabel := label("init_process") + if processLabel == "" { + processLabel = label("process") + } + + return addMcs(processLabel, label("file")) +} + +// containerLabels returns an allocated processLabel and fileLabel to be used for +// container labeling by the calling process. +func containerLabels() (processLabel string, fileLabel string) { + if !getEnabled() { + return "", "" + } + + processLabel = label("process") + fileLabel = label("file") + readOnlyFileLabel = label("ro_file") + + if processLabel == "" || fileLabel == "" { + return "", fileLabel + } + + if readOnlyFileLabel == "" { + readOnlyFileLabel = fileLabel + } + + return addMcs(processLabel, fileLabel) +} + +func addMcs(processLabel, fileLabel string) (string, string) { + scon, _ := NewContext(processLabel) + if scon["level"] != "" { + mcs := uniqMcs(CategoryRange) + scon["level"] = mcs + processLabel = scon.Get() + scon, _ = NewContext(fileLabel) + scon["level"] = mcs + fileLabel = scon.Get() + } + return processLabel, fileLabel +} + +// securityCheckContext validates that the SELinux label is understood by the kernel +func securityCheckContext(val string) error { + return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0) +} + +// copyLevel returns a label with the MLS/MCS level from src label replaced on +// the dest label. +func copyLevel(src, dest string) (string, error) { + if src == "" { + return "", nil + } + if err := SecurityCheckContext(src); err != nil { + return "", err + } + if err := SecurityCheckContext(dest); err != nil { + return "", err + } + scon, err := NewContext(src) + if err != nil { + return "", err + } + tcon, err := NewContext(dest) + if err != nil { + return "", err + } + mcsDelete(tcon["level"]) + _ = mcsAdd(scon["level"]) + tcon["level"] = scon["level"] + return tcon.Get(), nil +} + +// 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. +func chcon(fpath string, label string, recurse bool) error { + if fpath == "" { + return ErrEmptyPath + } + if label == "" { + return nil + } + + excludePaths := map[string]bool{ + "/": true, + "/bin": true, + "/boot": true, + "/dev": true, + "/etc": true, + "/etc/passwd": true, + "/etc/pki": true, + "/etc/shadow": true, + "/home": true, + "/lib": true, + "/lib64": true, + "/media": true, + "/opt": true, + "/proc": true, + "/root": true, + "/run": true, + "/sbin": true, + "/srv": true, + "/sys": true, + "/tmp": true, + "/usr": true, + "/var": true, + "/var/lib": true, + "/var/log": true, + } + + if home := os.Getenv("HOME"); home != "" { + excludePaths[home] = true + } + + if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" { + if usr, err := user.Lookup(sudoUser); err == nil { + excludePaths[usr.HomeDir] = true + } + } + + if fpath != "/" { + fpath = strings.TrimSuffix(fpath, "/") + } + if excludePaths[fpath] { + return fmt.Errorf("SELinux relabeling of %s is not allowed", fpath) + } + + if !recurse { + err := lSetFileLabel(fpath, label) + if err != nil { + // Check if file doesn't exist, must have been removed + if errors.Is(err, os.ErrNotExist) { + return nil + } + // Check if current label is correct on disk + flabel, nerr := lFileLabel(fpath) + if nerr == nil && flabel == label { + return nil + } + // Check if file doesn't exist, must have been removed + if errors.Is(nerr, os.ErrNotExist) { + return nil + } + return err + } + return nil + } + + return rchcon(fpath, label) +} + +func rchcon(fpath, label string) error { //revive:disable:cognitive-complexity + fastMode := false + // If the current label matches the new label, assume + // other labels are correct. + if cLabel, err := lFileLabel(fpath); err == nil && cLabel == label { + fastMode = true + } + return pwalkdir.Walk(fpath, func(p string, _ fs.DirEntry, _ error) error { + if fastMode { + if cLabel, err := lFileLabel(p); err == nil && cLabel == label { + return nil + } + } + err := lSetFileLabel(p, label) + // Walk a file tree can race with removal, so ignore ENOENT. + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err + }) +} + +// 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) { + if src == "" { + return nil, nil + } + con, err := NewContext(src) + if err != nil { + return nil, err + } + if con["user"] == "" || + con["role"] == "" || + con["type"] == "" { + return nil, nil + } + dup := []string{ + "user:" + con["user"], + "role:" + con["role"], + "type:" + con["type"], + } + + if con["level"] != "" { + dup = append(dup, "level:"+con["level"]) + } + + return dup, nil +} + +// findUserInContext scans the reader for a valid SELinux context +// match that is verified with the verifier. Invalid contexts are +// skipped. It returns a matched context or an empty string if no +// match is found. If a scanner error occurs, it is returned. +func findUserInContext(context Context, r io.Reader, verifier func(string) error) (string, error) { + fromRole := context["role"] + fromType := context["type"] + scanner := bufio.NewScanner(r) + + for scanner.Scan() { + fromConns := strings.Fields(scanner.Text()) + if len(fromConns) == 0 { + // Skip blank lines + continue + } + + line := fromConns[0] + + if line[0] == ';' || line[0] == '#' { + // Skip comments + continue + } + + // user context files contexts are formatted as + // role_r:type_t:s0 where the user is missing. + lineArr := strings.SplitN(line, ":", 4) + // skip context with typo, or role and type do not match + if len(lineArr) != 3 || + lineArr[0] != fromRole || + lineArr[1] != fromType { + continue + } + + for _, cc := range fromConns[1:] { + toConns := strings.SplitN(cc, ":", 4) + if len(toConns) != 3 { + continue + } + + context["role"] = toConns[0] + context["type"] = toConns[1] + + outConn := context.get() + if err := verifier(outConn); err != nil { + continue + } + + return outConn, nil + } + } + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("failed to scan for context: %w", err) + } + + return "", nil +} + +func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { + if c.verifier == nil { + return "", ErrVerifierNil + } + + context, err := newContext(c.scon) + if err != nil { + return "", fmt.Errorf("failed to create label for %s: %w", c.scon, err) + } + + // set so the verifier validates the matched context with the provided user and level. + context["user"] = c.user + context["level"] = c.level + + conn, err := findUserInContext(context, c.userRdr, c.verifier) + if err != nil { + return "", err + } + + if conn != "" { + return conn, nil + } + + conn, err = findUserInContext(context, c.defaultRdr, c.verifier) + if err != nil { + return "", err + } + + if conn != "" { + return conn, nil + } + + return "", fmt.Errorf("context %q not found: %w", c.scon, ErrContextMissing) +} + +func getDefaultContextWithLevel(user, level, scon string) (string, error) { + userPath := filepath.Join(policyRoot(), selinuxUsersDir, user) + fu, err := os.Open(userPath) + if err != nil { + return "", err + } + defer fu.Close() + + defaultPath := filepath.Join(policyRoot(), defaultContexts) + fd, err := os.Open(defaultPath) + if err != nil { + return "", err + } + defer fd.Close() + + c := defaultSECtx{ + user: user, + level: level, + scon: scon, + userRdr: fu, + defaultRdr: fd, + verifier: securityCheckContext, + } + + return getDefaultContextFromReaders(&c) +} diff --git a/internal/third_party/selinux/go-selinux/selinux_linux_test.go b/internal/third_party/selinux/go-selinux/selinux_linux_test.go new file mode 100644 index 000000000..71aa0b82c --- /dev/null +++ b/internal/third_party/selinux/go-selinux/selinux_linux_test.go @@ -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() + } +} diff --git a/internal/third_party/selinux/go-selinux/selinux_stub.go b/internal/third_party/selinux/go-selinux/selinux_stub.go new file mode 100644 index 000000000..267921239 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/selinux_stub.go @@ -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 "" +} diff --git a/internal/third_party/selinux/go-selinux/selinux_stub_test.go b/internal/third_party/selinux/go-selinux/selinux_stub_test.go new file mode 100644 index 000000000..19ea636a4 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/selinux_stub_test.go @@ -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) + } +} diff --git a/internal/third_party/selinux/go-selinux/xattrs_linux.go b/internal/third_party/selinux/go-selinux/xattrs_linux.go new file mode 100644 index 000000000..559c85107 --- /dev/null +++ b/internal/third_party/selinux/go-selinux/xattrs_linux.go @@ -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 + } + } +} diff --git a/internal/third_party/selinux/go.mod b/internal/third_party/selinux/go.mod new file mode 100644 index 000000000..24d3261a9 --- /dev/null +++ b/internal/third_party/selinux/go.mod @@ -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 +) diff --git a/internal/third_party/selinux/go.sum b/internal/third_party/selinux/go.sum new file mode 100644 index 000000000..b9ae09877 --- /dev/null +++ b/internal/third_party/selinux/go.sum @@ -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= diff --git a/internal/third_party/selinux/pkg/pwalk/README.md b/internal/third_party/selinux/pkg/pwalk/README.md new file mode 100644 index 000000000..a060ad364 --- /dev/null +++ b/internal/third_party/selinux/pkg/pwalk/README.md @@ -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. diff --git a/internal/third_party/selinux/pkg/pwalk/pwalk.go b/internal/third_party/selinux/pkg/pwalk/pwalk.go new file mode 100644 index 000000000..686c8bac3 --- /dev/null +++ b/internal/third_party/selinux/pkg/pwalk/pwalk.go @@ -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 +} diff --git a/internal/third_party/selinux/pkg/pwalk/pwalk_test.go b/internal/third_party/selinux/pkg/pwalk/pwalk_test.go new file mode 100644 index 000000000..9cca3b6b1 --- /dev/null +++ b/internal/third_party/selinux/pkg/pwalk/pwalk_test.go @@ -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 +} diff --git a/internal/third_party/selinux/pkg/pwalkdir/README.md b/internal/third_party/selinux/pkg/pwalkdir/README.md new file mode 100644 index 000000000..b827e7dd7 --- /dev/null +++ b/internal/third_party/selinux/pkg/pwalkdir/README.md @@ -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. diff --git a/internal/third_party/selinux/pkg/pwalkdir/pwalkdir.go b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir.go new file mode 100644 index 000000000..5d2d09a29 --- /dev/null +++ b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir.go @@ -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 +} diff --git a/internal/third_party/selinux/pkg/pwalkdir/pwalkdir_test.go b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir_test.go new file mode 100644 index 000000000..e66a80d1a --- /dev/null +++ b/internal/third_party/selinux/pkg/pwalkdir/pwalkdir_test.go @@ -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 +} diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go index 1258c98ce..95f29e21f 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/label/label_linux.go @@ -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] diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go index 9f0740ef6..15150d475 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go @@ -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 diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go index a60798072..70392d989 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go @@ -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/ 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/. +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 to /proc/thread-self/. +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/ 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/. +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 to /proc/self/. +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/ 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 != "" { diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go index 0889fbe0e..267921239 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go @@ -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 "" } diff --git a/vendor/modules.txt b/vendor/modules.txt index 91eef3d4b..c0f707f10 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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