summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:13:30 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 16:13:30 +0000
commita446493d654b6f816bc8b803e5a8945835c17da3 (patch)
tree1a9c576cba065f482742a0729249bf33ea1972ca
parentInitial commit. (diff)
downloadgolang-github-opencontainers-selinux-a446493d654b6f816bc8b803e5a8945835c17da3.tar.xz
golang-github-opencontainers-selinux-a446493d654b6f816bc8b803e5a8945835c17da3.zip
Adding upstream version 1.11.0+ds1.upstream/1.11.0+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/workflows/validate.yml81
-rw-r--r--.gitignore1
-rw-r--r--.golangci.yml36
-rw-r--r--CODEOWNERS1
-rw-r--r--CONTRIBUTING.md119
-rw-r--r--LICENSE201
-rw-r--r--MAINTAINERS5
-rw-r--r--Makefile36
-rw-r--r--README.md23
-rw-r--r--VERSION1
-rw-r--r--go-selinux/doc.go13
-rw-r--r--go-selinux/label/label.go115
-rw-r--r--go-selinux/label/label_linux.go150
-rw-r--r--go-selinux/label/label_linux_test.go224
-rw-r--r--go-selinux/label/label_stub.go50
-rw-r--r--go-selinux/label/label_stub_test.go121
-rw-r--r--go-selinux/label/label_test.go35
-rw-r--r--go-selinux/selinux.go314
-rw-r--r--go-selinux/selinux_linux.go1295
-rw-r--r--go-selinux/selinux_linux_test.go591
-rw-r--r--go-selinux/selinux_stub.go155
-rw-r--r--go-selinux/selinux_stub_test.go127
-rw-r--r--go-selinux/xattrs_linux.go71
-rw-r--r--go.mod5
-rw-r--r--go.sum2
-rw-r--r--pkg/pwalk/README.md48
-rw-r--r--pkg/pwalk/pwalk.go123
-rw-r--r--pkg/pwalk/pwalk_test.go217
-rw-r--r--pkg/pwalkdir/README.md54
-rw-r--r--pkg/pwalkdir/pwalkdir.go116
-rw-r--r--pkg/pwalkdir/pwalkdir_test.go221
31 files changed, 4551 insertions, 0 deletions
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 <joe@gmail.com>
+
+using your real name (sorry, no pseudonyms or anonymous contributions.)
+
+You can add the sign off when creating the git commit via `git commit -s`.
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@redhat.com> (@runcom)
+Daniel J Walsh <dwalsh@redhat.com> (@rhatdan)
+Mrunal Patel <mpatel@redhat.com> (@mrunalp)
+Sebastiaan van Stijn <github@gone.nl> (@thaJeztah)
+Kirill Kolyshikin <kolyshkin@gmail.com> (@kolyshkin)
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/<username> if it exists,
+// and falls back to the global /etc/selinux/{SELINUXTYPE}/contexts/default_contexts
+// file.
+func GetDefaultContextWithLevel(user, level, scon string) (string, error) {
+ return getDefaultContextWithLevel(user, level, scon)
+}
+
+// PrivContainerMountLabel returns mount label for privileged containers
+func PrivContainerMountLabel() string {
+ // Make sure label is initialized.
+ _ = label("")
+ return privContainerMountLabel
+}
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
+}