pax_global_header00006660000000000000000000000064146546553760014537gustar00rootroot0000000000000052 comment=44b3337c67171896cf6f299ece82ec31abfe8c0d selinux-1.11.1/000077500000000000000000000000001465465537600133075ustar00rootroot00000000000000selinux-1.11.1/.codespellrc000066400000000000000000000000711465465537600156050ustar00rootroot00000000000000[codespell] skip = ./.git,./go.sum,./go-selinux/testdata selinux-1.11.1/.github/000077500000000000000000000000001465465537600146475ustar00rootroot00000000000000selinux-1.11.1/.github/dependabot.yml000066400000000000000000000005521465465537600175010ustar00rootroot00000000000000# Please see the documentation for all configuration options: # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: # Dependencies listed in .github/workflows/*.yml - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" selinux-1.11.1/.github/workflows/000077500000000000000000000000001465465537600167045ustar00rootroot00000000000000selinux-1.11.1/.github/workflows/validate.yml000066400000000000000000000042631465465537600212250ustar00rootroot00000000000000name: 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.3.1 with: token: ${{ secrets.GITHUB_TOKEN }} - name: check subject line length uses: tim-actions/commit-message-checker-with-regex@v0.3.2 with: commits: ${{ steps.get-pr-commits.outputs.commits }} pattern: '^.{0,72}(\n.*)*$' error: 'Subject too long (max 72)' lint: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: 1.22.x cache: false # golangci-lint-action does its own caching - uses: golangci/golangci-lint-action@v6 with: version: v1.56 codespell: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: install deps # Version of codespell bundled with Ubuntu is way old, so use pip. run: pip install codespell - name: run codespell run: codespell cross: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: cross run: make build-cross test-stubs: runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: 1.22.x cache: false # golangci-lint-action does its own caching - uses: golangci/golangci-lint-action@v6 with: version: v1.56 - name: test-stubs run: make test test: strategy: fail-fast: false matrix: go-version: [1.21.x, 1.22.x] race: ["-race", ""] runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: install go ${{ matrix.go-version }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: build run: make BUILDFLAGS="${{ matrix.race }}" build - name: test run: make TESTFLAGS="${{ matrix.race }}" test selinux-1.11.1/.gitignore000066400000000000000000000000061465465537600152730ustar00rootroot00000000000000build selinux-1.11.1/.golangci.yml000066400000000000000000000027221465465537600156760ustar00rootroot00000000000000--- 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 selinux-1.11.1/CODEOWNERS000066400000000000000000000000621465465537600147000ustar00rootroot00000000000000* @kolyshkin @mrunalp @rhatdan @runcom @thajeztah selinux-1.11.1/CONTRIBUTING.md000066400000000000000000000116701465465537600155450ustar00rootroot00000000000000## Contribution Guidelines ### Security issues If you are reporting a security issue, do not create an issue or file a pull request on GitHub. Instead, disclose the issue responsibly by sending an email to security@opencontainers.org (which is inhabited only by the maintainers of the various OCI projects). ### Pull requests are always welcome We are always thrilled to receive pull requests, and do our best to process them as fast as possible. Not sure if that typo is worth a pull request? Do it! We will appreciate it. If your pull request is not accepted on the first try, don't be discouraged! If there's a problem with the implementation, hopefully you received feedback on what to improve. We're trying very hard to keep the project lean and focused. We don't want it to do everything for everybody. This means that we might decide against incorporating a new feature. ### Conventions Fork the repo and make changes on your fork in a feature branch. For larger bugs and enhancements, consider filing a leader issue or mailing-list thread for discussion that is independent of the implementation. Small changes or changes that have been discussed on the project mailing list may be submitted without a leader issue. If the project has a test suite, submit unit tests for your changes. Take a look at existing tests for inspiration. Run the full test suite on your branch before submitting a pull request. Update the documentation when creating or modifying features. Test your documentation changes for clarity, concision, and correctness, as well as a clean documentation build. See ``docs/README.md`` for more information on building the docs and how docs get released. Write clean code. Universally formatted code promotes ease of writing, reading, and maintenance. Always run `gofmt -s -w file.go` on each changed file before committing your changes. Most editors have plugins that do this automatically. Pull requests descriptions should be as clear as possible and include a reference to all the issues that they address. Commit messages must start with a capitalized and short summary written in the imperative, followed by an optional, more detailed explanatory text which is separated from the summary by an empty line. Code review comments may be added to your pull request. Discuss, then make the suggested modifications and push additional commits to your feature branch. Be sure to post a comment after pushing. The new commits will show up in the pull request automatically, but the reviewers will not be notified unless you comment. Before the pull request is merged, make sure that you squash your commits into logical units of work using `git rebase -i` and `git push -f`. After every commit the test suite (if any) should be passing. Include documentation changes in the same commit so that a revert would remove all traces of the feature or fix. Commits that fix or close an issue should include a reference like `Closes #XXX` or `Fixes #XXX`, which will automatically close the issue when merged. ### Sign your work The sign-off is a simple line at the end of the explanation for the patch, which certifies that you wrote it or otherwise have the right to pass it on as an open-source patch. The rules are pretty simple: if you can certify the below (from [developercertificate.org](http://developercertificate.org/)): ``` Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 York Street, Suite 102, San Francisco, CA 94110 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` then you just add a line to every git commit message: Signed-off-by: Joe Smith using your real name (sorry, no pseudonyms or anonymous contributions.) You can add the sign off when creating the git commit via `git commit -s`. selinux-1.11.1/LICENSE000066400000000000000000000261351465465537600143230ustar00rootroot00000000000000 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. selinux-1.11.1/MAINTAINERS000066400000000000000000000003601465465537600150030ustar00rootroot00000000000000Antonio Murdaca (@runcom) Daniel J Walsh (@rhatdan) Mrunal Patel (@mrunalp) Sebastiaan van Stijn (@thaJeztah) Kirill Kolyshikin (@kolyshkin) selinux-1.11.1/Makefile000066400000000000000000000011741465465537600147520ustar00rootroot00000000000000GO ?= go all: build build-cross define go-build GOOS=$(1) GOARCH=$(2) $(GO) build ${BUILDFLAGS} ./... endef .PHONY: build build: $(call go-build,linux,amd64) .PHONY: build-cross build-cross: $(call go-build,linux,386) $(call go-build,linux,arm) $(call go-build,linux,arm64) $(call go-build,linux,ppc64le) $(call go-build,linux,s390x) $(call go-build,linux,mips64le) $(call go-build,linux,riscv64) $(call go-build,windows,amd64) $(call go-build,windows,386) .PHONY: test test: $(GO) test -timeout 3m ${TESTFLAGS} -v ./... .PHONY: lint lint: golangci-lint run .PHONY: vendor vendor: $(GO) mod tidy $(GO) mod verify selinux-1.11.1/README.md000066400000000000000000000022321465465537600145650ustar00rootroot00000000000000# 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 selinux-1.11.1/VERSION000066400000000000000000000000131465465537600143510ustar00rootroot000000000000001.11.1-dev selinux-1.11.1/go-selinux/000077500000000000000000000000001465465537600154015ustar00rootroot00000000000000selinux-1.11.1/go-selinux/doc.go000066400000000000000000000004601465465537600164750ustar00rootroot00000000000000/* 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 selinux-1.11.1/go-selinux/label/000077500000000000000000000000001465465537600164605ustar00rootroot00000000000000selinux-1.11.1/go-selinux/label/label.go000066400000000000000000000104701465465537600200700ustar00rootroot00000000000000package 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 } selinux-1.11.1/go-selinux/label/label_linux.go000066400000000000000000000103441465465537600213070ustar00rootroot00000000000000package 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() } return selinux.Chcon(path, fileLabel, true) } // 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") } selinux-1.11.1/go-selinux/label/label_linux_test.go000066400000000000000000000130301465465537600223410ustar00rootroot00000000000000package 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) } } selinux-1.11.1/go-selinux/label/label_stub.go000066400000000000000000000022261465465537600211250ustar00rootroot00000000000000//go:build !linux // +build !linux package label // InitLabels returns the process label and file labels to be used within // the container. A list of options can be passed into this function to alter // the labels. func InitLabels([]string) (string, string, error) { return "", "", nil } // Deprecated: The GenLabels function is only to be used during the transition // to the official API. Use InitLabels(strings.Fields(options)) instead. func GenLabels(string) (string, string, error) { return "", "", nil } func SetFileLabel(string, string) error { return nil } func SetFileCreateLabel(string) error { return nil } func Relabel(string, string, bool) error { return nil } // DisableSecOpt returns a security opt that can disable labeling // support for future container processes func DisableSecOpt() []string { return nil } // Validate checks that the label does not include unexpected options func Validate(string) error { return nil } // RelabelNeeded checks whether the user requested a relabel func RelabelNeeded(string) bool { return false } // IsShared checks that the label includes a "shared" mark func IsShared(string) bool { return false } selinux-1.11.1/go-selinux/label/label_stub_test.go000066400000000000000000000044741465465537600221730ustar00rootroot00000000000000//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") } } selinux-1.11.1/go-selinux/label/label_test.go000066400000000000000000000021141465465537600211230ustar00rootroot00000000000000package 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) } } selinux-1.11.1/go-selinux/selinux.go000066400000000000000000000255531465465537600174310ustar00rootroot00000000000000package selinux import ( "errors" ) const ( // Enforcing constant indicate SELinux is in enforcing mode Enforcing = 1 // Permissive constant to indicate SELinux is in permissive mode Permissive = 0 // Disabled constant to indicate SELinux is disabled Disabled = -1 // maxCategory is the maximum number of categories used within containers maxCategory = 1024 // DefaultCategoryRange is the upper bound on the category range DefaultCategoryRange = uint32(maxCategory) ) var ( // ErrMCSAlreadyExists is returned when trying to allocate a duplicate MCS. ErrMCSAlreadyExists = errors.New("MCS label already exists") // ErrEmptyPath is returned when an empty path has been specified. ErrEmptyPath = errors.New("empty path") // ErrInvalidLabel is returned when an invalid label is specified. ErrInvalidLabel = errors.New("invalid Label") // InvalidLabel is returned when an invalid label is specified. // // Deprecated: use [ErrInvalidLabel]. InvalidLabel = ErrInvalidLabel // ErrIncomparable is returned two levels are not comparable ErrIncomparable = errors.New("incomparable levels") // ErrLevelSyntax is returned when a sensitivity or category do not have correct syntax in a level ErrLevelSyntax = errors.New("invalid level syntax") // ErrContextMissing is returned if a requested context is not found in a file. ErrContextMissing = errors.New("context does not have a match") // ErrVerifierNil is returned when a context verifier function is nil. ErrVerifierNil = errors.New("verifier function is nil") // CategoryRange allows the upper bound on the category range to be adjusted CategoryRange = DefaultCategoryRange privContainerMountLabel string ) // Context is a representation of the SELinux label broken into 4 parts type Context map[string]string // SetDisabled disables SELinux support for the package func SetDisabled() { setDisabled() } // GetEnabled returns whether SELinux is currently enabled. func GetEnabled() bool { return getEnabled() } // ClassIndex returns the int index for an object class in the loaded policy, // or -1 and an error func ClassIndex(class string) (int, error) { return classIndex(class) } // SetFileLabel sets the SELinux label for this path, following symlinks, // or returns an error. func SetFileLabel(fpath string, label string) error { return setFileLabel(fpath, label) } // LsetFileLabel sets the SELinux label for this path, not following symlinks, // or returns an error. func LsetFileLabel(fpath string, label string) error { return lSetFileLabel(fpath, label) } // FileLabel returns the SELinux label for this path, following symlinks, // or returns an error. func FileLabel(fpath string) (string, error) { return fileLabel(fpath) } // LfileLabel returns the SELinux label for this path, not following symlinks, // or returns an error. func LfileLabel(fpath string) (string, error) { return lFileLabel(fpath) } // SetFSCreateLabel tells the kernel what label to use for all file system objects // created by this task. // Set the label to an empty string to return to the default label. Calls to SetFSCreateLabel // should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until file system // objects created by this task are finished to guarantee another goroutine does not migrate // to the current thread before execution is complete. func SetFSCreateLabel(label string) error { return setFSCreateLabel(label) } // FSCreateLabel returns the default label the kernel which the kernel is using // for file system objects created by this task. "" indicates default. func FSCreateLabel() (string, error) { return fsCreateLabel() } // CurrentLabel returns the SELinux label of the current process thread, or an error. func CurrentLabel() (string, error) { return currentLabel() } // PidLabel returns the SELinux label of the given pid, or an error. func PidLabel(pid int) (string, error) { return pidLabel(pid) } // ExecLabel returns the SELinux label that the kernel will use for any programs // that are executed by the current process thread, or an error. func ExecLabel() (string, error) { return execLabel() } // CanonicalizeContext takes a context string and writes it to the kernel // the function then returns the context that the kernel will use. Use this // function to check if two contexts are equivalent func CanonicalizeContext(val string) (string, error) { return canonicalizeContext(val) } // ComputeCreateContext requests the type transition from source to target for // class from the kernel. func ComputeCreateContext(source string, target string, class string) (string, error) { return computeCreateContext(source, target, class) } // CalculateGlbLub computes the glb (greatest lower bound) and lub (least upper bound) // of a source and target range. // The glblub is calculated as the greater of the low sensitivities and // the lower of the high sensitivities and the and of each category bitset. func CalculateGlbLub(sourceRange, targetRange string) (string, error) { return calculateGlbLub(sourceRange, targetRange) } // SetExecLabel sets the SELinux label that the kernel will use for any programs // that are executed by the current process thread, or an error. Calls to SetExecLabel // should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until execution // of the program is finished to guarantee another goroutine does not migrate to the current // thread before execution is complete. func SetExecLabel(label string) error { return writeCon(attrPath("exec"), label) } // SetTaskLabel sets the SELinux label for the current thread, or an error. // This requires the dyntransition permission. Calls to SetTaskLabel should // be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() to guarantee // the current thread does not run in a new mislabeled thread. func SetTaskLabel(label string) error { return writeCon(attrPath("current"), label) } // SetSocketLabel takes a process label and tells the kernel to assign the // label to the next socket that gets created. Calls to SetSocketLabel // should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until // the socket is created to guarantee another goroutine does not migrate // to the current thread before execution is complete. func SetSocketLabel(label string) error { return writeCon(attrPath("sockcreate"), label) } // SocketLabel retrieves the current socket label setting func SocketLabel() (string, error) { return readCon(attrPath("sockcreate")) } // PeerLabel retrieves the label of the client on the other side of a socket func PeerLabel(fd uintptr) (string, error) { return peerLabel(fd) } // SetKeyLabel takes a process label and tells the kernel to assign the // label to the next kernel keyring that gets created. Calls to SetKeyLabel // should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until // the kernel keyring is created to guarantee another goroutine does not migrate // to the current thread before execution is complete. func SetKeyLabel(label string) error { return setKeyLabel(label) } // KeyLabel retrieves the current kernel keyring label setting func KeyLabel() (string, error) { return readCon("/proc/self/attr/keycreate") } // Get returns the Context as a string func (c Context) Get() string { return c.get() } // NewContext creates a new Context struct from the specified label func NewContext(label string) (Context, error) { return newContext(label) } // ClearLabels clears all reserved labels func ClearLabels() { clearLabels() } // ReserveLabel reserves the MLS/MCS level component of the specified label func ReserveLabel(label string) { reserveLabel(label) } // MLSEnabled checks if MLS is enabled. func MLSEnabled() bool { return isMLSEnabled() } // EnforceMode returns the current SELinux mode Enforcing, Permissive, Disabled func EnforceMode() int { return enforceMode() } // SetEnforceMode sets the current SELinux mode Enforcing, Permissive. // Disabled is not valid, since this needs to be set at boot time. func SetEnforceMode(mode int) error { return setEnforceMode(mode) } // DefaultEnforceMode returns the systems default SELinux mode Enforcing, // Permissive or Disabled. Note this is just the default at boot time. // EnforceMode tells you the systems current mode. func DefaultEnforceMode() int { return defaultEnforceMode() } // ReleaseLabel un-reserves the MLS/MCS Level field of the specified label, // allowing it to be used by another process. func ReleaseLabel(label string) { releaseLabel(label) } // ROFileLabel returns the specified SELinux readonly file label func ROFileLabel() string { return roFileLabel() } // KVMContainerLabels returns the default processLabel and mountLabel to be used // for kvm containers by the calling process. func KVMContainerLabels() (string, string) { return kvmContainerLabels() } // InitContainerLabels returns the default processLabel and file labels to be // used for containers running an init system like systemd by the calling process. func InitContainerLabels() (string, string) { return initContainerLabels() } // ContainerLabels returns an allocated processLabel and fileLabel to be used for // container labeling by the calling process. func ContainerLabels() (processLabel string, fileLabel string) { return containerLabels() } // SecurityCheckContext validates that the SELinux label is understood by the kernel func SecurityCheckContext(val string) error { return securityCheckContext(val) } // CopyLevel returns a label with the MLS/MCS level from src label replaced on // the dest label. func CopyLevel(src, dest string) (string, error) { return copyLevel(src, dest) } // Chcon changes the fpath file object to the SELinux label. // If fpath is a directory and recurse is true, then Chcon walks the // directory tree setting the label. // // The fpath itself is guaranteed to be relabeled last. func Chcon(fpath string, label string, recurse bool) error { return chcon(fpath, label, recurse) } // DupSecOpt takes an SELinux process label and returns security options that // can be used to set the SELinux Type and Level for future container processes. func DupSecOpt(src string) ([]string, error) { return dupSecOpt(src) } // DisableSecOpt returns a security opt that can be used to disable SELinux // labeling support for future container processes. func DisableSecOpt() []string { return []string{"disable"} } // GetDefaultContextWithLevel gets a single context for the specified SELinux user // identity that is reachable from the specified scon context. The context is based // on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/ if it exists, // and falls back to the global /etc/selinux/{SELINUXTYPE}/contexts/default_contexts // file. func GetDefaultContextWithLevel(user, level, scon string) (string, error) { return getDefaultContextWithLevel(user, level, scon) } // PrivContainerMountLabel returns mount label for privileged containers func PrivContainerMountLabel() string { // Make sure label is initialized. _ = label("") return privContainerMountLabel } selinux-1.11.1/go-selinux/selinux_linux.go000066400000000000000000000714611465465537600206470ustar00rootroot00000000000000package 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 { 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 { 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 { return &os.PathError{Op: fmt.Sprintf("lsetxattr(label=%s)", label), Path: fpath, Err: err} } } return nil } // setFileLabel sets the SELinux label for this path, following symlinks, // or returns an error. func setFileLabel(fpath string, label string) error { if fpath == "" { return ErrEmptyPath } for { err := unix.Setxattr(fpath, xattrNameSelinux, []byte(label), 0) if err == nil { break } if err != unix.EINTR { return &os.PathError{Op: fmt.Sprintf("setxattr(label=%s)", label), Path: fpath, Err: err} } } return nil } // fileLabel returns the SELinux label for this path, following symlinks, // or returns an error. func fileLabel(fpath string) (string, error) { if fpath == "" { return "", ErrEmptyPath } label, err := getxattr(fpath, xattrNameSelinux) if err != nil { return "", &os.PathError{Op: "getxattr", Path: fpath, Err: err} } // Trim the NUL byte at the end of the byte buffer, if present. if len(label) > 0 && label[len(label)-1] == '\x00' { label = label[:len(label)-1] } return string(label), nil } // lFileLabel returns the SELinux label for this path, not following symlinks, // or returns an error. func lFileLabel(fpath string) (string, error) { if fpath == "" { return "", ErrEmptyPath } label, err := lgetxattr(fpath, xattrNameSelinux) if err != nil { return "", &os.PathError{Op: "lgetxattr", Path: fpath, Err: err} } // Trim the NUL byte at the end of the byte buffer, if present. if len(label) > 0 && label[len(label)-1] == '\x00' { label = label[:len(label)-1] } return string(label), nil } func setFSCreateLabel(label string) error { return 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 } // TODO: remove min and max once Go < 1.21 is not supported. 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(p); err == nil && cLabel == label { return nil } } err := lSetFileLabel(p, label) // Walk a file tree can race with removal, so ignore ENOENT. if errors.Is(err, os.ErrNotExist) { return nil } return err }) } // dupSecOpt takes an SELinux process label and returns security options that // can be used to set the SELinux Type and Level for future container processes. func dupSecOpt(src string) ([]string, error) { if src == "" { return nil, nil } con, err := NewContext(src) if err != nil { return nil, err } if con["user"] == "" || con["role"] == "" || con["type"] == "" { return nil, nil } dup := []string{ "user:" + con["user"], "role:" + con["role"], "type:" + con["type"], } if con["level"] != "" { dup = append(dup, "level:"+con["level"]) } return dup, nil } // findUserInContext scans the reader for a valid SELinux context // match that is verified with the verifier. Invalid contexts are // skipped. It returns a matched context or an empty string if no // match is found. If a scanner error occurs, it is returned. func findUserInContext(context Context, r io.Reader, verifier func(string) error) (string, error) { fromRole := context["role"] fromType := context["type"] scanner := bufio.NewScanner(r) for scanner.Scan() { fromConns := strings.Fields(scanner.Text()) if len(fromConns) == 0 { // Skip blank lines continue } line := fromConns[0] if line[0] == ';' || line[0] == '#' { // Skip comments continue } // user context files contexts are formatted as // role_r:type_t:s0 where the user is missing. lineArr := strings.SplitN(line, ":", 4) // skip context with typo, or role and type do not match if len(lineArr) != 3 || lineArr[0] != fromRole || lineArr[1] != fromType { continue } for _, cc := range fromConns[1:] { toConns := strings.SplitN(cc, ":", 4) if len(toConns) != 3 { continue } context["role"] = toConns[0] context["type"] = toConns[1] outConn := context.get() if err := verifier(outConn); err != nil { continue } return outConn, nil } } if err := scanner.Err(); err != nil { return "", fmt.Errorf("failed to scan for context: %w", err) } return "", nil } func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { if c.verifier == nil { return "", ErrVerifierNil } context, err := newContext(c.scon) if err != nil { return "", fmt.Errorf("failed to create label for %s: %w", c.scon, err) } // set so the verifier validates the matched context with the provided user and level. context["user"] = c.user context["level"] = c.level conn, err := findUserInContext(context, c.userRdr, c.verifier) if err != nil { return "", err } if conn != "" { return conn, nil } conn, err = findUserInContext(context, c.defaultRdr, c.verifier) if err != nil { return "", err } if conn != "" { return conn, nil } return "", fmt.Errorf("context %q not found: %w", c.scon, ErrContextMissing) } func getDefaultContextWithLevel(user, level, scon string) (string, error) { userPath := filepath.Join(policyRoot(), selinuxUsersDir, user) fu, err := os.Open(userPath) if err != nil { return "", err } defer fu.Close() defaultPath := filepath.Join(policyRoot(), defaultContexts) fd, err := os.Open(defaultPath) if err != nil { return "", err } defer fd.Close() c := defaultSECtx{ user: user, level: level, scon: scon, userRdr: fu, defaultRdr: fd, verifier: securityCheckContext, } return getDefaultContextFromReaders(&c) } selinux-1.11.1/go-selinux/selinux_linux_test.go000066400000000000000000000420771465465537600217070ustar00rootroot00000000000000package 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() } } selinux-1.11.1/go-selinux/selinux_stub.go000066400000000000000000000042111465465537600204520ustar00rootroot00000000000000//go:build !linux // +build !linux package selinux func attrPath(string) string { return "" } func readCon(string) (string, error) { return "", nil } func writeCon(string, string) error { return nil } func setDisabled() {} func getEnabled() bool { return false } func classIndex(string) (int, error) { return -1, nil } func setFileLabel(string, string) error { return nil } func lSetFileLabel(string, string) error { return nil } func fileLabel(string) (string, error) { return "", nil } func lFileLabel(string) (string, error) { return "", nil } func setFSCreateLabel(string) error { return nil } func fsCreateLabel() (string, error) { return "", nil } func currentLabel() (string, error) { return "", nil } func pidLabel(int) (string, error) { return "", nil } func execLabel() (string, error) { return "", nil } func canonicalizeContext(string) (string, error) { return "", nil } func computeCreateContext(string, string, string) (string, error) { return "", nil } func calculateGlbLub(string, string) (string, error) { return "", nil } func peerLabel(uintptr) (string, error) { return "", nil } func setKeyLabel(string) error { return nil } func (c Context) get() string { return "" } func newContext(string) (Context, error) { return Context{}, nil } func clearLabels() { } func reserveLabel(string) { } func isMLSEnabled() bool { return false } func enforceMode() int { return Disabled } func setEnforceMode(int) error { return nil } func defaultEnforceMode() int { return Disabled } func releaseLabel(string) { } func roFileLabel() string { return "" } func kvmContainerLabels() (string, string) { return "", "" } func initContainerLabels() (string, string) { return "", "" } func containerLabels() (string, string) { return "", "" } func securityCheckContext(string) error { return nil } func copyLevel(string, string) (string, error) { return "", nil } func chcon(string, string, bool) error { return nil } func dupSecOpt(string) ([]string, error) { return nil, nil } func getDefaultContextWithLevel(string, string, string) (string, error) { return "", nil } func label(_ string) string { return "" } selinux-1.11.1/go-selinux/selinux_stub_test.go000066400000000000000000000052111465465537600215120ustar00rootroot00000000000000//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) } } selinux-1.11.1/go-selinux/xattrs_linux.go000066400000000000000000000033471465465537600205030ustar00rootroot00000000000000package selinux import ( "golang.org/x/sys/unix" ) // lgetxattr returns a []byte slice containing the value of // an extended attribute attr set for path. func lgetxattr(path, attr string) ([]byte, error) { // Start with a 128 length byte array dest := make([]byte, 128) sz, errno := doLgetxattr(path, attr, dest) for errno == unix.ERANGE { //nolint:errorlint // unix errors are bare // Buffer too small, use zero-sized buffer to get the actual size sz, errno = doLgetxattr(path, attr, []byte{}) if errno != nil { return nil, errno } dest = make([]byte, sz) sz, errno = doLgetxattr(path, attr, dest) } if errno != nil { return nil, errno } return dest[:sz], nil } // doLgetxattr is a wrapper that retries on EINTR func doLgetxattr(path, attr string, dest []byte) (int, error) { for { sz, err := unix.Lgetxattr(path, attr, dest) if err != unix.EINTR { return sz, err } } } // getxattr returns a []byte slice containing the value of // an extended attribute attr set for path. func getxattr(path, attr string) ([]byte, error) { // Start with a 128 length byte array dest := make([]byte, 128) sz, errno := dogetxattr(path, attr, dest) for errno == unix.ERANGE { //nolint:errorlint // unix errors are bare // Buffer too small, use zero-sized buffer to get the actual size sz, errno = dogetxattr(path, attr, []byte{}) if errno != nil { return nil, errno } dest = make([]byte, sz) sz, errno = dogetxattr(path, attr, dest) } if errno != nil { return nil, errno } return dest[:sz], nil } // dogetxattr is a wrapper that retries on EINTR func dogetxattr(path, attr string, dest []byte) (int, error) { for { sz, err := unix.Getxattr(path, attr, dest) if err != unix.EINTR { return sz, err } } } selinux-1.11.1/go.mod000066400000000000000000000001571465465537600144200ustar00rootroot00000000000000module github.com/opencontainers/selinux go 1.19 require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 selinux-1.11.1/go.sum000066400000000000000000000003171465465537600144430ustar00rootroot00000000000000golang.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= selinux-1.11.1/pkg/000077500000000000000000000000001465465537600140705ustar00rootroot00000000000000selinux-1.11.1/pkg/pwalk/000077500000000000000000000000001465465537600152065ustar00rootroot00000000000000selinux-1.11.1/pkg/pwalk/README.md000066400000000000000000000034051465465537600164670ustar00rootroot00000000000000## pwalk: parallel implementation of filepath.Walk This is a wrapper for [filepath.Walk](https://pkg.go.dev/path/filepath?tab=doc#Walk) which may speed it up by calling multiple callback functions (WalkFunc) in parallel, utilizing goroutines. By default, it utilizes 2\*runtime.NumCPU() goroutines for callbacks. This can be changed by using WalkN function which has the additional parameter, specifying the number of goroutines (concurrency). ### pwalk vs pwalkdir This package is deprecated in favor of [pwalkdir](https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir), which is faster, but requires at least Go 1.16. ### Caveats Please note the following limitations of this code: * Unlike filepath.Walk, the order of calls is non-deterministic; * Only primitive error handling is supported: * filepath.SkipDir is not supported; * ErrNotExist errors from filepath.Walk are silently ignored for any path except the top directory (Walk argument); any other error is returned to the caller of Walk; * no errors are ever passed to WalkFunc; * once any error is returned from any WalkFunc instance, no more new calls to WalkFunc are made, and the error is returned to the caller of Walk; * if more than one walkFunc instance will return an error, only one of such errors will be propagated and returned by Walk, others will be silently discarded. ### Documentation For the official documentation, see https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalk?tab=doc ### Benchmarks For a WalkFunc that consists solely of the return statement, this implementation is about 10% slower than the standard library's filepath.Walk. Otherwise (if a WalkFunc is doing something) this is usually faster, except when the WalkN(..., 1) is used. selinux-1.11.1/pkg/pwalk/pwalk.go000066400000000000000000000063741465465537600166650ustar00rootroot00000000000000package pwalk import ( "errors" "fmt" "os" "path/filepath" "runtime" "sync" ) // WalkFunc is the type of the function called by Walk to visit each // file or directory. It is an alias for [filepath.WalkFunc]. // // Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir] and [fs.WalkDirFunc]. type WalkFunc = filepath.WalkFunc // Walk is a wrapper for filepath.Walk which can call multiple walkFn // in parallel, allowing to handle each item concurrently. A maximum of // twice the runtime.NumCPU() walkFn will be called at any one time. // If you want to change the maximum, use WalkN instead. // // The order of calls is non-deterministic. // // Note that this implementation only supports primitive error handling: // // - no errors are ever passed to walkFn; // // - once a walkFn returns any error, all further processing stops // and the error is returned to the caller of Walk; // // - filepath.SkipDir is not supported; // // - if more than one walkFn instance will return an error, only one // of such errors will be propagated and returned by Walk, others // will be silently discarded. // // Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir.Walk] func Walk(root string, walkFn WalkFunc) error { return WalkN(root, walkFn, runtime.NumCPU()*2) } // WalkN is a wrapper for filepath.Walk which can call multiple walkFn // in parallel, allowing to handle each item concurrently. A maximum of // num walkFn will be called at any one time. // // Please see Walk documentation for caveats of using this function. // // Deprecated: use [github.com/opencontainers/selinux/pkg/pwalkdir.WalkN] func WalkN(root string, walkFn WalkFunc, num int) error { // make sure limit is sensible if num < 1 { return fmt.Errorf("walk(%q): num must be > 0", root) } files := make(chan *walkArgs, 2*num) errCh := make(chan error, 1) // get the first error, ignore others // Start walking a tree asap var ( err error wg sync.WaitGroup rootLen = len(root) rootEntry *walkArgs ) wg.Add(1) go func() { err = filepath.Walk(root, func(p string, info os.FileInfo, err error) error { if err != nil { // Walking a file tree can race with removal, // so ignore ENOENT, except for root. // https://github.com/opencontainers/selinux/issues/199. if errors.Is(err, os.ErrNotExist) && len(p) != rootLen { return nil } close(files) return err } if len(p) == rootLen { // Root entry is processed separately below. rootEntry = &walkArgs{path: p, info: &info} return nil } // add a file to the queue unless a callback sent an error select { case e := <-errCh: close(files) return e default: files <- &walkArgs{path: p, info: &info} return nil } }) if err == nil { close(files) } wg.Done() }() wg.Add(num) for i := 0; i < num; i++ { go func() { for file := range files { if e := walkFn(file.path, *file.info, nil); e != nil { select { case errCh <- e: // sent ok default: // buffer full } } } wg.Done() }() } wg.Wait() if err == nil { err = walkFn(rootEntry.path, *rootEntry.info, nil) } return err } // walkArgs holds the arguments that were passed to the Walk or WalkN // functions. type walkArgs struct { info *os.FileInfo path string } selinux-1.11.1/pkg/pwalk/pwalk_test.go000066400000000000000000000131461465465537600177170ustar00rootroot00000000000000package 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 := prepareTestSet(t, 3, 2, 1) 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", concurrency, count) } func TestWalkTopLevelErrNotExistNotIgnored(t *testing.T) { if WalkN("non-existent-directory", cbEmpty, 8) == nil { t.Fatal("expected ErrNotExist, got nil") } } // https://github.com/opencontainers/selinux/issues/199 func TestWalkRaceWithRemoval(t *testing.T) { var count uint32 concurrency := runtime.NumCPU() * 2 // This test is still on a best-effort basis, meaning it can still pass // when there is a bug in the code, but the larger the test set is, the // higher the probability that this test fails (without a fix). // // With this set (4, 5, 6), and the fix commented out, it fails // 100 out of 100 runs on my machine. dir, total := prepareTestSet(t, 4, 5, 6) // Race walk with removal. go os.RemoveAll(dir) err := WalkN(dir, func(_ string, _ os.FileInfo, _ error) error { atomic.AddUint32(&count, 1) return nil }, concurrency) t.Logf("found %d of %d files", count, total) if err != nil { t.Fatalf("expected nil, got %v", err) } } func TestWalkDirManyErrors(t *testing.T) { var count uint32 dir, total := prepareTestSet(t, 3, 3, 2) max := uint32(total / 2) e42 := errors.New("42") err := Walk(dir, func(_ string, _ 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(tb testing.TB, levels, dirs, files int) (dir string, total int) { tb.Helper() var err error dir, err = os.MkdirTemp(".", "pwalk-test-") if err != nil { tb.Fatal(err) } tb.Cleanup(func() { if err := os.RemoveAll(dir); err != nil && !errors.Is(err, os.ErrNotExist) { tb.Errorf("cleanup error: %v", err) } }) total, err = makeManyDirs(dir, levels, dirs, files) if err != nil { tb.Fatal(err) } 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 := prepareTestSet(b, levels, dirs, files) b.Logf("dataset: %d levels x %d dirs x %d files, total entries: %d", levels, dirs, files, total) for _, bm := range benchmarks { for _, w := range walkers { walker := w.walker walkFn := bm.walk // preheat if err := w.walker(dir, bm.walk); err != nil { b.Errorf("walk failed: %v", err) } // benchmark b.Run(bm.name+"/"+w.name, func(b *testing.B) { for i := 0; i < b.N; i++ { if err := walker(dir, walkFn); err != nil { b.Errorf("walk failed: %v", err) } } }) } } } func cbEmpty(_ string, _ os.FileInfo, _ error) error { return nil } func cbChownChmod(path string, info os.FileInfo, _ error) error { _ = os.Chown(path, 0, 0) mode := os.FileMode(0o644) if info.Mode().IsDir() { mode = os.FileMode(0o755) } _ = os.Chmod(path, mode) return nil } func cbReadFile(path string, info os.FileInfo, _ error) error { var err error if info.Mode().IsRegular() { _, err = os.ReadFile(path) } return err } func cbRandomSleep(_ string, _ os.FileInfo, _ error) error { time.Sleep(time.Duration(rand.Intn(500)) * time.Microsecond) //nolint:gosec // ignore G404: Use of weak random number generator return nil } selinux-1.11.1/pkg/pwalkdir/000077500000000000000000000000001465465537600157055ustar00rootroot00000000000000selinux-1.11.1/pkg/pwalkdir/README.md000066400000000000000000000041011465465537600171600ustar00rootroot00000000000000## pwalkdir: parallel implementation of filepath.WalkDir This is a wrapper for [filepath.WalkDir](https://pkg.go.dev/path/filepath#WalkDir) which may speed it up by calling multiple callback functions (WalkDirFunc) in parallel, utilizing goroutines. By default, it utilizes 2\*runtime.NumCPU() goroutines for callbacks. This can be changed by using WalkN function which has the additional parameter, specifying the number of goroutines (concurrency). ### pwalk vs pwalkdir This package is very similar to [pwalk](https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir), but utilizes `filepath.WalkDir` (added to Go 1.16), which does not call stat(2) on every entry and is therefore faster (up to 3x, depending on usage scenario). Users who are OK with requiring Go 1.16+ should switch to this implementation. ### Caveats Please note the following limitations of this code: * Unlike filepath.WalkDir, the order of calls is non-deterministic; * Only primitive error handling is supported: * fs.SkipDir is not supported; * ErrNotExist errors from filepath.WalkDir are silently ignored for any path except the top directory (WalkDir argument); any other error is returned to the caller of WalkDir; * once any error is returned from any walkDirFunc instance, no more calls to WalkDirFunc are made, and the error is returned to the caller of WalkDir; * if more than one WalkDirFunc instance will return an error, only one of such errors will be propagated to and returned by WalkDir, others will be silently discarded. ### Documentation For the official documentation, see https://pkg.go.dev/github.com/opencontainers/selinux/pkg/pwalkdir ### Benchmarks For a WalkDirFunc that consists solely of the return statement, this implementation is about 15% slower than the standard library's filepath.WalkDir. Otherwise (if a WalkDirFunc is actually doing something) this is usually faster, except when the WalkDirN(..., 1) is used. Run `go test -bench .` to see how different operations can benefit from it, as well as how the level of parallelism affects the speed. selinux-1.11.1/pkg/pwalkdir/pwalkdir.go000066400000000000000000000056451465465537600200630ustar00rootroot00000000000000//go:build go1.16 // +build go1.16 package pwalkdir import ( "errors" "fmt" "io/fs" "path/filepath" "runtime" "sync" ) // Walk is a wrapper for filepath.WalkDir which can call multiple walkFn // in parallel, allowing to handle each item concurrently. A maximum of // twice the runtime.NumCPU() walkFn will be called at any one time. // If you want to change the maximum, use WalkN instead. // // The order of calls is non-deterministic. // // Note that this implementation only supports primitive error handling: // // - no errors are ever passed to walkFn; // // - once a walkFn returns any error, all further processing stops // and the error is returned to the caller of Walk; // // - filepath.SkipDir is not supported; // // - if more than one walkFn instance will return an error, only one // of such errors will be propagated and returned by Walk, others // will be silently discarded. func Walk(root string, walkFn fs.WalkDirFunc) error { return WalkN(root, walkFn, runtime.NumCPU()*2) } // WalkN is a wrapper for filepath.WalkDir which can call multiple walkFn // in parallel, allowing to handle each item concurrently. A maximum of // num walkFn will be called at any one time. // // Please see Walk documentation for caveats of using this function. func WalkN(root string, walkFn fs.WalkDirFunc, num int) error { // make sure limit is sensible if num < 1 { return fmt.Errorf("walk(%q): num must be > 0", root) } files := make(chan *walkArgs, 2*num) errCh := make(chan error, 1) // Get the first error, ignore others. // Start walking a tree asap. var ( err error wg sync.WaitGroup rootLen = len(root) rootEntry *walkArgs ) wg.Add(1) go func() { err = filepath.WalkDir(root, func(p string, entry fs.DirEntry, err error) error { if err != nil { // Walking a file tree can race with removal, // so ignore ENOENT, except for root. // https://github.com/opencontainers/selinux/issues/199. if errors.Is(err, fs.ErrNotExist) && len(p) != rootLen { return nil } close(files) return err } if len(p) == rootLen { // Root entry is processed separately below. rootEntry = &walkArgs{path: p, entry: entry} return nil } // Add a file to the queue unless a callback sent an error. select { case e := <-errCh: close(files) return e default: files <- &walkArgs{path: p, entry: entry} return nil } }) if err == nil { close(files) } wg.Done() }() wg.Add(num) for i := 0; i < num; i++ { go func() { for file := range files { if e := walkFn(file.path, file.entry, nil); e != nil { select { case errCh <- e: // sent ok default: // buffer full } } } wg.Done() }() } wg.Wait() if err == nil { err = walkFn(rootEntry.path, rootEntry.entry, nil) } return err } // walkArgs holds the arguments that were passed to the Walk or WalkN // functions. type walkArgs struct { entry fs.DirEntry path string } selinux-1.11.1/pkg/pwalkdir/pwalkdir_test.go000066400000000000000000000133101465465537600211060ustar00rootroot00000000000000//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 := prepareTestSet(t, 3, 2, 1) 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", concurrency, count) } func TestWalkDirTopLevelErrNotExistNotIgnored(t *testing.T) { err := WalkN("non-existent-directory", cbEmpty, 8) if err == nil { t.Fatal("expected ErrNotExist, got nil") } } // https://github.com/opencontainers/selinux/issues/199 func TestWalkDirRaceWithRemoval(t *testing.T) { var count uint32 concurrency := runtime.NumCPU() * 2 // This test is still on a best-effort basis, meaning it can still pass // when there is a bug in the code, but the larger the test set is, the // higher the probability that this test fails (without a fix). // // With this set (4, 5, 6), and the fix commented out, it fails // about 90 out of 100 runs on my machine. dir, total := prepareTestSet(t, 4, 5, 6) // Make walk race with removal. go os.RemoveAll(dir) err := WalkN(dir, func(_ string, _ fs.DirEntry, _ error) error { atomic.AddUint32(&count, 1) return nil }, concurrency) t.Logf("found %d of %d files", count, total) if err != nil { t.Fatalf("expected nil, got %v", err) } } func TestWalkDirManyErrors(t *testing.T) { var count uint32 dir, total := prepareTestSet(t, 3, 3, 2) max := uint32(total / 2) e42 := errors.New("42") err := Walk(dir, func(_ string, _ 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(tb testing.TB, levels, dirs, files int) (dir string, total int) { tb.Helper() var err error dir, err = os.MkdirTemp(".", "pwalk-test-") if err != nil { tb.Fatal(err) } tb.Cleanup(func() { if err := os.RemoveAll(dir); err != nil && !errors.Is(err, os.ErrNotExist) { tb.Errorf("cleanup error: %v", err) } }) total, err = makeManyDirs(dir, levels, dirs, files) if err != nil { tb.Fatal(err) } 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 := prepareTestSet(b, levels, dirs, files) b.Logf("dataset: %d levels x %d dirs x %d files, total entries: %d", levels, dirs, files, total) for _, bm := range benchmarks { for _, w := range walkers { walker := w.walker walkFn := bm.walk // preheat if err := w.walker(dir, bm.walk); err != nil { b.Errorf("walk failed: %v", err) } // benchmark b.Run(bm.name+"/"+w.name, func(b *testing.B) { for i := 0; i < b.N; i++ { if err := walker(dir, walkFn); err != nil { b.Errorf("walk failed: %v", err) } } }) } } } func cbEmpty(_ string, _ fs.DirEntry, _ error) error { return nil } func cbChownChmod(path string, e fs.DirEntry, _ error) error { _ = os.Chown(path, 0, 0) mode := os.FileMode(0o644) if e.IsDir() { mode = os.FileMode(0o755) } _ = os.Chmod(path, mode) return nil } func cbReadFile(path string, e fs.DirEntry, _ error) error { var err error if e.Type().IsRegular() { _, err = os.ReadFile(path) } return err } func cbRandomSleep(_ string, _ fs.DirEntry, _ error) error { time.Sleep(time.Duration(rand.Intn(500)) * time.Microsecond) //nolint:gosec // ignore G404: Use of weak random number generator return nil }