From a446493d654b6f816bc8b803e5a8945835c17da3 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 18:13:30 +0200 Subject: Adding upstream version 1.11.0+ds1. Signed-off-by: Daniel Baumann --- .github/workflows/validate.yml | 81 +++ .gitignore | 1 + .golangci.yml | 36 + CODEOWNERS | 1 + CONTRIBUTING.md | 119 ++++ LICENSE | 201 ++++++ MAINTAINERS | 5 + Makefile | 36 + README.md | 23 + VERSION | 1 + go-selinux/doc.go | 13 + go-selinux/label/label.go | 115 +++ go-selinux/label/label_linux.go | 150 ++++ go-selinux/label/label_linux_test.go | 224 ++++++ go-selinux/label/label_stub.go | 50 ++ go-selinux/label/label_stub_test.go | 121 ++++ go-selinux/label/label_test.go | 35 + go-selinux/selinux.go | 314 +++++++++ go-selinux/selinux_linux.go | 1295 ++++++++++++++++++++++++++++++++++ go-selinux/selinux_linux_test.go | 591 ++++++++++++++++ go-selinux/selinux_stub.go | 155 ++++ go-selinux/selinux_stub_test.go | 127 ++++ go-selinux/xattrs_linux.go | 71 ++ go.mod | 5 + go.sum | 2 + pkg/pwalk/README.md | 48 ++ pkg/pwalk/pwalk.go | 123 ++++ pkg/pwalk/pwalk_test.go | 217 ++++++ pkg/pwalkdir/README.md | 54 ++ pkg/pwalkdir/pwalkdir.go | 116 +++ pkg/pwalkdir/pwalkdir_test.go | 221 ++++++ 31 files changed, 4551 insertions(+) create mode 100644 .github/workflows/validate.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 MAINTAINERS create mode 100644 Makefile create mode 100644 README.md create mode 100644 VERSION create mode 100644 go-selinux/doc.go create mode 100644 go-selinux/label/label.go create mode 100644 go-selinux/label/label_linux.go create mode 100644 go-selinux/label/label_linux_test.go create mode 100644 go-selinux/label/label_stub.go create mode 100644 go-selinux/label/label_stub_test.go create mode 100644 go-selinux/label/label_test.go create mode 100644 go-selinux/selinux.go create mode 100644 go-selinux/selinux_linux.go create mode 100644 go-selinux/selinux_linux_test.go create mode 100644 go-selinux/selinux_stub.go create mode 100644 go-selinux/selinux_stub_test.go create mode 100644 go-selinux/xattrs_linux.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/pwalk/README.md create mode 100644 pkg/pwalk/pwalk.go create mode 100644 pkg/pwalk/pwalk_test.go create mode 100644 pkg/pwalkdir/README.md create mode 100644 pkg/pwalkdir/pwalkdir.go create mode 100644 pkg/pwalkdir/pwalkdir_test.go diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..7331776 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,81 @@ +name: validate +on: + push: + tags: + - v* + branches: + - master + pull_request: + +jobs: + + commit: + runs-on: ubuntu-20.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.0.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: check subject line length + uses: tim-actions/commit-message-checker-with-regex@v0.3.1 + with: + commits: ${{ steps.get-pr-commits.outputs.commits }} + pattern: '^.{0,72}(\n.*)*$' + error: 'Subject too long (max 72)' + + lint: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.20.x + - uses: golangci/golangci-lint-action@v3 + with: + version: v1.51 + + cross: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: cross + run: make build-cross + + test-stubs: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.20.x + - uses: golangci/golangci-lint-action@v3 + with: + version: v1.51 + - name: test-stubs + run: make test + + test: + strategy: + fail-fast: false + matrix: + go-version: [1.19.x, 1.20.x] + race: ["-race", ""] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + + - name: install go ${{ matrix.go-version }} + uses: actions/setup-go@v3 + with: + stable: '!contains(${{ matrix.go-version }}, "beta") && !contains(${{ matrix.go-version }}, "rc")' + go-version: ${{ matrix.go-version }} + + - name: build + run: make BUILDFLAGS="${{ matrix.race }}" build + + - name: test + run: make TESTFLAGS="${{ matrix.race }}" test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a570a2e --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,36 @@ +--- +run: + concurrency: 6 + deadline: 5m +linters: + enable: + - dupword # Detects duplicate words. + - errorlint # Detects code that may cause problems with Go 1.13 error wrapping. + - exportloopref # Detects pointers to enclosing loop variables. + - gocritic # Metalinter; detects bugs, performance, and styling issues. + - gofumpt # Detects whether code was gofumpt-ed. + - 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. + - tenv # Detects using os.Setenv instead of t.Setenv. + - thelper # Detects test helpers without t.Helper(). + - tparallel # Detects inappropriate usage of t.Parallel(). + - unconvert # Detects unnecessary type conversions. +linters-settings: + govet: + check-shadowing: true + enable-all: true + settings: + shadow: + strict: true +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + exclude-rules: + - text: '^shadow: declaration of "err" shadows declaration' + linters: + - govet diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..1439217 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dc3ff6a --- /dev/null +++ b/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/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/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/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000..748c18b --- /dev/null +++ b/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/Makefile b/Makefile new file mode 100644 index 0000000..c39d6e0 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +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,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/README.md b/README.md new file mode 100644 index 0000000..cd6a60f --- /dev/null +++ b/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/VERSION b/VERSION new file mode 100644 index 0000000..1cac385 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.11.0 diff --git a/go-selinux/doc.go b/go-selinux/doc.go new file mode 100644 index 0000000..57a15c9 --- /dev/null +++ b/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/go-selinux/label/label.go b/go-selinux/label/label.go new file mode 100644 index 0000000..07e0f77 --- /dev/null +++ b/go-selinux/label/label.go @@ -0,0 +1,115 @@ +package label + +import ( + "fmt" + + "github.com/opencontainers/selinux/go-selinux" +) + +// Deprecated: use selinux.ROFileLabel +var ROMountLabel = selinux.ROFileLabel + +// SetProcessLabel takes a process label and tells the kernel to assign the +// label to the next program executed by the current process. +// Deprecated: use selinux.SetExecLabel +var SetProcessLabel = selinux.SetExecLabel + +// ProcessLabel returns the process label that the kernel will assign +// to the next program executed by the current process. If "" is returned +// this indicates that the default labeling will happen for the process. +// Deprecated: use selinux.ExecLabel +var ProcessLabel = selinux.ExecLabel + +// SetSocketLabel takes a process label and tells the kernel to assign the +// label to the next socket that gets created +// Deprecated: use selinux.SetSocketLabel +var SetSocketLabel = selinux.SetSocketLabel + +// SocketLabel retrieves the current default socket label setting +// Deprecated: use selinux.SocketLabel +var SocketLabel = selinux.SocketLabel + +// SetKeyLabel takes a process label and tells the kernel to assign the +// label to the next kernel keyring that gets created +// Deprecated: use selinux.SetKeyLabel +var SetKeyLabel = selinux.SetKeyLabel + +// KeyLabel retrieves the current default kernel keyring label setting +// Deprecated: use selinux.KeyLabel +var KeyLabel = selinux.KeyLabel + +// FileLabel returns the label for specified path +// Deprecated: use selinux.FileLabel +var FileLabel = selinux.FileLabel + +// PidLabel will return the label of the process running with the specified pid +// Deprecated: use selinux.PidLabel +var PidLabel = selinux.PidLabel + +// Init initialises the labeling system +func Init() { + _ = selinux.GetEnabled() +} + +// ClearLabels will clear all reserved labels +// Deprecated: use selinux.ClearLabels +var ClearLabels = selinux.ClearLabels + +// ReserveLabel will record the fact that the MCS label has already been used. +// This will prevent InitLabels from using the MCS label in a newly created +// container +// Deprecated: use selinux.ReserveLabel +func ReserveLabel(label string) error { + selinux.ReserveLabel(label) + return nil +} + +// ReleaseLabel will remove the reservation of the MCS label. +// This will allow InitLabels to use the MCS label in a newly created +// containers +// Deprecated: use selinux.ReleaseLabel +func ReleaseLabel(label string) error { + selinux.ReleaseLabel(label) + return nil +} + +// DupSecOpt takes a process label and returns security options that +// can be used to set duplicate labels on future container processes +// Deprecated: use selinux.DupSecOpt +var DupSecOpt = selinux.DupSecOpt + +// 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/go-selinux/label/label_linux.go b/go-selinux/label/label_linux.go new file mode 100644 index 0000000..f61a560 --- /dev/null +++ b/go-selinux/label/label_linux.go @@ -0,0 +1,150 @@ +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 +} + +// Deprecated: The GenLabels function is only to be used during the transition +// to the official API. Use InitLabels(strings.Fields(options)) instead. +func GenLabels(options string) (string, string, error) { + return InitLabels(strings.Fields(options)) +} + +// 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() + } + if err := selinux.Chcon(path, fileLabel, true); err != nil { + return err + } + return nil +} + +// DisableSecOpt returns a security opt that can disable labeling +// support for future container processes +// Deprecated: use selinux.DisableSecOpt +var DisableSecOpt = selinux.DisableSecOpt + +// 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/go-selinux/label/label_linux_test.go b/go-selinux/label/label_linux_test.go new file mode 100644 index 0000000..0200810 --- /dev/null +++ b/go-selinux/label/label_linux_test.go @@ -0,0 +1,224 @@ +package label + +import ( + "errors" + "os" + "strings" + "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"} + roMountLabel := ROMountLabel() + if roMountLabel == "" { + t.Fatal("ROMountLabel: 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 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: expected \"disable\", got %q", secopt[0]) + } +} + +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 TestSELinuxNoLevel(t *testing.T) { + needSELinux(t) + + 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: expected 3, got %d", len(dup)) + } + con, err := selinux.NewContext(tlabel) + if err != nil { + t.Fatal(err) + } + if con.Get() != tlabel { + t.Errorf("NewContaxt and con.Get() failed on non mls label: expected %q, got %q", tlabel, con.Get()) + } +} + +func TestSocketLabel(t *testing.T) { + needSELinux(t) + + label := "system_u:object_r:container_t:s0:c1,c2" + if err := selinux.SetSocketLabel(label); err != nil { + t.Fatal(err) + } + nlabel, err := selinux.SocketLabel() + if err != nil { + t.Fatal(err) + } + if label != nlabel { + t.Errorf("SocketLabel %s != %s", nlabel, label) + } +} + +func TestKeyLabel(t *testing.T) { + needSELinux(t) + + label := "system_u:object_r:container_t:s0:c1,c2" + if err := selinux.SetKeyLabel(label); err != nil { + t.Fatal(err) + } + nlabel, err := selinux.KeyLabel() + if err != nil { + t.Fatal(err) + } + if label != nlabel { + t.Errorf("KeyLabel %s != %s", nlabel, label) + } +} + +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/go-selinux/label/label_stub.go b/go-selinux/label/label_stub.go new file mode 100644 index 0000000..f21c80c --- /dev/null +++ b/go-selinux/label/label_stub.go @@ -0,0 +1,50 @@ +//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(options []string) (string, string, error) { + return "", "", nil +} + +// Deprecated: The GenLabels function is only to be used during the transition +// to the official API. Use InitLabels(strings.Fields(options)) instead. +func GenLabels(options string) (string, string, error) { + return "", "", nil +} + +func SetFileLabel(path string, fileLabel string) error { + return nil +} + +func SetFileCreateLabel(fileLabel string) error { + return nil +} + +func Relabel(path string, fileLabel string, shared 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(label string) error { + return nil +} + +// RelabelNeeded checks whether the user requested a relabel +func RelabelNeeded(label string) bool { + return false +} + +// IsShared checks that the label includes a "shared" mark +func IsShared(label string) bool { + return false +} diff --git a/go-selinux/label/label_stub_test.go b/go-selinux/label/label_stub_test.go new file mode 100644 index 0000000..9742e6e --- /dev/null +++ b/go-selinux/label/label_stub_test.go @@ -0,0 +1,121 @@ +//go:build !linux +// +build !linux + +package label + +import "testing" + +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"} + roMountLabel := ROMountLabel() + if roMountLabel != "" { + t.Errorf("ROMountLabel 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 TestSocketLabel(t *testing.T) { + label := testLabel + if err := SetSocketLabel(label); err != nil { + t.Fatal(err) + } + if _, err := SocketLabel(); err != nil { + t.Fatal(err) + } +} + +func TestKeyLabel(t *testing.T) { + label := testLabel + if err := SetKeyLabel(label); err != nil { + t.Fatal(err) + } + if _, err := KeyLabel(); err != nil { + t.Fatal(err) + } +} + +func TestProcessLabel(t *testing.T) { + label := testLabel + if err := SetProcessLabel(label); err != nil { + t.Fatal(err) + } + if _, err := ProcessLabel(); err != nil { + t.Fatal(err) + } +} + +func TestCheckLabelCompile(t *testing.T) { + if _, _, err := GenLabels(""); err != nil { + t.Fatal(err) + } + + tmpDir := t.TempDir() + if _, err := FileLabel(tmpDir); err != nil { + t.Fatal(err) + } + + if err := SetFileLabel(tmpDir, "foobar"); err != nil { + t.Fatal(err) + } + + if err := SetFileCreateLabel("foobar"); err != nil { + t.Fatal(err) + } + + if _, err := PidLabel(0); err != nil { + t.Fatal(err) + } + + ClearLabels() + + if err := ReserveLabel("foobar"); err != nil { + t.Fatal(err) + } + + if err := ReleaseLabel("foobar"); err != nil { + t.Fatal(err) + } + + _, _ = DupSecOpt("foobar") + 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/go-selinux/label/label_test.go b/go-selinux/label/label_test.go new file mode 100644 index 0000000..fb172f3 --- /dev/null +++ b/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/go-selinux/selinux.go b/go-selinux/selinux.go new file mode 100644 index 0000000..af058b8 --- /dev/null +++ b/go-selinux/selinux.go @@ -0,0 +1,314 @@ +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") + + // 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 writeCon(attrPath("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 writeCon(attrPath("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 writeCon(attrPath("sockcreate"), label) +} + +// SocketLabel retrieves the current socket label setting +func SocketLabel() (string, error) { + return readCon(attrPath("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. +func SetKeyLabel(label string) error { + return setKeyLabel(label) +} + +// KeyLabel retrieves the current kernel keyring label setting +func KeyLabel() (string, error) { + return readCon("/proc/self/attr/keycreate") +} + +// 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/go-selinux/selinux_linux.go b/go-selinux/selinux_linux.go new file mode 100644 index 0000000..f1e9597 --- /dev/null +++ b/go-selinux/selinux_linux.go @@ -0,0 +1,1295 @@ +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/opencontainers/selinux/pkg/pwalkdir" + "golang.org/x/sys/unix" +) + +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 uint +} + +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 attrPath() + attrPathOnce sync.Once + haveThreadSelf 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 { //nolint:errorlint // unix errors are bare + continue + } + return false + } + + 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 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 { //nolint:errorlint // unix errors are bare + 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 { + return "", err + } + return string(bytes.TrimSuffix(data, []byte{0})), 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 { //nolint:errorlint // unix errors are bare + return &os.PathError{Op: "lsetxattr", 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 { //nolint:errorlint // unix errors are bare + return &os.PathError{Op: "setxattr", 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 writeCon(attrPath("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")) +} + +// currentLabel returns the SELinux label of the current process thread, or an error. +func currentLabel() (string, error) { + return readCon(attrPath("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)) +} + +// 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) +} + +// 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, int(i), 1) + } + } else { + cat, err := parseLevelItem(ranges[0], category) + if err != nil { + return nil, err + } + bitset.SetBit(bitset, int(cat), 1) + } + } + + return bitset, nil +} + +// parseLevelItem parses and verifies that a sensitivity or category are valid +func parseLevelItem(s string, sep levelItem) (uint, error) { + if len(s) < minSensLen || levelItem(s[0]) != sep { + return 0, ErrLevelSyntax + } + val, err := strconv.ParseUint(s[1:], 10, 32) + if err != nil { + return 0, err + } + + return uint(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 + for i := int(c.TrailingZeroBits()); 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(int(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(int(m.high.sens)) + if m.high.cats != nil && m.high.cats.BitLen() > 0 { + high += ":" + bitsetToStr(m.high.cats) + } + + return low + "-" + high +} + +func max(a, b uint) uint { + if a > b { + return a + } + return b +} + +func min(a, b uint) uint { + 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 = max(s.low.sens, t.low.sens) + + /* take the least of the high */ + outrange.high.sens = min(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 { + err := writeCon("/proc/self/attr/keycreate", label) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if label == "" && errors.Is(err, os.ErrPermission) { + return nil + } + return err +} + +// 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 { + //nolint:gosec // ignore G306: permissions to be 0600 or less. + return os.WriteFile(selinuxEnforcePath(), []byte(strconv.Itoa(mode)), 0o644) +} + +// 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 { + //nolint:gosec // ignore G306: permissions to be 0600 or less. + return os.WriteFile(filepath.Join(getSelinuxMountPoint(), "context"), []byte(val), 0o644) +} + +// 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(fpath); 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/go-selinux/selinux_linux_test.go b/go-selinux/selinux_linux_test.go new file mode 100644 index 0000000..c49e2bf --- /dev/null +++ b/go-selinux/selinux_linux_test.go @@ -0,0 +1,591 @@ +package selinux + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "testing" +) + +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 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.") + } + + 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\n", pid, intToMcs(pid, 1023)) + err = SetFSCreateLabel("unconfined_u:unconfined_r:unconfined_t:s0") + if err == nil { + t.Log(FSCreateLabel()) + } else { + t.Log("SetFSCreateLabel failed", err) + t.Fatal(err) + } + err = SetFSCreateLabel("") + if err == nil { + t.Log(FSCreateLabel()) + } else { + t.Log("SetFSCreateLabel failed", err) + t.Fatal(err) + } + 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/go-selinux/selinux_stub.go b/go-selinux/selinux_stub.go new file mode 100644 index 0000000..bc3fd3b --- /dev/null +++ b/go-selinux/selinux_stub.go @@ -0,0 +1,155 @@ +//go:build !linux +// +build !linux + +package selinux + +func attrPath(string) string { + return "" +} + +func readCon(fpath string) (string, error) { + return "", nil +} + +func writeCon(string, string) error { + return nil +} + +func setDisabled() {} + +func getEnabled() bool { + return false +} + +func classIndex(class string) (int, error) { + return -1, nil +} + +func setFileLabel(fpath string, label string) error { + return nil +} + +func lSetFileLabel(fpath string, label string) error { + return nil +} + +func fileLabel(fpath string) (string, error) { + return "", nil +} + +func lFileLabel(fpath string) (string, error) { + return "", nil +} + +func setFSCreateLabel(label string) error { + return nil +} + +func fsCreateLabel() (string, error) { + return "", nil +} + +func currentLabel() (string, error) { + return "", nil +} + +func pidLabel(pid int) (string, error) { + return "", nil +} + +func execLabel() (string, error) { + return "", nil +} + +func canonicalizeContext(val string) (string, error) { + return "", nil +} + +func computeCreateContext(source string, target string, class string) (string, error) { + return "", nil +} + +func calculateGlbLub(sourceRange, targetRange string) (string, error) { + return "", nil +} + +func peerLabel(fd uintptr) (string, error) { + return "", nil +} + +func setKeyLabel(label string) error { + return nil +} + +func (c Context) get() string { + return "" +} + +func newContext(label string) (Context, error) { + return Context{}, nil +} + +func clearLabels() { +} + +func reserveLabel(label string) { +} + +func isMLSEnabled() bool { + return false +} + +func enforceMode() int { + return Disabled +} + +func setEnforceMode(mode int) error { + return nil +} + +func defaultEnforceMode() int { + return Disabled +} + +func releaseLabel(label string) { +} + +func roFileLabel() string { + return "" +} + +func kvmContainerLabels() (string, string) { + return "", "" +} + +func initContainerLabels() (string, string) { + return "", "" +} + +func containerLabels() (processLabel string, fileLabel string) { + return "", "" +} + +func securityCheckContext(val string) error { + return nil +} + +func copyLevel(src, dest string) (string, error) { + return "", nil +} + +func chcon(fpath string, label string, recurse bool) error { + return nil +} + +func dupSecOpt(src string) ([]string, error) { + return nil, nil +} + +func getDefaultContextWithLevel(user, level, scon string) (string, error) { + return "", nil +} + +func label(_ string) string { + return "" +} diff --git a/go-selinux/selinux_stub_test.go b/go-selinux/selinux_stub_test.go new file mode 100644 index 0000000..19ea636 --- /dev/null +++ b/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/go-selinux/xattrs_linux.go b/go-selinux/xattrs_linux.go new file mode 100644 index 0000000..9e473ca --- /dev/null +++ b/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 { //nolint:errorlint // unix errors are bare + 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 { //nolint:errorlint // unix errors are bare + return sz, err + } + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..56328f1 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/opencontainers/selinux + +go 1.19 + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..28ab7f7 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/pwalk/README.md b/pkg/pwalk/README.md new file mode 100644 index 0000000..7e78dce --- /dev/null +++ b/pkg/pwalk/README.md @@ -0,0 +1,48 @@ +## 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; + + * 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/pkg/pwalk/pwalk.go b/pkg/pwalk/pwalk.go new file mode 100644 index 0000000..a28b4c4 --- /dev/null +++ b/pkg/pwalk/pwalk.go @@ -0,0 +1,123 @@ +package pwalk + +import ( + "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 { + 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/pkg/pwalk/pwalk_test.go b/pkg/pwalk/pwalk_test.go new file mode 100644 index 0000000..bf50613 --- /dev/null +++ b/pkg/pwalk/pwalk_test.go @@ -0,0 +1,217 @@ +package pwalk + +import ( + "errors" + "math/rand" + "os" + "path/filepath" + "runtime" + "sync/atomic" + "testing" + "time" +) + +func TestWalk(t *testing.T) { + var count uint32 + concurrency := runtime.NumCPU() * 2 + + dir, total, err := prepareTestSet(3, 2, 1) + if err != nil { + t.Fatalf("dataset creation failed: %v", err) + } + defer os.RemoveAll(dir) + + err = WalkN(dir, + func(_ string, _ os.FileInfo, _ error) error { + atomic.AddUint32(&count, 1) + return nil + }, + concurrency) + + if err != nil { + t.Errorf("Walk failed: %v", err) + } + if count != uint32(total) { + t.Errorf("File count mismatch: found %d, expected %d", count, total) + } + + t.Logf("concurrency: %d, files found: %d\n", concurrency, count) +} + +func TestWalkManyErrors(t *testing.T) { + var count uint32 + + dir, total, err := prepareTestSet(3, 3, 2) + if err != nil { + t.Fatalf("dataset creation failed: %v", err) + } + defer os.RemoveAll(dir) + + max := uint32(total / 2) + e42 := errors.New("42") + err = Walk(dir, + func(p string, i os.FileInfo, _ error) error { + if atomic.AddUint32(&count, 1) > max { + return e42 + } + return nil + }) + t.Logf("found %d of %d files", count, total) + + if err == nil { + t.Errorf("Walk succeeded, but error is expected") + if count != uint32(total) { + t.Errorf("File count mismatch: found %d, expected %d", count, total) + } + } +} + +func makeManyDirs(prefix string, levels, dirs, files int) (count int, err error) { + for d := 0; d < dirs; d++ { + var dir string + dir, err = os.MkdirTemp(prefix, "d-") + if err != nil { + return + } + 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 int + if c, err = makeManyDirs(dir, levels-1, dirs, files); err != nil { + return + } + count += c + } + + return +} + +// 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(levels, dirs, files int) (dir string, total int, err error) { + dir, err = os.MkdirTemp(".", "pwalk-test-") + if err != nil { + return + } + total, err = makeManyDirs(dir, levels, dirs, files) + if err != nil && total > 0 { + _ = os.RemoveAll(dir) + dir = "" + total = 0 + return + } + total++ // this dir + + return +} + +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, err := prepareTestSet(levels, dirs, files) + if err != nil { + b.Fatalf("dataset creation failed: %v", err) + } + defer os.RemoveAll(dir) + 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/pkg/pwalkdir/README.md b/pkg/pwalkdir/README.md new file mode 100644 index 0000000..068ac40 --- /dev/null +++ b/pkg/pwalkdir/README.md @@ -0,0 +1,54 @@ +## 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; + + * no errors are ever passed to WalkDirFunc; + + * 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 paralellism affects the speed. diff --git a/pkg/pwalkdir/pwalkdir.go b/pkg/pwalkdir/pwalkdir.go new file mode 100644 index 0000000..0f5d9f5 --- /dev/null +++ b/pkg/pwalkdir/pwalkdir.go @@ -0,0 +1,116 @@ +//go:build go1.16 +// +build go1.16 + +package pwalkdir + +import ( + "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 { + 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/pkg/pwalkdir/pwalkdir_test.go b/pkg/pwalkdir/pwalkdir_test.go new file mode 100644 index 0000000..c173001 --- /dev/null +++ b/pkg/pwalkdir/pwalkdir_test.go @@ -0,0 +1,221 @@ +//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 count uint32 + concurrency := runtime.NumCPU() * 2 + + dir, total, err := prepareTestSet(3, 2, 1) + if err != nil { + t.Fatalf("dataset creation failed: %v", err) + } + defer os.RemoveAll(dir) + + err = WalkN(dir, + func(_ string, _ fs.DirEntry, _ error) error { + atomic.AddUint32(&count, 1) + return nil + }, + concurrency) + + if err != nil { + t.Errorf("Walk failed: %v", err) + } + if count != uint32(total) { + t.Errorf("File count mismatch: found %d, expected %d", count, total) + } + + t.Logf("concurrency: %d, files found: %d\n", concurrency, count) +} + +func TestWalkDirManyErrors(t *testing.T) { + var count uint32 + + dir, total, err := prepareTestSet(3, 3, 2) + if err != nil { + t.Fatalf("dataset creation failed: %v", err) + } + defer os.RemoveAll(dir) + + max := uint32(total / 2) + e42 := errors.New("42") + err = Walk(dir, + func(p string, e fs.DirEntry, _ error) error { + if atomic.AddUint32(&count, 1) > max { + return e42 + } + return nil + }) + t.Logf("found %d of %d files", count, total) + + if err == nil { + t.Error("Walk succeeded, but error is expected") + if count != uint32(total) { + t.Errorf("File count mismatch: found %d, expected %d", count, total) + } + } +} + +func makeManyDirs(prefix string, levels, dirs, files int) (count int, err error) { + for d := 0; d < dirs; d++ { + var dir string + dir, err = os.MkdirTemp(prefix, "d-") + if err != nil { + return + } + 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 int + if c, err = makeManyDirs(dir, levels-1, dirs, files); err != nil { + return + } + count += c + } + + return +} + +// 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(levels, dirs, files int) (dir string, total int, err error) { + dir, err = os.MkdirTemp(".", "pwalk-test-") + if err != nil { + return + } + total, err = makeManyDirs(dir, levels, dirs, files) + if err != nil && total > 0 { + _ = os.RemoveAll(dir) + dir = "" + total = 0 + return + } + total++ // this dir + + return +} + +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, err := prepareTestSet(levels, dirs, files) + if err != nil { + b.Fatalf("dataset creation failed: %v", err) + } + defer os.RemoveAll(dir) + 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 +} -- cgit v1.2.3