Merge pull request #6403 from Luap99/revert-securejoin
Revert "fix(deps): update module github.com/cyphar/filepath-securejoin to v0.5.0"
This commit is contained in:
commit
b74149334e
2
go.mod
2
go.mod
|
@ -9,7 +9,7 @@ require (
|
||||||
github.com/containernetworking/cni v1.3.0
|
github.com/containernetworking/cni v1.3.0
|
||||||
github.com/containers/luksy v0.0.0-20250714213221-8fccf784694e
|
github.com/containers/luksy v0.0.0-20250714213221-8fccf784694e
|
||||||
github.com/containers/ocicrypt v1.2.1
|
github.com/containers/ocicrypt v1.2.1
|
||||||
github.com/cyphar/filepath-securejoin v0.5.0
|
github.com/cyphar/filepath-securejoin v0.4.1
|
||||||
github.com/docker/distribution v2.8.3+incompatible
|
github.com/docker/distribution v2.8.3+incompatible
|
||||||
github.com/docker/docker v28.4.0+incompatible
|
github.com/docker/docker v28.4.0+incompatible
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.6.0
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -62,8 +62,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
|
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
|
||||||
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
|
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
|
||||||
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
|
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||||
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
# Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
# Copyright (C) 2025 SUSE LLC
|
|
||||||
#
|
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
version: "2"
|
|
||||||
|
|
||||||
linters:
|
|
||||||
enable:
|
|
||||||
- asasalint
|
|
||||||
- asciicheck
|
|
||||||
- containedctx
|
|
||||||
- contextcheck
|
|
||||||
- errcheck
|
|
||||||
- errorlint
|
|
||||||
- exhaustive
|
|
||||||
- forcetypeassert
|
|
||||||
- godot
|
|
||||||
- goprintffuncname
|
|
||||||
- govet
|
|
||||||
- importas
|
|
||||||
- ineffassign
|
|
||||||
- makezero
|
|
||||||
- misspell
|
|
||||||
- musttag
|
|
||||||
- nilerr
|
|
||||||
- nilnesserr
|
|
||||||
- nilnil
|
|
||||||
- noctx
|
|
||||||
- prealloc
|
|
||||||
- revive
|
|
||||||
- staticcheck
|
|
||||||
- testifylint
|
|
||||||
- unconvert
|
|
||||||
- unparam
|
|
||||||
- unused
|
|
||||||
- usetesting
|
|
||||||
settings:
|
|
||||||
govet:
|
|
||||||
enable:
|
|
||||||
- nilness
|
|
||||||
testifylint:
|
|
||||||
enable-all: true
|
|
||||||
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
settings:
|
|
||||||
goimports:
|
|
||||||
local-prefixes:
|
|
||||||
- github.com/cyphar/filepath-securejoin
|
|
|
@ -6,122 +6,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## [Unreleased] ##
|
## [Unreleased] ##
|
||||||
|
|
||||||
## [0.5.0] - 2025-09-26 ##
|
|
||||||
|
|
||||||
> Let the past die. Kill it if you have to.
|
|
||||||
|
|
||||||
> **NOTE**: With this release, some parts of
|
|
||||||
> `github.com/cyphar/filepath-securejoin` are now licensed under the Mozilla
|
|
||||||
> Public License (version 2). Please see [COPYING.md][] as well as the the
|
|
||||||
> license header in each file for more details.
|
|
||||||
|
|
||||||
[COPYING.md]: ./COPYING.md
|
|
||||||
|
|
||||||
### Breaking ###
|
|
||||||
- The new API introduced in the [0.3.0][] release has been moved to a new
|
|
||||||
subpackage called `pathrs-lite`. This was primarily done to better indicate
|
|
||||||
the split between the new and old APIs, as well as indicate to users the
|
|
||||||
purpose of this subpackage (it is a less complete version of [libpathrs][]).
|
|
||||||
|
|
||||||
We have added some wrappers to the top-level package to ease the transition,
|
|
||||||
but those are deprecated and will be removed in the next minor release of
|
|
||||||
filepath-securejoin. Users should update their import paths.
|
|
||||||
|
|
||||||
This new subpackage has also been relicensed under the Mozilla Public License
|
|
||||||
(version 2), please see [COPYING.md][] for more details.
|
|
||||||
|
|
||||||
### Added ###
|
|
||||||
- Most of the key bits the safe `procfs` API have now been exported and are
|
|
||||||
available in `github.com/cyphar/filepath-securejoin/pathrs-lite/procfs`. At
|
|
||||||
the moment this primarily consists of a new `procfs.Handle` API:
|
|
||||||
|
|
||||||
* `OpenProcRoot` returns a new handle to `/proc`, endeavouring to make it
|
|
||||||
safe if possible (`subset=pid` to protect against mistaken write attacks
|
|
||||||
and leaks, as well as using `fsopen(2)` to avoid racing mount attacks).
|
|
||||||
|
|
||||||
`OpenUnsafeProcRoot` returns a handle without attempting to create one
|
|
||||||
with `subset=pid`, which makes it more dangerous to leak. Most users
|
|
||||||
should use `OpenProcRoot` (even if you need to use `ProcRoot` as the base
|
|
||||||
of an operation, as filepath-securejoin will internally open a handle when
|
|
||||||
necessary).
|
|
||||||
|
|
||||||
* The `(*procfs.Handle).Open*` family of methods lets you get a safe
|
|
||||||
`O_PATH` handle to subpaths within `/proc` for certain subpaths.
|
|
||||||
|
|
||||||
For `OpenThreadSelf`, the returned `ProcThreadSelfCloser` needs to be
|
|
||||||
called after you completely finish using the handle (this is necessary
|
|
||||||
because Go is multi-threaded and `ProcThreadSelf` references
|
|
||||||
`/proc/thread-self` which may disappear if we do not
|
|
||||||
`runtime.LockOSThread` -- `ProcThreadSelfCloser` is currently equivalent
|
|
||||||
to `runtime.UnlockOSThread`).
|
|
||||||
|
|
||||||
Note that you cannot open any `procfs` symlinks (most notably magic-links)
|
|
||||||
using this API. At the moment, filepath-securejoin does not support this
|
|
||||||
feature (but [libpathrs][] does).
|
|
||||||
|
|
||||||
* `ProcSelfFdReadlink` lets you get the in-kernel path representation of a
|
|
||||||
file descriptor (think `readlink("/proc/self/fd/...")`), except that we
|
|
||||||
verify that there aren't any tricky overmounts that could fool the
|
|
||||||
process.
|
|
||||||
|
|
||||||
Please be aware that the returned string is simply a snapshot at that
|
|
||||||
particular moment, and an attacker could move the file being pointed to.
|
|
||||||
In addition, complex namespace configurations could result in non-sensical
|
|
||||||
or confusing paths to be returned. The value received from this function
|
|
||||||
should only be used as secondary verification of some security property,
|
|
||||||
not as proof that a particular handle has a particular path.
|
|
||||||
|
|
||||||
The procfs handle used internally by the API is the same as the rest of
|
|
||||||
`filepath-securejoin` (for privileged programs this is usually a private
|
|
||||||
in-process `procfs` instance created with `fsopen(2)`).
|
|
||||||
|
|
||||||
As before, this is intended as a stop-gap before users migrate to
|
|
||||||
[libpathrs][], which provides a far more extensive safe `procfs` API and is
|
|
||||||
generally more robust.
|
|
||||||
|
|
||||||
- Previously, the hardened procfs implementation (used internally within
|
|
||||||
`Reopen` and `Open(at)InRoot`) only protected against overmount attacks on
|
|
||||||
systems with `openat2(2)` (Linux 5.6) or systems with `fsopen(2)` or
|
|
||||||
`open_tree(2)` (Linux 5.2) and programs with privileges to use them (with
|
|
||||||
some caveats about locked mounts that probably affect very few users). For
|
|
||||||
other users, an attacker with the ability to create malicious mounts (on most
|
|
||||||
systems, a sysadmin) could trick you into operating on files you didn't
|
|
||||||
expect. This attack only really makes sense in the context of container
|
|
||||||
runtime implementations.
|
|
||||||
|
|
||||||
This was considered a reasonable trade-off, as the long-term intention was to
|
|
||||||
get all users to just switch to [libpathrs][] if they wanted to use the safe
|
|
||||||
`procfs` API (which had more extensive protections, and is what these new
|
|
||||||
protections in `filepath-securejoin` are based on). However, as the API
|
|
||||||
is now being exported it seems unwise to advertise the API as "safe" if we do
|
|
||||||
not protect against known attacks.
|
|
||||||
|
|
||||||
The procfs API is now more protected against attackers on systems lacking the
|
|
||||||
aforementioned protections. However, the most comprehensive of these
|
|
||||||
protections effectively rely on [`statx(STATX_MNT_ID)`][statx.2] (Linux 5.8).
|
|
||||||
On older kernel versions, there is no effective protection (there is some
|
|
||||||
minimal protection against non-`procfs` filesystem components but a
|
|
||||||
sufficiently clever attacker can work around those). In addition,
|
|
||||||
`STATX_MNT_ID` is vulnerable to mount ID reuse attacks by sufficiently
|
|
||||||
motivated and privileged attackers -- this problem is mitigated with
|
|
||||||
`STATX_MNT_ID_UNIQUE` (Linux 6.8) but that raises the minimum kernel version
|
|
||||||
for more protection.
|
|
||||||
|
|
||||||
The fact that these protections are quite limited despite needing a fair bit
|
|
||||||
of extra code to handle was one of the primary reasons we did not initially
|
|
||||||
implement this in `filepath-securejoin` ([libpathrs][] supports all of this,
|
|
||||||
of course).
|
|
||||||
|
|
||||||
### Fixed ###
|
|
||||||
- RHEL 8 kernels have backports of `fsopen(2)` but in some testing we've found
|
|
||||||
that it has very bad (and very difficult to debug) performance issues, and so
|
|
||||||
we will explicitly refuse to use `fsopen(2)` if the running kernel version is
|
|
||||||
pre-5.2 and will instead fallback to `open("/proc")`.
|
|
||||||
|
|
||||||
[CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
|
|
||||||
[libpathrs]: https://github.com/cyphar/libpathrs
|
|
||||||
[statx.2]: https://www.man7.org/linux/man-pages/man2/statx.2.html
|
|
||||||
|
|
||||||
## [0.4.1] - 2025-01-28 ##
|
## [0.4.1] - 2025-01-28 ##
|
||||||
|
|
||||||
### Fixed ###
|
### Fixed ###
|
||||||
|
@ -289,7 +173,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
safe to start migrating to as we have extensive tests ensuring they behave
|
safe to start migrating to as we have extensive tests ensuring they behave
|
||||||
correctly and are safe against various races and other attacks.
|
correctly and are safe against various races and other attacks.
|
||||||
|
|
||||||
[libpathrs]: https://github.com/cyphar/libpathrs
|
[libpathrs]: https://github.com/openSUSE/libpathrs
|
||||||
[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
|
[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
|
||||||
|
|
||||||
## [0.2.5] - 2024-05-03 ##
|
## [0.2.5] - 2024-05-03 ##
|
||||||
|
@ -354,8 +238,7 @@ This is our first release of `github.com/cyphar/filepath-securejoin`,
|
||||||
containing a full implementation with a coverage of 93.5% (the only missing
|
containing a full implementation with a coverage of 93.5% (the only missing
|
||||||
cases are the error cases, which are hard to mocktest at the moment).
|
cases are the error cases, which are hard to mocktest at the moment).
|
||||||
|
|
||||||
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.5.0...HEAD
|
[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...HEAD
|
||||||
[0.5.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0
|
|
||||||
[0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1
|
[0.4.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.4.0...v0.4.1
|
||||||
[0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0
|
[0.4.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.6...v0.4.0
|
||||||
[0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6
|
[0.3.6]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.5...v0.3.6
|
||||||
|
|
|
@ -1,447 +0,0 @@
|
||||||
## COPYING ##
|
|
||||||
|
|
||||||
`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`
|
|
||||||
|
|
||||||
This project is made up of code licensed under different licenses. Which code
|
|
||||||
you use will have an impact on whether only one or both licenses apply to your
|
|
||||||
usage of this library.
|
|
||||||
|
|
||||||
Note that **each file** in this project individually has a code comment at the
|
|
||||||
start describing the license of that particular file -- this is the most
|
|
||||||
accurate license information of this project; in case there is any conflict
|
|
||||||
between this document and the comment at the start of a file, the comment shall
|
|
||||||
take precedence. The only purpose of this document is to work around [a known
|
|
||||||
technical limitation of pkg.go.dev's license checking tool when dealing with
|
|
||||||
non-trivial project licenses][go75067].
|
|
||||||
|
|
||||||
[go75067]: https://go.dev/issue/75067
|
|
||||||
|
|
||||||
### `BSD-3-Clause` ###
|
|
||||||
|
|
||||||
At time of writing, the following files and directories are licensed under the
|
|
||||||
BSD-3-Clause license:
|
|
||||||
|
|
||||||
* `doc.go`
|
|
||||||
* `join*.go`
|
|
||||||
* `vfs.go`
|
|
||||||
* `internal/consts/*.go`
|
|
||||||
* `pathrs-lite/internal/gocompat/*.go`
|
|
||||||
* `pathrs-lite/internal/kernelversion/*.go`
|
|
||||||
|
|
||||||
The text of the BSD-3-Clause license used by this project is the following (the
|
|
||||||
text is also available from the [`LICENSE.BSD`](./LICENSE.BSD) file):
|
|
||||||
|
|
||||||
```
|
|
||||||
Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
|
||||||
Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
```
|
|
||||||
|
|
||||||
### `MPL-2.0` ###
|
|
||||||
|
|
||||||
All other files (unless otherwise marked) are licensed under the Mozilla Public
|
|
||||||
License (version 2.0).
|
|
||||||
|
|
||||||
The text of the Mozilla Public License (version 2.0) is the following (the text
|
|
||||||
is also available from the [`LICENSE.MPL-2.0`](./LICENSE.MPL-2.0) file):
|
|
||||||
|
|
||||||
```
|
|
||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
||||||
```
|
|
|
@ -1,373 +0,0 @@
|
||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
|
@ -67,8 +67,7 @@ func SecureJoin(root, unsafePath string) (string, error) {
|
||||||
[libpathrs]: https://github.com/openSUSE/libpathrs
|
[libpathrs]: https://github.com/openSUSE/libpathrs
|
||||||
[go#20126]: https://github.com/golang/go/issues/20126
|
[go#20126]: https://github.com/golang/go/issues/20126
|
||||||
|
|
||||||
### <a name="new-api" /> New API ###
|
### New API ###
|
||||||
[#new-api]: #new-api
|
|
||||||
|
|
||||||
While we recommend users switch to [libpathrs][libpathrs] as soon as it has a
|
While we recommend users switch to [libpathrs][libpathrs] as soon as it has a
|
||||||
stable release, some methods implemented by libpathrs have been ported to this
|
stable release, some methods implemented by libpathrs have been ported to this
|
||||||
|
@ -166,19 +165,5 @@ after `MkdirAll`).
|
||||||
|
|
||||||
### License ###
|
### License ###
|
||||||
|
|
||||||
`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`
|
The license of this project is the same as Go, which is a BSD 3-clause license
|
||||||
|
available in the `LICENSE` file.
|
||||||
Some of the code in this project is derived from Go, and is licensed under a
|
|
||||||
BSD 3-clause license (available in `LICENSE.BSD`). Other files (many of which
|
|
||||||
are derived from [libpathrs][libpathrs]) are licensed under the Mozilla Public
|
|
||||||
License version 2.0 (available in `LICENSE.MPL-2.0`). If you are using the
|
|
||||||
["New API" described above][#new-api], you are probably using code from files
|
|
||||||
released under this license.
|
|
||||||
|
|
||||||
Every source file in this project has a copyright header describing its
|
|
||||||
license. Please check the license headers of each file to see what license
|
|
||||||
applies to it.
|
|
||||||
|
|
||||||
See [COPYING.md](./COPYING.md) for some more details.
|
|
||||||
|
|
||||||
[umoci]: https://github.com/opencontainers/umoci
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.5.0
|
0.4.1
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
# Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
# Copyright (C) 2025 SUSE LLC
|
|
||||||
#
|
|
||||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
comment:
|
|
||||||
layout: "condensed_header, reach, diff, components, condensed_files, condensed_footer"
|
|
||||||
require_changes: true
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
range: 60..100
|
|
||||||
status:
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: 85%
|
|
||||||
threshold: 0%
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
informational: true
|
|
||||||
|
|
||||||
github_checks:
|
|
||||||
annotations: false
|
|
|
@ -1,48 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package securejoin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// MkdirAll is a wrapper around [pathrs.MkdirAll].
|
|
||||||
//
|
|
||||||
// Deprecated: You should use [pathrs.MkdirAll] directly instead. This
|
|
||||||
// wrapper will be removed in filepath-securejoin v0.6.
|
|
||||||
MkdirAll = pathrs.MkdirAll
|
|
||||||
|
|
||||||
// MkdirAllHandle is a wrapper around [pathrs.MkdirAllHandle].
|
|
||||||
//
|
|
||||||
// Deprecated: You should use [pathrs.MkdirAllHandle] directly instead.
|
|
||||||
// This wrapper will be removed in filepath-securejoin v0.6.
|
|
||||||
MkdirAllHandle = pathrs.MkdirAllHandle
|
|
||||||
|
|
||||||
// OpenInRoot is a wrapper around [pathrs.OpenInRoot].
|
|
||||||
//
|
|
||||||
// Deprecated: You should use [pathrs.OpenInRoot] directly instead. This
|
|
||||||
// wrapper will be removed in filepath-securejoin v0.6.
|
|
||||||
OpenInRoot = pathrs.OpenInRoot
|
|
||||||
|
|
||||||
// OpenatInRoot is a wrapper around [pathrs.OpenatInRoot].
|
|
||||||
//
|
|
||||||
// Deprecated: You should use [pathrs.OpenatInRoot] directly instead. This
|
|
||||||
// wrapper will be removed in filepath-securejoin v0.6.
|
|
||||||
OpenatInRoot = pathrs.OpenatInRoot
|
|
||||||
|
|
||||||
// Reopen is a wrapper around [pathrs.Reopen].
|
|
||||||
//
|
|
||||||
// Deprecated: You should use [pathrs.Reopen] directly instead. This
|
|
||||||
// wrapper will be removed in filepath-securejoin v0.6.
|
|
||||||
Reopen = pathrs.Reopen
|
|
||||||
)
|
|
|
@ -1,5 +1,3 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
||||||
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
|
@ -16,13 +14,14 @@
|
||||||
// **not** safe against race conditions where an attacker changes the
|
// **not** safe against race conditions where an attacker changes the
|
||||||
// filesystem after (or during) the [SecureJoin] operation.
|
// filesystem after (or during) the [SecureJoin] operation.
|
||||||
//
|
//
|
||||||
// The new API is available in the [pathrs-lite] subpackage, and provide
|
// The new API is made up of [OpenInRoot] and [MkdirAll] (and derived
|
||||||
// protections against racing attackers as well as several other key
|
// functions). These are safe against racing attackers and have several other
|
||||||
// protections against attacks often seen by container runtimes. As the name
|
// protections that are not provided by the legacy API. There are many more
|
||||||
// suggests, [pathrs-lite] is a stripped down (pure Go) reimplementation of
|
// operations that most programs expect to be able to do safely, but we do not
|
||||||
// [libpathrs]. The main APIs provided are [OpenInRoot], [MkdirAll], and
|
// provide explicit support for them because we want to encourage users to
|
||||||
// [procfs.Handle] -- other APIs are not planned to be ported. The long-term
|
// switch to [libpathrs](https://github.com/openSUSE/libpathrs) which is a
|
||||||
// goal is for users to migrate to [libpathrs] which is more fully-featured.
|
// cross-language next-generation library that is entirely designed around
|
||||||
|
// operating on paths safely.
|
||||||
//
|
//
|
||||||
// securejoin has been used by several container runtimes (Docker, runc,
|
// securejoin has been used by several container runtimes (Docker, runc,
|
||||||
// Kubernetes, etc) for quite a few years as a de-facto standard for operating
|
// Kubernetes, etc) for quite a few years as a de-facto standard for operating
|
||||||
|
@ -32,16 +31,9 @@
|
||||||
// API as soon as possible (or even better, switch to libpathrs).
|
// API as soon as possible (or even better, switch to libpathrs).
|
||||||
//
|
//
|
||||||
// This project was initially intended to be included in the Go standard
|
// This project was initially intended to be included in the Go standard
|
||||||
// library, but it was rejected (see https://go.dev/issue/20126). Much later,
|
// library, but [it was rejected](https://go.dev/issue/20126). There is now a
|
||||||
// [os.Root] was added to the Go stdlib that shares some of the goals of
|
// [new Go proposal](https://go.dev/issue/67002) for a safe path resolution API
|
||||||
// filepath-securejoin. However, its design is intended to work like
|
// that shares some of the goals of filepath-securejoin. However, that design
|
||||||
// openat2(RESOLVE_BENEATH) which does not fit the usecase of container
|
// is intended to work like `openat2(RESOLVE_BENEATH)` which does not fit the
|
||||||
// runtimes and most system tools.
|
// usecase of container runtimes and most system tools.
|
||||||
//
|
|
||||||
// [pathrs-lite]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite
|
|
||||||
// [libpathrs]: https://github.com/openSUSE/libpathrs
|
|
||||||
// [OpenInRoot]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#OpenInRoot
|
|
||||||
// [MkdirAll]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite#MkdirAll
|
|
||||||
// [procfs.Handle]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs#Handle
|
|
||||||
// [os.Root]: https:///pkg.go.dev/os#Root
|
|
||||||
package securejoin
|
package securejoin
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
//go:build linux && go1.20
|
//go:build linux && go1.20
|
||||||
|
|
||||||
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package gocompat
|
package securejoin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
||||||
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
||||||
// is only guaranteed to give you baseErr.
|
// is only guaranteed to give you baseErr.
|
||||||
func WrapBaseError(baseErr, extraErr error) error {
|
func wrapBaseError(baseErr, extraErr error) error {
|
||||||
return fmt.Errorf("%w: %w", extraErr, baseErr)
|
return fmt.Errorf("%w: %w", extraErr, baseErr)
|
||||||
}
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build linux && !go1.20
|
//go:build linux && !go1.20
|
||||||
|
|
||||||
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package gocompat
|
package securejoin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -29,10 +27,10 @@ func (err wrappedError) Error() string {
|
||||||
return fmt.Sprintf("%v: %v", err.isError, err.inner)
|
return fmt.Sprintf("%v: %v", err.isError, err.inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
// wrapBaseError is a helper that is equivalent to fmt.Errorf("%w: %w"), except
|
||||||
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
// that on pre-1.20 Go versions only errors.Is() works properly (errors.Unwrap)
|
||||||
// is only guaranteed to give you baseErr.
|
// is only guaranteed to give you baseErr.
|
||||||
func WrapBaseError(baseErr, extraErr error) error {
|
func wrapBaseError(baseErr, extraErr error) error {
|
||||||
return wrappedError{
|
return wrappedError{
|
||||||
inner: baseErr,
|
inner: baseErr,
|
||||||
isError: extraErr,
|
isError: extraErr,
|
32
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go
generated
vendored
Normal file
32
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_go121.go
generated
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
//go:build linux && go1.21
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func slices_DeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S {
|
||||||
|
return slices.DeleteFunc(slice, delFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func slices_Contains[S ~[]E, E comparable](slice S, val E) bool {
|
||||||
|
return slices.Contains(slice, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func slices_Clone[S ~[]E, E any](slice S) S {
|
||||||
|
return slices.Clone(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sync_OnceValue[T any](f func() T) func() T {
|
||||||
|
return sync.OnceValue(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
||||||
|
return sync.OnceValues(f)
|
||||||
|
}
|
124
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go
generated
vendored
Normal file
124
vendor/github.com/cyphar/filepath-securejoin/gocompat_generics_unsupported.go
generated
vendored
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
//go:build linux && !go1.21
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are very minimal implementations of functions that appear in Go 1.21's
|
||||||
|
// stdlib, included so that we can build on older Go versions. Most are
|
||||||
|
// borrowed directly from the stdlib, and a few are modified to be "obviously
|
||||||
|
// correct" without needing to copy too many other helpers.
|
||||||
|
|
||||||
|
// clearSlice is equivalent to the builtin clear from Go 1.21.
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func clearSlice[S ~[]E, E any](slice S) {
|
||||||
|
var zero E
|
||||||
|
for i := range slice {
|
||||||
|
slice[i] = zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func slices_IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
||||||
|
for i := range s {
|
||||||
|
if f(s[i]) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func slices_DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
||||||
|
i := slices_IndexFunc(s, del)
|
||||||
|
if i == -1 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
// Don't start copying elements until we find one to delete.
|
||||||
|
for j := i + 1; j < len(s); j++ {
|
||||||
|
if v := s[j]; !del(v) {
|
||||||
|
s[i] = v
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to the stdlib slices.Contains, except that we don't have
|
||||||
|
// slices.Index so we need to use slices.IndexFunc for this non-Func helper.
|
||||||
|
func slices_Contains[S ~[]E, E comparable](s S, v E) bool {
|
||||||
|
return slices_IndexFunc(s, func(e E) bool { return e == v }) >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func slices_Clone[S ~[]E, E any](s S) S {
|
||||||
|
// Preserve nil in case it matters.
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return append(S([]E{}), s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func sync_OnceValue[T any](f func() T) func() T {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
valid bool
|
||||||
|
p any
|
||||||
|
result T
|
||||||
|
)
|
||||||
|
g := func() {
|
||||||
|
defer func() {
|
||||||
|
p = recover()
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
result = f()
|
||||||
|
f = nil
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
return func() T {
|
||||||
|
once.Do(g)
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copied from the Go 1.24 stdlib implementation.
|
||||||
|
func sync_OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
valid bool
|
||||||
|
p any
|
||||||
|
r1 T1
|
||||||
|
r2 T2
|
||||||
|
)
|
||||||
|
g := func() {
|
||||||
|
defer func() {
|
||||||
|
p = recover()
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
r1, r2 = f()
|
||||||
|
f = nil
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
return func() (T1, T2) {
|
||||||
|
once.Do(g)
|
||||||
|
if !valid {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
return r1, r2
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
|
||||||
// Copyright (C) 2017-2025 SUSE LLC. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package consts contains the definitions of internal constants used
|
|
||||||
// throughout filepath-securejoin.
|
|
||||||
package consts
|
|
||||||
|
|
||||||
// MaxSymlinkLimit is the maximum number of symlinks that can be encountered
|
|
||||||
// during a single lookup before returning -ELOOP. At time of writing, Linux
|
|
||||||
// has an internal limit of 40.
|
|
||||||
const MaxSymlinkLimit = 255
|
|
|
@ -1,5 +1,3 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
|
||||||
// Copyright (C) 2017-2025 SUSE LLC. All rights reserved.
|
// Copyright (C) 2017-2025 SUSE LLC. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
|
@ -13,10 +11,10 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/internal/consts"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const maxSymlinkLimit = 255
|
||||||
|
|
||||||
// IsNotExist tells you if err is an error that implies that either the path
|
// IsNotExist tells you if err is an error that implies that either the path
|
||||||
// accessed does not exist (or path components don't exist). This is
|
// accessed does not exist (or path components don't exist). This is
|
||||||
// effectively a more broad version of [os.IsNotExist].
|
// effectively a more broad version of [os.IsNotExist].
|
||||||
|
@ -51,13 +49,12 @@ func hasDotDot(path string) bool {
|
||||||
return strings.Contains("/"+path+"/", "/../")
|
return strings.Contains("/"+path+"/", "/../")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecureJoinVFS joins the two given path components (similar to
|
// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except
|
||||||
// [filepath.Join]) except that the returned path is guaranteed to be scoped
|
// that the returned path is guaranteed to be scoped inside the provided root
|
||||||
// inside the provided root path (when evaluated). Any symbolic links in the
|
// path (when evaluated). Any symbolic links in the path are evaluated with the
|
||||||
// path are evaluated with the given root treated as the root of the
|
// given root treated as the root of the filesystem, similar to a chroot. The
|
||||||
// filesystem, similar to a chroot. The filesystem state is evaluated through
|
// filesystem state is evaluated through the given [VFS] interface (if nil, the
|
||||||
// the given [VFS] interface (if nil, the standard [os].* family of functions
|
// standard [os].* family of functions are used).
|
||||||
// are used).
|
|
||||||
//
|
//
|
||||||
// Note that the guarantees provided by this function only apply if the path
|
// Note that the guarantees provided by this function only apply if the path
|
||||||
// components in the returned string are not modified (in other words are not
|
// components in the returned string are not modified (in other words are not
|
||||||
|
@ -81,7 +78,7 @@ func hasDotDot(path string) bool {
|
||||||
// fully resolved using [filepath.EvalSymlinks] or otherwise constructed to
|
// fully resolved using [filepath.EvalSymlinks] or otherwise constructed to
|
||||||
// avoid containing symlink components. Of course, the root also *must not* be
|
// avoid containing symlink components. Of course, the root also *must not* be
|
||||||
// attacker-controlled.
|
// attacker-controlled.
|
||||||
func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { //nolint:revive // name is part of public API
|
func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) {
|
||||||
// The root path must not contain ".." components, otherwise when we join
|
// The root path must not contain ".." components, otherwise when we join
|
||||||
// the subpath we will end up with a weird path. We could work around this
|
// the subpath we will end up with a weird path. We could work around this
|
||||||
// in other ways but users shouldn't be giving us non-lexical root paths in
|
// in other ways but users shouldn't be giving us non-lexical root paths in
|
||||||
|
@ -141,7 +138,7 @@ func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) { //nolint:
|
||||||
// It's a symlink, so get its contents and expand it by prepending it
|
// It's a symlink, so get its contents and expand it by prepending it
|
||||||
// to the yet-unparsed path.
|
// to the yet-unparsed path.
|
||||||
linksWalked++
|
linksWalked++
|
||||||
if linksWalked > consts.MaxSymlinkLimit {
|
if linksWalked > maxSymlinkLimit {
|
||||||
return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
|
return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
// Use of this source code is governed by a BSD-style
|
||||||
//
|
// license that can be found in the LICENSE file.
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package pathrs
|
package securejoin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -20,12 +15,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/internal/consts"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type symlinkStackEntry struct {
|
type symlinkStackEntry struct {
|
||||||
|
@ -123,12 +112,12 @@ func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Split the link target and clean up any "" parts.
|
// Split the link target and clean up any "" parts.
|
||||||
linkTargetParts := gocompat.SlicesDeleteFunc(
|
linkTargetParts := slices_DeleteFunc(
|
||||||
strings.Split(linkTarget, "/"),
|
strings.Split(linkTarget, "/"),
|
||||||
func(part string) bool { return part == "" || part == "." })
|
func(part string) bool { return part == "" || part == "." })
|
||||||
|
|
||||||
// Copy the directory so the caller doesn't close our copy.
|
// Copy the directory so the caller doesn't close our copy.
|
||||||
dirCopy, err := fd.Dup(dir)
|
dirCopy, err := dupFile(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -170,11 +159,11 @@ func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
|
||||||
// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
|
// within the provided root (a-la RESOLVE_IN_ROOT) and opens the final existing
|
||||||
// component of the requested path, returning a file handle to the final
|
// component of the requested path, returning a file handle to the final
|
||||||
// existing component and a string containing the remaining path components.
|
// existing component and a string containing the remaining path components.
|
||||||
func partialLookupInRoot(root fd.Fd, unsafePath string) (*os.File, string, error) {
|
func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) {
|
||||||
return lookupInRoot(root, unsafePath, true)
|
return lookupInRoot(root, unsafePath, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeLookupInRoot(root fd.Fd, unsafePath string) (*os.File, error) {
|
func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||||
handle, remainingPath, err := lookupInRoot(root, unsafePath, false)
|
handle, remainingPath, err := lookupInRoot(root, unsafePath, false)
|
||||||
if remainingPath != "" && err == nil {
|
if remainingPath != "" && err == nil {
|
||||||
// should never happen
|
// should never happen
|
||||||
|
@ -185,7 +174,7 @@ func completeLookupInRoot(root fd.Fd, unsafePath string) (*os.File, error) {
|
||||||
return handle, err
|
return handle, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
|
func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
|
||||||
unsafePath = filepath.ToSlash(unsafePath) // noop
|
unsafePath = filepath.ToSlash(unsafePath) // noop
|
||||||
|
|
||||||
// This is very similar to SecureJoin, except that we operate on the
|
// This is very similar to SecureJoin, except that we operate on the
|
||||||
|
@ -193,20 +182,20 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
|
||||||
// managed open, along with the remaining path components not opened.
|
// managed open, along with the remaining path components not opened.
|
||||||
|
|
||||||
// Try to use openat2 if possible.
|
// Try to use openat2 if possible.
|
||||||
if linux.HasOpenat2() {
|
if hasOpenat2() {
|
||||||
return lookupOpenat2(root, unsafePath, partial)
|
return lookupOpenat2(root, unsafePath, partial)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the "actual" root path from /proc/self/fd. This is necessary if the
|
// Get the "actual" root path from /proc/self/fd. This is necessary if the
|
||||||
// root is some magic-link like /proc/$pid/root, in which case we want to
|
// root is some magic-link like /proc/$pid/root, in which case we want to
|
||||||
// make sure when we do procfs.CheckProcSelfFdPath that we are using the
|
// make sure when we do checkProcSelfFdPath that we are using the correct
|
||||||
// correct root path.
|
// root path.
|
||||||
logicalRootPath, err := procfs.ProcSelfFdReadlink(root)
|
logicalRootPath, err := procSelfFdReadlink(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("get real root path: %w", err)
|
return nil, "", fmt.Errorf("get real root path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentDir, err := fd.Dup(root)
|
currentDir, err := dupFile(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -271,7 +260,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
|
||||||
return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err)
|
return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err)
|
||||||
}
|
}
|
||||||
// Jump to root.
|
// Jump to root.
|
||||||
rootClone, err := fd.Dup(root)
|
rootClone, err := dupFile(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -282,21 +271,21 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to open the next component.
|
// Try to open the next component.
|
||||||
nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
nextDir, err := openatFile(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||||
switch err {
|
switch {
|
||||||
case nil:
|
case err == nil:
|
||||||
st, err := nextDir.Stat()
|
st, err := nextDir.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = nextDir.Close()
|
_ = nextDir.Close()
|
||||||
return nil, "", fmt.Errorf("stat component %q: %w", part, err)
|
return nil, "", fmt.Errorf("stat component %q: %w", part, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch st.Mode() & os.ModeType { //nolint:exhaustive // just a glorified if statement
|
switch st.Mode() & os.ModeType {
|
||||||
case os.ModeSymlink:
|
case os.ModeSymlink:
|
||||||
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
|
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
|
||||||
// Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
|
// Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
|
||||||
// fstatat() with empty relative pathnames").
|
// fstatat() with empty relative pathnames").
|
||||||
linkDest, err := fd.Readlinkat(nextDir, "")
|
linkDest, err := readlinkatFile(nextDir, "")
|
||||||
// We don't need the handle anymore.
|
// We don't need the handle anymore.
|
||||||
_ = nextDir.Close()
|
_ = nextDir.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -304,7 +293,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
|
||||||
}
|
}
|
||||||
|
|
||||||
linksWalked++
|
linksWalked++
|
||||||
if linksWalked > consts.MaxSymlinkLimit {
|
if linksWalked > maxSymlinkLimit {
|
||||||
return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP}
|
return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +307,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
|
||||||
// Absolute symlinks reset any work we've already done.
|
// Absolute symlinks reset any work we've already done.
|
||||||
if path.IsAbs(linkDest) {
|
if path.IsAbs(linkDest) {
|
||||||
// Jump to root.
|
// Jump to root.
|
||||||
rootClone, err := fd.Dup(root)
|
rootClone, err := dupFile(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
return nil, "", fmt.Errorf("clone root fd: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -346,12 +335,12 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
|
||||||
// rename or mount on the system.
|
// rename or mount on the system.
|
||||||
if part == ".." {
|
if part == ".." {
|
||||||
// Make sure the root hasn't moved.
|
// Make sure the root hasn't moved.
|
||||||
if err := procfs.CheckProcSelfFdPath(logicalRootPath, root); err != nil {
|
if err := checkProcSelfFdPath(logicalRootPath, root); err != nil {
|
||||||
return nil, "", fmt.Errorf("root path moved during lookup: %w", err)
|
return nil, "", fmt.Errorf("root path moved during lookup: %w", err)
|
||||||
}
|
}
|
||||||
// Make sure the path is what we expect.
|
// Make sure the path is what we expect.
|
||||||
fullPath := logicalRootPath + nextPath
|
fullPath := logicalRootPath + nextPath
|
||||||
if err := procfs.CheckProcSelfFdPath(fullPath, currentDir); err != nil {
|
if err := checkProcSelfFdPath(fullPath, currentDir); err != nil {
|
||||||
return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err)
|
return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,7 +371,7 @@ func lookupInRoot(root fd.Fd, unsafePath string, partial bool) (Handle *os.File,
|
||||||
// context of openat2, a trailing slash and a trailing "/." are completely
|
// context of openat2, a trailing slash and a trailing "/." are completely
|
||||||
// equivalent.
|
// equivalent.
|
||||||
if strings.HasSuffix(unsafePath, "/") {
|
if strings.HasSuffix(unsafePath, "/") {
|
||||||
nextDir, err := fd.Openat(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
nextDir, err := openatFile(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !partial {
|
if !partial {
|
||||||
_ = currentDir.Close()
|
_ = currentDir.Close()
|
|
@ -1,15 +1,10 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
// Use of this source code is governed by a BSD-style
|
||||||
//
|
// license that can be found in the LICENSE file.
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package pathrs
|
package securejoin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -19,13 +14,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var errInvalidMode = errors.New("invalid permission mode")
|
var (
|
||||||
|
errInvalidMode = errors.New("invalid permission mode")
|
||||||
|
errPossibleAttack = errors.New("possible attack detected")
|
||||||
|
)
|
||||||
|
|
||||||
// modePermExt is like os.ModePerm except that it also includes the set[ug]id
|
// modePermExt is like os.ModePerm except that it also includes the set[ug]id
|
||||||
// and sticky bits.
|
// and sticky bits.
|
||||||
|
@ -72,8 +66,6 @@ func toUnixMode(mode os.FileMode) (uint32, error) {
|
||||||
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
|
// a brand new lookup of unsafePath (such as with [SecureJoin] or openat2) after
|
||||||
// doing [MkdirAll]. If you intend to open the directory after creating it, you
|
// doing [MkdirAll]. If you intend to open the directory after creating it, you
|
||||||
// should use MkdirAllHandle.
|
// should use MkdirAllHandle.
|
||||||
//
|
|
||||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
|
||||||
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) {
|
func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.File, Err error) {
|
||||||
unixMode, err := toUnixMode(mode)
|
unixMode, err := toUnixMode(mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -110,7 +102,7 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||||
//
|
//
|
||||||
// This is mostly a quality-of-life check, because mkdir will simply fail
|
// This is mostly a quality-of-life check, because mkdir will simply fail
|
||||||
// later if the attacker deletes the tree after this check.
|
// later if the attacker deletes the tree after this check.
|
||||||
if err := fd.IsDeadInode(currentDir); err != nil {
|
if err := isDeadInode(currentDir); err != nil {
|
||||||
return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err)
|
return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,13 +113,13 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||||
return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
|
return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
|
return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
|
||||||
} else { //nolint:revive // indent-error-flow lint doesn't make sense here
|
} else {
|
||||||
_ = currentDir.Close()
|
_ = currentDir.Close()
|
||||||
currentDir = reopenDir
|
currentDir = reopenDir
|
||||||
}
|
}
|
||||||
|
|
||||||
remainingParts := strings.Split(remainingPath, string(filepath.Separator))
|
remainingParts := strings.Split(remainingPath, string(filepath.Separator))
|
||||||
if gocompat.SlicesContains(remainingParts, "..") {
|
if slices_Contains(remainingParts, "..") {
|
||||||
// The path contained ".." components after the end of the "real"
|
// The path contained ".." components after the end of the "real"
|
||||||
// components. We could try to safely resolve ".." here but that would
|
// components. We could try to safely resolve ".." here but that would
|
||||||
// add a bunch of extra logic for something that it's not clear even
|
// add a bunch of extra logic for something that it's not clear even
|
||||||
|
@ -158,12 +150,12 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||||
if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) {
|
if err := unix.Mkdirat(int(currentDir.Fd()), part, unixMode); err != nil && !errors.Is(err, unix.EEXIST) {
|
||||||
err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
|
err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
|
||||||
// Make the error a bit nicer if the directory is dead.
|
// Make the error a bit nicer if the directory is dead.
|
||||||
if deadErr := fd.IsDeadInode(currentDir); deadErr != nil {
|
if deadErr := isDeadInode(currentDir); deadErr != nil {
|
||||||
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||||
// multiple %w verbs for this wrapping. For now we need to use a
|
// multiple %w verbs for this wrapping. For now we need to use a
|
||||||
// compatibility shim for older Go versions.
|
// compatibility shim for older Go versions.
|
||||||
// err = fmt.Errorf("%w (%w)", err, deadErr)
|
//err = fmt.Errorf("%w (%w)", err, deadErr)
|
||||||
err = gocompat.WrapBaseError(err, deadErr)
|
err = wrapBaseError(err, deadErr)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -171,13 +163,13 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||||
// Get a handle to the next component. O_DIRECTORY means we don't need
|
// Get a handle to the next component. O_DIRECTORY means we don't need
|
||||||
// to use O_PATH.
|
// to use O_PATH.
|
||||||
var nextDir *os.File
|
var nextDir *os.File
|
||||||
if linux.HasOpenat2() {
|
if hasOpenat2() {
|
||||||
nextDir, err = openat2(currentDir, part, &unix.OpenHow{
|
nextDir, err = openat2File(currentDir, part, &unix.OpenHow{
|
||||||
Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC,
|
Flags: unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC,
|
||||||
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
|
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
nextDir, err = fd.Openat(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
nextDir, err = openatFile(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -228,14 +220,12 @@ func MkdirAllHandle(root *os.File, unsafePath string, mode os.FileMode) (_ *os.F
|
||||||
// If you plan to open the directory after you have created it or want to use
|
// If you plan to open the directory after you have created it or want to use
|
||||||
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
|
// an open directory handle as the root, you should use [MkdirAllHandle] instead.
|
||||||
// This function is a wrapper around [MkdirAllHandle].
|
// This function is a wrapper around [MkdirAllHandle].
|
||||||
//
|
|
||||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
|
||||||
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
|
func MkdirAll(root, unsafePath string, mode os.FileMode) error {
|
||||||
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
|
defer rootDir.Close()
|
||||||
|
|
||||||
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
|
f, err := MkdirAllHandle(rootDir, unsafePath, mode)
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -1,22 +1,17 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
// Use of this source code is governed by a BSD-style
|
||||||
//
|
// license that can be found in the LICENSE file.
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package pathrs
|
package securejoin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
|
||||||
|
@ -45,14 +40,12 @@ func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
|
||||||
// disconnected TTY that could cause a DoS, or some other issue). In order to
|
// disconnected TTY that could cause a DoS, or some other issue). In order to
|
||||||
// use the returned handle, you can "upgrade" it to a proper handle using
|
// use the returned handle, you can "upgrade" it to a proper handle using
|
||||||
// [Reopen].
|
// [Reopen].
|
||||||
//
|
|
||||||
// [SecureJoin]: https://pkg.go.dev/github.com/cyphar/filepath-securejoin#SecureJoin
|
|
||||||
func OpenInRoot(root, unsafePath string) (*os.File, error) {
|
func OpenInRoot(root, unsafePath string) (*os.File, error) {
|
||||||
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rootDir.Close() //nolint:errcheck // close failures aren't critical here
|
defer rootDir.Close()
|
||||||
return OpenatInRoot(rootDir, unsafePath)
|
return OpenatInRoot(rootDir, unsafePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,5 +63,41 @@ func OpenInRoot(root, unsafePath string) (*os.File, error) {
|
||||||
//
|
//
|
||||||
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
|
// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
|
||||||
func Reopen(handle *os.File, flags int) (*os.File, error) {
|
func Reopen(handle *os.File, flags int) (*os.File, error) {
|
||||||
return procfs.ReopenFd(handle, flags)
|
procRoot, err := getProcRoot()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't operate on /proc/thread-self/fd/$n directly when doing a
|
||||||
|
// re-open, so we need to open /proc/thread-self/fd and then open a single
|
||||||
|
// final component.
|
||||||
|
procFdDir, closer, err := procThreadSelf(procRoot, "fd/")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err)
|
||||||
|
}
|
||||||
|
defer procFdDir.Close()
|
||||||
|
defer closer()
|
||||||
|
|
||||||
|
// Try to detect if there is a mount on top of the magic-link we are about
|
||||||
|
// to open. If we are using unsafeHostProcRoot(), this could change after
|
||||||
|
// we check it (and there's nothing we can do about that) but for
|
||||||
|
// privateProcRoot() this should be guaranteed to be safe (at least since
|
||||||
|
// Linux 5.12[1], when anonymous mount namespaces were completely isolated
|
||||||
|
// from external mounts including mount propagation events).
|
||||||
|
//
|
||||||
|
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
||||||
|
// onto targets that reside on shared mounts").
|
||||||
|
fdStr := strconv.Itoa(int(handle.Fd()))
|
||||||
|
if err := checkSymlinkOvermount(procRoot, procFdDir, fdStr); err != nil {
|
||||||
|
return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
flags |= unix.O_CLOEXEC
|
||||||
|
// Rather than just wrapping openatFile, open-code it so we can copy
|
||||||
|
// handle.Name().
|
||||||
|
reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(reopenFd), handle.Name()), nil
|
||||||
}
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var hasOpenat2 = sync_OnceValue(func() bool {
|
||||||
|
fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ = unix.Close(fd)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
|
||||||
|
// RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
|
||||||
|
// ".." while a mount or rename occurs anywhere on the system. This could
|
||||||
|
// happen spuriously, or as the result of an attacker trying to mess with
|
||||||
|
// us during lookup.
|
||||||
|
//
|
||||||
|
// In addition, scoped lookups have a "safety check" at the end of
|
||||||
|
// complete_walk which will return -EXDEV if the final path is not in the
|
||||||
|
// root.
|
||||||
|
return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
|
||||||
|
(errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopedLookupMaxRetries = 10
|
||||||
|
|
||||||
|
func openat2File(dir *os.File, path string, how *unix.OpenHow) (*os.File, error) {
|
||||||
|
fullPath := dir.Name() + "/" + path
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
how.Flags |= unix.O_CLOEXEC
|
||||||
|
var tries int
|
||||||
|
for tries < scopedLookupMaxRetries {
|
||||||
|
fd, err := unix.Openat2(int(dir.Fd()), path, how)
|
||||||
|
if err != nil {
|
||||||
|
if scopedLookupShouldRetry(how, err) {
|
||||||
|
// We retry a couple of times to avoid the spurious errors, and
|
||||||
|
// if we are being attacked then returning -EAGAIN is the best
|
||||||
|
// we can do.
|
||||||
|
tries++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
|
||||||
|
}
|
||||||
|
// If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
|
||||||
|
// NOTE: The procRoot code MUST NOT use RESOLVE_IN_ROOT, otherwise
|
||||||
|
// you'll get infinite recursion here.
|
||||||
|
if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT {
|
||||||
|
if actualPath, err := rawProcSelfFdReadlink(fd); err == nil {
|
||||||
|
fullPath = actualPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), fullPath), nil
|
||||||
|
}
|
||||||
|
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: errPossibleAttack}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupOpenat2(root *os.File, unsafePath string, partial bool) (*os.File, string, error) {
|
||||||
|
if !partial {
|
||||||
|
file, err := openat2File(root, unsafePath, &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
||||||
|
})
|
||||||
|
return file, "", err
|
||||||
|
}
|
||||||
|
return partialLookupOpenat2(root, unsafePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// partialLookupOpenat2 is an alternative implementation of
|
||||||
|
// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a
|
||||||
|
// handle to the deepest existing child of the requested path within the root.
|
||||||
|
func partialLookupOpenat2(root *os.File, unsafePath string) (*os.File, string, error) {
|
||||||
|
// TODO: Implement this as a git-bisect-like binary search.
|
||||||
|
|
||||||
|
unsafePath = filepath.ToSlash(unsafePath) // noop
|
||||||
|
endIdx := len(unsafePath)
|
||||||
|
var lastError error
|
||||||
|
for endIdx > 0 {
|
||||||
|
subpath := unsafePath[:endIdx]
|
||||||
|
|
||||||
|
handle, err := openat2File(root, subpath, &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
// Jump over the slash if we have a non-"" remainingPath.
|
||||||
|
if endIdx < len(unsafePath) {
|
||||||
|
endIdx += 1
|
||||||
|
}
|
||||||
|
// We found a subpath!
|
||||||
|
return handle, unsafePath[endIdx:], lastError
|
||||||
|
}
|
||||||
|
if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
|
||||||
|
// That path doesn't exist, let's try the next directory up.
|
||||||
|
endIdx = strings.LastIndexByte(subpath, '/')
|
||||||
|
lastError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, "", fmt.Errorf("open subpath: %w", err)
|
||||||
|
}
|
||||||
|
// If we couldn't open anything, the whole subpath is missing. Return a
|
||||||
|
// copy of the root fd so that the caller doesn't close this one by
|
||||||
|
// accident.
|
||||||
|
rootClone, err := dupFile(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return rootClone, unsafePath, lastError
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dupFile(f *os.File) (*os.File, error) {
|
||||||
|
fd, err := unix.FcntlInt(f.Fd(), unix.F_DUPFD_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), f.Name()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openatFile(dir *os.File, path string, flags int, mode int) (*os.File, error) {
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.O_CLOEXEC
|
||||||
|
fd, err := unix.Openat(int(dir.Fd()), path, flags, uint32(mode))
|
||||||
|
if err != nil {
|
||||||
|
return nil, &os.PathError{Op: "openat", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
// All of the paths we use with openatFile(2) are guaranteed to be
|
||||||
|
// lexically safe, so we can use path.Join here.
|
||||||
|
fullPath := filepath.Join(dir.Name(), path)
|
||||||
|
return os.NewFile(uintptr(fd), fullPath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fstatatFile(dir *os.File, path string, flags int) (unix.Stat_t, error) {
|
||||||
|
var stat unix.Stat_t
|
||||||
|
if err := unix.Fstatat(int(dir.Fd()), path, &stat, flags); err != nil {
|
||||||
|
return stat, &os.PathError{Op: "fstatat", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
return stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readlinkatFile(dir *os.File, path string) (string, error) {
|
||||||
|
size := 4096
|
||||||
|
for {
|
||||||
|
linkBuf := make([]byte, size)
|
||||||
|
n, err := unix.Readlinkat(int(dir.Fd()), path, linkBuf)
|
||||||
|
if err != nil {
|
||||||
|
return "", &os.PathError{Op: "readlinkat", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
if n != size {
|
||||||
|
return string(linkBuf[:n]), nil
|
||||||
|
}
|
||||||
|
// Possible truncation, resize the buffer.
|
||||||
|
size *= 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,33 +0,0 @@
|
||||||
## `pathrs-lite` ##
|
|
||||||
|
|
||||||
`github.com/cyphar/filepath-securejoin/pathrs-lite` provides a minimal **pure
|
|
||||||
Go** implementation of the core bits of [libpathrs][]. This is not intended to
|
|
||||||
be a complete replacement for libpathrs, instead it is mainly intended to be
|
|
||||||
useful as a transition tool for existing Go projects.
|
|
||||||
|
|
||||||
The long-term plan for `pathrs-lite` is to provide a build tag that will cause
|
|
||||||
all `pathrs-lite` operations to call into libpathrs directly, thus removing
|
|
||||||
code duplication for projects that wish to make use of libpathrs (and providing
|
|
||||||
the ability for software packagers to opt-in to libpathrs support without
|
|
||||||
needing to patch upstream).
|
|
||||||
|
|
||||||
[libpathrs]: https://github.com/cyphar/libpathrs
|
|
||||||
|
|
||||||
### License ###
|
|
||||||
|
|
||||||
Most of this subpackage is licensed under the Mozilla Public License (version
|
|
||||||
2.0). For more information, see the top-level [COPYING.md][] and
|
|
||||||
[LICENSE.MPL-2.0][] files, as well as the individual license headers for each
|
|
||||||
file.
|
|
||||||
|
|
||||||
```
|
|
||||||
Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
```
|
|
||||||
|
|
||||||
[COPYING.md]: ../COPYING.md
|
|
||||||
[LICENSE.MPL-2.0]: ../LICENSE.MPL-2.0
|
|
|
@ -1,14 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// Package pathrs (pathrs-lite) is a less complete pure Go implementation of
|
|
||||||
// some of the APIs provided by [libpathrs].
|
|
||||||
package pathrs
|
|
30
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert/assert.go
generated
vendored
30
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert/assert.go
generated
vendored
|
@ -1,30 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// Package assert provides some basic assertion helpers for Go.
|
|
||||||
package assert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert panics if the predicate is false with the provided argument.
|
|
||||||
func Assert(predicate bool, msg any) {
|
|
||||||
if !predicate {
|
|
||||||
panic(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assertf panics if the predicate is false and formats the message using the
|
|
||||||
// same formatting as [fmt.Printf].
|
|
||||||
//
|
|
||||||
// [fmt.Printf]: https://pkg.go.dev/fmt#Printf
|
|
||||||
func Assertf(predicate bool, fmtMsg string, args ...any) {
|
|
||||||
Assert(predicate, fmt.Sprintf(fmtMsg, args...))
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// Package internal contains unexported common code for filepath-securejoin.
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrPossibleAttack indicates that some attack was detected.
|
|
||||||
ErrPossibleAttack = errors.New("possible attack detected")
|
|
||||||
|
|
||||||
// ErrPossibleBreakout indicates that during an operation we ended up in a
|
|
||||||
// state that could be a breakout but we detected it.
|
|
||||||
ErrPossibleBreakout = errors.New("possible breakout detected")
|
|
||||||
|
|
||||||
// ErrInvalidDirectory indicates an unlinked directory.
|
|
||||||
ErrInvalidDirectory = errors.New("wandered into deleted directory")
|
|
||||||
|
|
||||||
// ErrDeletedInode indicates an unlinked file (non-directory).
|
|
||||||
ErrDeletedInode = errors.New("cannot verify path of deleted inode")
|
|
||||||
)
|
|
148
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/at_linux.go
generated
vendored
148
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/at_linux.go
generated
vendored
|
@ -1,148 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package fd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
)
|
|
||||||
|
|
||||||
// prepareAtWith returns -EBADF (an invalid fd) if dir is nil, otherwise using
|
|
||||||
// the dir.Fd(). We use -EBADF because in filepath-securejoin we generally
|
|
||||||
// don't want to allow relative-to-cwd paths. The returned path is an
|
|
||||||
// *informational* string that describes a reasonable pathname for the given
|
|
||||||
// *at(2) arguments. You must not use the full path for any actual filesystem
|
|
||||||
// operations.
|
|
||||||
func prepareAt(dir Fd, path string) (dirFd int, unsafeUnmaskedPath string) {
|
|
||||||
dirFd, dirPath := -int(unix.EBADF), "."
|
|
||||||
if dir != nil {
|
|
||||||
dirFd, dirPath = int(dir.Fd()), dir.Name()
|
|
||||||
}
|
|
||||||
if !filepath.IsAbs(path) {
|
|
||||||
// only prepend the dirfd path for relative paths
|
|
||||||
path = dirPath + "/" + path
|
|
||||||
}
|
|
||||||
// NOTE: If path is "." or "", the returned path won't be filepath.Clean,
|
|
||||||
// but that's okay since this path is either used for errors (in which case
|
|
||||||
// a trailing "/" or "/." is important information) or will be
|
|
||||||
// filepath.Clean'd later (in the case of fd.Openat).
|
|
||||||
return dirFd, path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Openat is an [Fd]-based wrapper around unix.Openat.
|
|
||||||
func Openat(dir Fd, path string, flags int, mode int) (*os.File, error) { //nolint:unparam // wrapper func
|
|
||||||
dirFd, fullPath := prepareAt(dir, path)
|
|
||||||
// Make sure we always set O_CLOEXEC.
|
|
||||||
flags |= unix.O_CLOEXEC
|
|
||||||
fd, err := unix.Openat(dirFd, path, flags, uint32(mode))
|
|
||||||
if err != nil {
|
|
||||||
return nil, &os.PathError{Op: "openat", Path: fullPath, Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(dir)
|
|
||||||
// openat is only used with lexically-safe paths so we can use
|
|
||||||
// filepath.Clean here, and also the path itself is not going to be used
|
|
||||||
// for actual path operations.
|
|
||||||
fullPath = filepath.Clean(fullPath)
|
|
||||||
return os.NewFile(uintptr(fd), fullPath), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fstatat is an [Fd]-based wrapper around unix.Fstatat.
|
|
||||||
func Fstatat(dir Fd, path string, flags int) (unix.Stat_t, error) {
|
|
||||||
dirFd, fullPath := prepareAt(dir, path)
|
|
||||||
var stat unix.Stat_t
|
|
||||||
if err := unix.Fstatat(dirFd, path, &stat, flags); err != nil {
|
|
||||||
return stat, &os.PathError{Op: "fstatat", Path: fullPath, Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(dir)
|
|
||||||
return stat, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Faccessat is an [Fd]-based wrapper around unix.Faccessat.
|
|
||||||
func Faccessat(dir Fd, path string, mode uint32, flags int) error {
|
|
||||||
dirFd, fullPath := prepareAt(dir, path)
|
|
||||||
err := unix.Faccessat(dirFd, path, mode, flags)
|
|
||||||
if err != nil {
|
|
||||||
err = &os.PathError{Op: "faccessat", Path: fullPath, Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(dir)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Readlinkat is an [Fd]-based wrapper around unix.Readlinkat.
|
|
||||||
func Readlinkat(dir Fd, path string) (string, error) {
|
|
||||||
dirFd, fullPath := prepareAt(dir, path)
|
|
||||||
size := 4096
|
|
||||||
for {
|
|
||||||
linkBuf := make([]byte, size)
|
|
||||||
n, err := unix.Readlinkat(dirFd, path, linkBuf)
|
|
||||||
if err != nil {
|
|
||||||
return "", &os.PathError{Op: "readlinkat", Path: fullPath, Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(dir)
|
|
||||||
if n != size {
|
|
||||||
return string(linkBuf[:n]), nil
|
|
||||||
}
|
|
||||||
// Possible truncation, resize the buffer.
|
|
||||||
size *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to
|
|
||||||
// avoid bumping the requirement for a single constant we can just define it
|
|
||||||
// ourselves.
|
|
||||||
_STATX_MNT_ID_UNIQUE = 0x4000 //nolint:revive // unix.* name
|
|
||||||
|
|
||||||
// We don't care which mount ID we get. The kernel will give us the unique
|
|
||||||
// one if it is supported. If the kernel doesn't support
|
|
||||||
// STATX_MNT_ID_UNIQUE, the bit is ignored and the returned request mask
|
|
||||||
// will only contain STATX_MNT_ID (if supported).
|
|
||||||
wantStatxMntMask = _STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
|
|
||||||
)
|
|
||||||
|
|
||||||
var hasStatxMountID = gocompat.SyncOnceValue(func() bool {
|
|
||||||
var stx unix.Statx_t
|
|
||||||
err := unix.Statx(-int(unix.EBADF), "/", 0, wantStatxMntMask, &stx)
|
|
||||||
return err == nil && stx.Mask&wantStatxMntMask != 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// GetMountID gets the mount identifier associated with the fd and path
|
|
||||||
// combination. It is effectively a wrapper around fetching
|
|
||||||
// STATX_MNT_ID{,_UNIQUE} with unix.Statx, but with a fallback to 0 if the
|
|
||||||
// kernel doesn't support the feature.
|
|
||||||
func GetMountID(dir Fd, path string) (uint64, error) {
|
|
||||||
// If we don't have statx(STATX_MNT_ID*) support, we can't do anything.
|
|
||||||
if !hasStatxMountID() {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dirFd, fullPath := prepareAt(dir, path)
|
|
||||||
|
|
||||||
var stx unix.Statx_t
|
|
||||||
err := unix.Statx(dirFd, path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, wantStatxMntMask, &stx)
|
|
||||||
if stx.Mask&wantStatxMntMask == 0 {
|
|
||||||
// It's not a kernel limitation, for some reason we couldn't get a
|
|
||||||
// mount ID. Assume it's some kind of attack.
|
|
||||||
err = fmt.Errorf("could not get mount id: %w", err)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: fullPath, Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(dir)
|
|
||||||
return stx.Mnt_id, nil
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
// Copyright (C) 2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// Package fd provides a drop-in interface-based replacement of [*os.File] that
|
|
||||||
// allows for things like noop-Close wrappers to be used.
|
|
||||||
//
|
|
||||||
// [*os.File]: https://pkg.go.dev/os#File
|
|
||||||
package fd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fd is an interface that mirrors most of the API of [*os.File], allowing you
|
|
||||||
// to create wrappers that can be used in place of [*os.File].
|
|
||||||
//
|
|
||||||
// [*os.File]: https://pkg.go.dev/os#File
|
|
||||||
type Fd interface {
|
|
||||||
io.Closer
|
|
||||||
Name() string
|
|
||||||
Fd() uintptr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time interface checks.
|
|
||||||
var (
|
|
||||||
_ Fd = (*os.File)(nil)
|
|
||||||
_ Fd = noClose{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type noClose struct{ inner Fd }
|
|
||||||
|
|
||||||
func (f noClose) Name() string { return f.inner.Name() }
|
|
||||||
func (f noClose) Fd() uintptr { return f.inner.Fd() }
|
|
||||||
|
|
||||||
func (f noClose) Close() error { return nil }
|
|
||||||
|
|
||||||
// NopCloser returns an [*os.File]-like object where the [Close] method is now
|
|
||||||
// a no-op.
|
|
||||||
//
|
|
||||||
// Note that for [*os.File] and similar objects, the Go garbage collector will
|
|
||||||
// still call [Close] on the underlying file unless you use
|
|
||||||
// [runtime.SetFinalizer] to disable this behaviour. This is up to the caller
|
|
||||||
// to do (if necessary).
|
|
||||||
//
|
|
||||||
// [*os.File]: https://pkg.go.dev/os#File
|
|
||||||
// [Close]: https://pkg.go.dev/io#Closer
|
|
||||||
// [runtime.SetFinalizer]: https://pkg.go.dev/runtime#SetFinalizer
|
|
||||||
func NopCloser(f Fd) Fd { return noClose{inner: f} }
|
|
78
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd_linux.go
generated
vendored
78
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/fd_linux.go
generated
vendored
|
@ -1,78 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package fd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DupWithName creates a new file descriptor referencing the same underlying
|
|
||||||
// file, but with the provided name instead of fd.Name().
|
|
||||||
func DupWithName(fd Fd, name string) (*os.File, error) {
|
|
||||||
fd2, err := unix.FcntlInt(fd.Fd(), unix.F_DUPFD_CLOEXEC, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, os.NewSyscallError("fcntl(F_DUPFD_CLOEXEC)", err)
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(fd)
|
|
||||||
return os.NewFile(uintptr(fd2), name), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dup creates a new file description referencing the same underlying file.
|
|
||||||
func Dup(fd Fd) (*os.File, error) {
|
|
||||||
return DupWithName(fd, fd.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fstat is an [Fd]-based wrapper around unix.Fstat.
|
|
||||||
func Fstat(fd Fd) (unix.Stat_t, error) {
|
|
||||||
var stat unix.Stat_t
|
|
||||||
if err := unix.Fstat(int(fd.Fd()), &stat); err != nil {
|
|
||||||
return stat, &os.PathError{Op: "fstat", Path: fd.Name(), Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(fd)
|
|
||||||
return stat, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fstatfs is an [Fd]-based wrapper around unix.Fstatfs.
|
|
||||||
func Fstatfs(fd Fd) (unix.Statfs_t, error) {
|
|
||||||
var statfs unix.Statfs_t
|
|
||||||
if err := unix.Fstatfs(int(fd.Fd()), &statfs); err != nil {
|
|
||||||
return statfs, &os.PathError{Op: "fstatfs", Path: fd.Name(), Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(fd)
|
|
||||||
return statfs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDeadInode detects whether the file has been unlinked from a filesystem and
|
|
||||||
// is thus a "dead inode" from the kernel's perspective.
|
|
||||||
func IsDeadInode(file Fd) error {
|
|
||||||
// If the nlink of a file drops to 0, there is an attacker deleting
|
|
||||||
// directories during our walk, which could result in weird /proc values.
|
|
||||||
// It's better to error out in this case.
|
|
||||||
stat, err := Fstat(file)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("check for dead inode: %w", err)
|
|
||||||
}
|
|
||||||
if stat.Nlink == 0 {
|
|
||||||
err := internal.ErrDeletedInode
|
|
||||||
if stat.Mode&unix.S_IFMT == unix.S_IFDIR {
|
|
||||||
err = internal.ErrInvalidDirectory
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%w %q", err, file.Name())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
54
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/mount_linux.go
generated
vendored
54
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/mount_linux.go
generated
vendored
|
@ -1,54 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package fd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fsopen is an [Fd]-based wrapper around unix.Fsopen.
|
|
||||||
func Fsopen(fsName string, flags int) (*os.File, error) {
|
|
||||||
// Make sure we always set O_CLOEXEC.
|
|
||||||
flags |= unix.FSOPEN_CLOEXEC
|
|
||||||
fd, err := unix.Fsopen(fsName, flags)
|
|
||||||
if err != nil {
|
|
||||||
return nil, os.NewSyscallError("fsopen "+fsName, err)
|
|
||||||
}
|
|
||||||
return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fsmount is an [Fd]-based wrapper around unix.Fsmount.
|
|
||||||
func Fsmount(ctx Fd, flags, mountAttrs int) (*os.File, error) {
|
|
||||||
// Make sure we always set O_CLOEXEC.
|
|
||||||
flags |= unix.FSMOUNT_CLOEXEC
|
|
||||||
fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, os.NewSyscallError("fsmount "+ctx.Name(), err)
|
|
||||||
}
|
|
||||||
return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenTree is an [Fd]-based wrapper around unix.OpenTree.
|
|
||||||
func OpenTree(dir Fd, path string, flags uint) (*os.File, error) {
|
|
||||||
dirFd, fullPath := prepareAt(dir, path)
|
|
||||||
// Make sure we always set O_CLOEXEC.
|
|
||||||
flags |= unix.OPEN_TREE_CLOEXEC
|
|
||||||
fd, err := unix.OpenTree(dirFd, path, flags)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &os.PathError{Op: "open_tree", Path: fullPath, Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(dir)
|
|
||||||
return os.NewFile(uintptr(fd), fullPath), nil
|
|
||||||
}
|
|
62
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go
generated
vendored
62
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd/openat2_linux.go
generated
vendored
|
@ -1,62 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package fd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func scopedLookupShouldRetry(how *unix.OpenHow, err error) bool {
|
|
||||||
// RESOLVE_IN_ROOT (and RESOLVE_BENEATH) can return -EAGAIN if we resolve
|
|
||||||
// ".." while a mount or rename occurs anywhere on the system. This could
|
|
||||||
// happen spuriously, or as the result of an attacker trying to mess with
|
|
||||||
// us during lookup.
|
|
||||||
//
|
|
||||||
// In addition, scoped lookups have a "safety check" at the end of
|
|
||||||
// complete_walk which will return -EXDEV if the final path is not in the
|
|
||||||
// root.
|
|
||||||
return how.Resolve&(unix.RESOLVE_IN_ROOT|unix.RESOLVE_BENEATH) != 0 &&
|
|
||||||
(errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EXDEV))
|
|
||||||
}
|
|
||||||
|
|
||||||
const scopedLookupMaxRetries = 32
|
|
||||||
|
|
||||||
// Openat2 is an [Fd]-based wrapper around unix.Openat2, but with some retry
|
|
||||||
// logic in case of EAGAIN errors.
|
|
||||||
func Openat2(dir Fd, path string, how *unix.OpenHow) (*os.File, error) {
|
|
||||||
dirFd, fullPath := prepareAt(dir, path)
|
|
||||||
// Make sure we always set O_CLOEXEC.
|
|
||||||
how.Flags |= unix.O_CLOEXEC
|
|
||||||
var tries int
|
|
||||||
for tries < scopedLookupMaxRetries {
|
|
||||||
fd, err := unix.Openat2(dirFd, path, how)
|
|
||||||
if err != nil {
|
|
||||||
if scopedLookupShouldRetry(how, err) {
|
|
||||||
// We retry a couple of times to avoid the spurious errors, and
|
|
||||||
// if we are being attacked then returning -EAGAIN is the best
|
|
||||||
// we can do.
|
|
||||||
tries++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: err}
|
|
||||||
}
|
|
||||||
runtime.KeepAlive(dir)
|
|
||||||
return os.NewFile(uintptr(fd), fullPath), nil
|
|
||||||
}
|
|
||||||
return nil, &os.PathError{Op: "openat2", Path: fullPath, Err: internal.ErrPossibleAttack}
|
|
||||||
}
|
|
10
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/README.md
generated
vendored
10
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/README.md
generated
vendored
|
@ -1,10 +0,0 @@
|
||||||
## gocompat ##
|
|
||||||
|
|
||||||
This directory contains backports of stdlib functions from later Go versions so
|
|
||||||
the filepath-securejoin can continue to be used by projects that are stuck with
|
|
||||||
Go 1.18 support. Note that often filepath-securejoin is added in security
|
|
||||||
patches for old releases, so avoiding the need to bump Go compiler requirements
|
|
||||||
is a huge plus to downstreams.
|
|
||||||
|
|
||||||
The source code is licensed under the same license as the Go stdlib. See the
|
|
||||||
source files for the precise license information.
|
|
13
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/doc.go
generated
vendored
13
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat/doc.go
generated
vendored
|
@ -1,13 +0,0 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
//go:build linux && go1.20
|
|
||||||
|
|
||||||
// Copyright (C) 2025 SUSE LLC. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package gocompat includes compatibility shims (backported from future Go
|
|
||||||
// stdlib versions) to permit filepath-securejoin to be used with older Go
|
|
||||||
// versions (often filepath-securejoin is added in security patches for old
|
|
||||||
// releases, so avoiding the need to bump Go compiler requirements is a huge
|
|
||||||
// plus to downstreams).
|
|
||||||
package gocompat
|
|
|
@ -1,53 +0,0 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build linux && go1.21
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package gocompat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
|
|
||||||
func SlicesDeleteFunc[S ~[]E, E any](slice S, delFn func(E) bool) S {
|
|
||||||
return slices.DeleteFunc(slice, delFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SlicesContains is equivalent to Go 1.21's slices.Contains.
|
|
||||||
func SlicesContains[S ~[]E, E comparable](slice S, val E) bool {
|
|
||||||
return slices.Contains(slice, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SlicesClone is equivalent to Go 1.21's slices.Clone.
|
|
||||||
func SlicesClone[S ~[]E, E any](slice S) S {
|
|
||||||
return slices.Clone(slice)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
|
|
||||||
func SyncOnceValue[T any](f func() T) func() T {
|
|
||||||
return sync.OnceValue(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
|
|
||||||
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
|
||||||
return sync.OnceValues(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
|
|
||||||
type CmpOrdered = cmp.Ordered
|
|
||||||
|
|
||||||
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
|
|
||||||
func CmpCompare[T CmpOrdered](x, y T) int {
|
|
||||||
return cmp.Compare(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max2 is equivalent to Go 1.21's max builtin (but only for two parameters).
|
|
||||||
func Max2[T CmpOrdered](x, y T) T {
|
|
||||||
return max(x, y)
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
//go:build linux && !go1.21
|
|
||||||
|
|
||||||
// Copyright (C) 2021, 2022 The Go Authors. All rights reserved.
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE.BSD file.
|
|
||||||
|
|
||||||
package gocompat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// These are very minimal implementations of functions that appear in Go 1.21's
|
|
||||||
// stdlib, included so that we can build on older Go versions. Most are
|
|
||||||
// borrowed directly from the stdlib, and a few are modified to be "obviously
|
|
||||||
// correct" without needing to copy too many other helpers.
|
|
||||||
|
|
||||||
// clearSlice is equivalent to Go 1.21's builtin clear.
|
|
||||||
// Copied from the Go 1.24 stdlib implementation.
|
|
||||||
func clearSlice[S ~[]E, E any](slice S) {
|
|
||||||
var zero E
|
|
||||||
for i := range slice {
|
|
||||||
slice[i] = zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// slicesIndexFunc is equivalent to Go 1.21's slices.IndexFunc.
|
|
||||||
// Copied from the Go 1.24 stdlib implementation.
|
|
||||||
func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
|
||||||
for i := range s {
|
|
||||||
if f(s[i]) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
// SlicesDeleteFunc is equivalent to Go 1.21's slices.DeleteFunc.
|
|
||||||
// Copied from the Go 1.24 stdlib implementation.
|
|
||||||
func SlicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
|
||||||
i := slicesIndexFunc(s, del)
|
|
||||||
if i == -1 {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
// Don't start copying elements until we find one to delete.
|
|
||||||
for j := i + 1; j < len(s); j++ {
|
|
||||||
if v := s[j]; !del(v) {
|
|
||||||
s[i] = v
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
clearSlice(s[i:]) // zero/nil out the obsolete elements, for GC
|
|
||||||
return s[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// SlicesContains is equivalent to Go 1.21's slices.Contains.
|
|
||||||
// Similar to the stdlib slices.Contains, except that we don't have
|
|
||||||
// slices.Index so we need to use slices.IndexFunc for this non-Func helper.
|
|
||||||
func SlicesContains[S ~[]E, E comparable](s S, v E) bool {
|
|
||||||
return slicesIndexFunc(s, func(e E) bool { return e == v }) >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// SlicesClone is equivalent to Go 1.21's slices.Clone.
|
|
||||||
// Copied from the Go 1.24 stdlib implementation.
|
|
||||||
func SlicesClone[S ~[]E, E any](s S) S {
|
|
||||||
// Preserve nil in case it matters.
|
|
||||||
if s == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return append(S([]E{}), s...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncOnceValue is equivalent to Go 1.21's sync.OnceValue.
|
|
||||||
// Copied from the Go 1.25 stdlib implementation.
|
|
||||||
func SyncOnceValue[T any](f func() T) func() T {
|
|
||||||
// Use a struct so that there's a single heap allocation.
|
|
||||||
d := struct {
|
|
||||||
f func() T
|
|
||||||
once sync.Once
|
|
||||||
valid bool
|
|
||||||
p any
|
|
||||||
result T
|
|
||||||
}{
|
|
||||||
f: f,
|
|
||||||
}
|
|
||||||
return func() T {
|
|
||||||
d.once.Do(func() {
|
|
||||||
defer func() {
|
|
||||||
d.f = nil
|
|
||||||
d.p = recover()
|
|
||||||
if !d.valid {
|
|
||||||
panic(d.p)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
d.result = d.f()
|
|
||||||
d.valid = true
|
|
||||||
})
|
|
||||||
if !d.valid {
|
|
||||||
panic(d.p)
|
|
||||||
}
|
|
||||||
return d.result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SyncOnceValues is equivalent to Go 1.21's sync.OnceValues.
|
|
||||||
// Copied from the Go 1.25 stdlib implementation.
|
|
||||||
func SyncOnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
|
|
||||||
// Use a struct so that there's a single heap allocation.
|
|
||||||
d := struct {
|
|
||||||
f func() (T1, T2)
|
|
||||||
once sync.Once
|
|
||||||
valid bool
|
|
||||||
p any
|
|
||||||
r1 T1
|
|
||||||
r2 T2
|
|
||||||
}{
|
|
||||||
f: f,
|
|
||||||
}
|
|
||||||
return func() (T1, T2) {
|
|
||||||
d.once.Do(func() {
|
|
||||||
defer func() {
|
|
||||||
d.f = nil
|
|
||||||
d.p = recover()
|
|
||||||
if !d.valid {
|
|
||||||
panic(d.p)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
d.r1, d.r2 = d.f()
|
|
||||||
d.valid = true
|
|
||||||
})
|
|
||||||
if !d.valid {
|
|
||||||
panic(d.p)
|
|
||||||
}
|
|
||||||
return d.r1, d.r2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmpOrdered is equivalent to Go 1.21's cmp.Ordered generic type definition.
|
|
||||||
// Copied from the Go 1.25 stdlib implementation.
|
|
||||||
type CmpOrdered interface {
|
|
||||||
~int | ~int8 | ~int16 | ~int32 | ~int64 |
|
|
||||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
|
|
||||||
~float32 | ~float64 |
|
|
||||||
~string
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNaN reports whether x is a NaN without requiring the math package.
|
|
||||||
// This will always return false if T is not floating-point.
|
|
||||||
// Copied from the Go 1.25 stdlib implementation.
|
|
||||||
func isNaN[T CmpOrdered](x T) bool {
|
|
||||||
return x != x
|
|
||||||
}
|
|
||||||
|
|
||||||
// CmpCompare is equivalent to Go 1.21's cmp.Compare.
|
|
||||||
// Copied from the Go 1.25 stdlib implementation.
|
|
||||||
func CmpCompare[T CmpOrdered](x, y T) int {
|
|
||||||
xNaN := isNaN(x)
|
|
||||||
yNaN := isNaN(y)
|
|
||||||
if xNaN {
|
|
||||||
if yNaN {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if yNaN {
|
|
||||||
return +1
|
|
||||||
}
|
|
||||||
if x < y {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if x > y {
|
|
||||||
return +1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Max2 is equivalent to Go 1.21's max builtin for two parameters.
|
|
||||||
func Max2[T CmpOrdered](x, y T) T {
|
|
||||||
m := x
|
|
||||||
if y > m {
|
|
||||||
m = y
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
// Copyright (C) 2022 The Go Authors. All rights reserved.
|
|
||||||
// Copyright (C) 2025 SUSE LLC. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE.BSD file.
|
|
||||||
|
|
||||||
// The parsing logic is very loosely based on the Go stdlib's
|
|
||||||
// src/internal/syscall/unix/kernel_version_linux.go but with an API that looks
|
|
||||||
// a bit like runc's libcontainer/system/kernelversion.
|
|
||||||
//
|
|
||||||
// TODO(cyphar): This API has been copied around to a lot of different projects
|
|
||||||
// (Docker, containerd, runc, and now filepath-securejoin) -- maybe we should
|
|
||||||
// put it in a separate project?
|
|
||||||
|
|
||||||
// Package kernelversion provides a simple mechanism for checking whether the
|
|
||||||
// running kernel is at least as new as some baseline kernel version. This is
|
|
||||||
// often useful when checking for features that would be too complicated to
|
|
||||||
// test support for (or in cases where we know that some kernel features in
|
|
||||||
// backport-heavy kernels are broken and need to be avoided).
|
|
||||||
package kernelversion
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KernelVersion is a numeric representation of the key numerical elements of a
|
|
||||||
// kernel version (for instance, "4.1.2-default-1" would be represented as
|
|
||||||
// KernelVersion{4, 1, 2}).
|
|
||||||
type KernelVersion []uint64
|
|
||||||
|
|
||||||
func (kver KernelVersion) String() string {
|
|
||||||
var str strings.Builder
|
|
||||||
for idx, elem := range kver {
|
|
||||||
if idx != 0 {
|
|
||||||
_, _ = str.WriteRune('.')
|
|
||||||
}
|
|
||||||
_, _ = str.WriteString(strconv.FormatUint(elem, 10))
|
|
||||||
}
|
|
||||||
return str.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
var errInvalidKernelVersion = errors.New("invalid kernel version")
|
|
||||||
|
|
||||||
// parseKernelVersion parses a string and creates a KernelVersion based on it.
|
|
||||||
func parseKernelVersion(kverStr string) (KernelVersion, error) {
|
|
||||||
kver := make(KernelVersion, 1, 3)
|
|
||||||
for idx, ch := range kverStr {
|
|
||||||
if '0' <= ch && ch <= '9' {
|
|
||||||
v := &kver[len(kver)-1]
|
|
||||||
*v = (*v * 10) + uint64(ch-'0')
|
|
||||||
} else {
|
|
||||||
if idx == 0 || kverStr[idx-1] < '0' || '9' < kverStr[idx-1] {
|
|
||||||
// "." must be preceded by a digit while in version section
|
|
||||||
return nil, fmt.Errorf("%w %q: kernel version has dot(s) followed by non-digit in version section", errInvalidKernelVersion, kverStr)
|
|
||||||
}
|
|
||||||
if ch != '.' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
kver = append(kver, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(kver) < 2 {
|
|
||||||
return nil, fmt.Errorf("%w %q: kernel versions must contain at least two components", errInvalidKernelVersion, kverStr)
|
|
||||||
}
|
|
||||||
return kver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getKernelVersion gets the current kernel version.
|
|
||||||
var getKernelVersion = gocompat.SyncOnceValues(func() (KernelVersion, error) {
|
|
||||||
var uts unix.Utsname
|
|
||||||
if err := unix.Uname(&uts); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Remove the \x00 from the release.
|
|
||||||
release := uts.Release[:]
|
|
||||||
return parseKernelVersion(string(release[:bytes.IndexByte(release, 0)]))
|
|
||||||
})
|
|
||||||
|
|
||||||
// GreaterEqualThan returns true if the the host kernel version is greater than
|
|
||||||
// or equal to the provided [KernelVersion]. When doing this comparison, any
|
|
||||||
// non-numerical suffixes of the host kernel version are ignored.
|
|
||||||
//
|
|
||||||
// If the number of components provided is not equal to the number of numerical
|
|
||||||
// components of the host kernel version, any missing components are treated as
|
|
||||||
// 0. This means that GreaterEqualThan(KernelVersion{4}) will be treated the
|
|
||||||
// same as GreaterEqualThan(KernelVersion{4, 0, 0, ..., 0, 0}), and that if the
|
|
||||||
// host kernel version is "4" then GreaterEqualThan(KernelVersion{4, 1}) will
|
|
||||||
// return false (because the host version will be treated as "4.0").
|
|
||||||
func GreaterEqualThan(wantKver KernelVersion) (bool, error) {
|
|
||||||
hostKver, err := getKernelVersion()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pad out the kernel version lengths to match one another.
|
|
||||||
cmpLen := gocompat.Max2(len(hostKver), len(wantKver))
|
|
||||||
hostKver = append(hostKver, make(KernelVersion, cmpLen-len(hostKver))...)
|
|
||||||
wantKver = append(wantKver, make(KernelVersion, cmpLen-len(wantKver))...)
|
|
||||||
|
|
||||||
for i := 0; i < cmpLen; i++ {
|
|
||||||
switch gocompat.CmpCompare(hostKver[i], wantKver[i]) {
|
|
||||||
case -1:
|
|
||||||
// host < want
|
|
||||||
return false, nil
|
|
||||||
case +1:
|
|
||||||
// host > want
|
|
||||||
return true, nil
|
|
||||||
case 0:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// equal version values
|
|
||||||
return true, nil
|
|
||||||
}
|
|
12
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/doc.go
generated
vendored
12
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/doc.go
generated
vendored
|
@ -1,12 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// Package linux returns information about what features are supported on the
|
|
||||||
// running kernel.
|
|
||||||
package linux
|
|
47
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/mount_linux.go
generated
vendored
47
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/mount_linux.go
generated
vendored
|
@ -1,47 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package linux
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HasNewMountAPI returns whether the new fsopen(2) mount API is supported on
|
|
||||||
// the running kernel.
|
|
||||||
var HasNewMountAPI = gocompat.SyncOnceValue(func() bool {
|
|
||||||
// All of the pieces of the new mount API we use (fsopen, fsconfig,
|
|
||||||
// fsmount, open_tree) were added together in Linux 5.2[1,2], so we can
|
|
||||||
// just check for one of the syscalls and the others should also be
|
|
||||||
// available.
|
|
||||||
//
|
|
||||||
// Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE.
|
|
||||||
// This is equivalent to openat(2), but tells us if open_tree is
|
|
||||||
// available (and thus all of the other basic new mount API syscalls).
|
|
||||||
// open_tree(2) is most light-weight syscall to test here.
|
|
||||||
//
|
|
||||||
// [1]: merge commit 400913252d09
|
|
||||||
// [2]: <https://lore.kernel.org/lkml/153754740781.17872.7869536526927736855.stgit@warthog.procyon.org.uk/>
|
|
||||||
fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_ = unix.Close(fd)
|
|
||||||
|
|
||||||
// RHEL 8 has a backport of fsopen(2) that appears to have some very
|
|
||||||
// difficult to debug performance pathology. As such, it seems prudent to
|
|
||||||
// simply reject pre-5.2 kernels.
|
|
||||||
isNotBackport, _ := kernelversion.GreaterEqualThan(kernelversion.KernelVersion{5, 2})
|
|
||||||
return isNotBackport
|
|
||||||
})
|
|
31
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/openat2_linux.go
generated
vendored
31
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux/openat2_linux.go
generated
vendored
|
@ -1,31 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package linux
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HasOpenat2 returns whether openat2(2) is supported on the running kernel.
|
|
||||||
var HasOpenat2 = gocompat.SyncOnceValue(func() bool {
|
|
||||||
fd, err := unix.Openat2(unix.AT_FDCWD, ".", &unix.OpenHow{
|
|
||||||
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
|
||||||
Resolve: unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_IN_ROOT,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_ = unix.Close(fd)
|
|
||||||
return true
|
|
||||||
})
|
|
544
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_linux.go
generated
vendored
544
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs/procfs_linux.go
generated
vendored
|
@ -1,544 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// Package procfs provides a safe API for operating on /proc on Linux. Note
|
|
||||||
// that this is the *internal* procfs API, mainy needed due to Go's
|
|
||||||
// restrictions on cyclic dependencies and its incredibly minimal visibility
|
|
||||||
// system without making a separate internal/ package.
|
|
||||||
package procfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The kernel guarantees that the root inode of a procfs mount has an
|
|
||||||
// f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO.
|
|
||||||
const (
|
|
||||||
procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC
|
|
||||||
procRootIno = 1 // PROC_ROOT_INO
|
|
||||||
)
|
|
||||||
|
|
||||||
// verifyProcHandle checks that the handle is from a procfs filesystem.
|
|
||||||
// Contrast this to [verifyProcRoot], which also verifies that the handle is
|
|
||||||
// the root of a procfs mount.
|
|
||||||
func verifyProcHandle(procHandle fd.Fd) error {
|
|
||||||
if statfs, err := fd.Fstatfs(procHandle); err != nil {
|
|
||||||
return err
|
|
||||||
} else if statfs.Type != procSuperMagic {
|
|
||||||
return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifyProcRoot verifies that the handle is the root of a procfs filesystem.
|
|
||||||
// Contrast this to [verifyProcHandle], which only verifies if the handle is
|
|
||||||
// some file on procfs (regardless of what file it is).
|
|
||||||
func verifyProcRoot(procRoot fd.Fd) error {
|
|
||||||
if err := verifyProcHandle(procRoot); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if stat, err := fd.Fstat(procRoot); err != nil {
|
|
||||||
return err
|
|
||||||
} else if stat.Ino != procRootIno {
|
|
||||||
return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type procfsFeatures struct {
|
|
||||||
// hasSubsetPid was added in Linux 5.8, along with hidepid=ptraceable (and
|
|
||||||
// string-based hidepid= values). Before this patchset, it was not really
|
|
||||||
// safe to try to modify procfs superblock flags because the superblock was
|
|
||||||
// shared -- so if this feature is not available, **you should not set any
|
|
||||||
// superblock flags**.
|
|
||||||
//
|
|
||||||
// 6814ef2d992a ("proc: add option to mount only a pids subset")
|
|
||||||
// fa10fed30f25 ("proc: allow to mount many instances of proc in one pid namespace")
|
|
||||||
// 24a71ce5c47f ("proc: instantiate only pids that we can ptrace on 'hidepid=4' mount option")
|
|
||||||
// 1c6c4d112e81 ("proc: use human-readable values for hidepid")
|
|
||||||
// 9ff7258575d5 ("Merge branch 'proc-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/ebiederm/user-namespace")
|
|
||||||
hasSubsetPid bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var getProcfsFeatures = gocompat.SyncOnceValue(func() procfsFeatures {
|
|
||||||
if !linux.HasNewMountAPI() {
|
|
||||||
return procfsFeatures{}
|
|
||||||
}
|
|
||||||
procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC)
|
|
||||||
if err != nil {
|
|
||||||
return procfsFeatures{}
|
|
||||||
}
|
|
||||||
defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here
|
|
||||||
|
|
||||||
return procfsFeatures{
|
|
||||||
hasSubsetPid: unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid") == nil,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
func newPrivateProcMount(subset bool) (_ *Handle, Err error) {
|
|
||||||
procfsCtx, err := fd.Fsopen("proc", unix.FSOPEN_CLOEXEC)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer procfsCtx.Close() //nolint:errcheck // close failures aren't critical here
|
|
||||||
|
|
||||||
if subset && getProcfsFeatures().hasSubsetPid {
|
|
||||||
// Try to configure hidepid=ptraceable,subset=pid if possible, but
|
|
||||||
// ignore errors.
|
|
||||||
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable")
|
|
||||||
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get an actual handle.
|
|
||||||
if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil {
|
|
||||||
return nil, os.NewSyscallError("fsconfig create procfs", err)
|
|
||||||
}
|
|
||||||
// TODO: Output any information from the fscontext log to debug logs.
|
|
||||||
procRoot, err := fd.Fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if Err != nil {
|
|
||||||
_ = procRoot.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return newHandle(procRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func clonePrivateProcMount() (_ *Handle, Err error) {
|
|
||||||
// Try to make a clone without using AT_RECURSIVE if we can. If this works,
|
|
||||||
// we can be sure there are no over-mounts and so if the root is valid then
|
|
||||||
// we're golden. Otherwise, we have to deal with over-mounts.
|
|
||||||
procRoot, err := fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE)
|
|
||||||
if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procRoot) {
|
|
||||||
procRoot, err = fd.OpenTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("creating a detached procfs clone: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if Err != nil {
|
|
||||||
_ = procRoot.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return newHandle(procRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func privateProcRoot(subset bool) (*Handle, error) {
|
|
||||||
if !linux.HasNewMountAPI() || hookForceGetProcRootUnsafe() {
|
|
||||||
return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP)
|
|
||||||
}
|
|
||||||
// Try to create a new procfs mount from scratch if we can. This ensures we
|
|
||||||
// can get a procfs mount even if /proc is fake (for whatever reason).
|
|
||||||
procRoot, err := newPrivateProcMount(subset)
|
|
||||||
if err != nil || hookForcePrivateProcRootOpenTree(procRoot) {
|
|
||||||
// Try to clone /proc then...
|
|
||||||
procRoot, err = clonePrivateProcMount()
|
|
||||||
}
|
|
||||||
return procRoot, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func unsafeHostProcRoot() (_ *Handle, Err error) {
|
|
||||||
procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if Err != nil {
|
|
||||||
_ = procRoot.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return newHandle(procRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle is a wrapper around an *os.File handle to "/proc", which can be used
|
|
||||||
// to do further procfs-related operations in a safe way.
|
|
||||||
type Handle struct {
|
|
||||||
Inner fd.Fd
|
|
||||||
// Does this handle have subset=pid set?
|
|
||||||
isSubset bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHandle(procRoot fd.Fd) (*Handle, error) {
|
|
||||||
if err := verifyProcRoot(procRoot); err != nil {
|
|
||||||
// This is only used in methods that
|
|
||||||
_ = procRoot.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
proc := &Handle{Inner: procRoot}
|
|
||||||
// With subset=pid we can be sure that /proc/uptime will not exist.
|
|
||||||
if err := fd.Faccessat(proc.Inner, "uptime", unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
|
||||||
proc.isSubset = errors.Is(err, os.ErrNotExist)
|
|
||||||
}
|
|
||||||
return proc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying file for the Handle.
|
|
||||||
func (proc *Handle) Close() error { return proc.Inner.Close() }
|
|
||||||
|
|
||||||
var getCachedProcRoot = gocompat.SyncOnceValue(func() *Handle {
|
|
||||||
procRoot, err := getProcRoot(true)
|
|
||||||
if err != nil {
|
|
||||||
return nil // just don't cache if we see an error
|
|
||||||
}
|
|
||||||
if !procRoot.isSubset {
|
|
||||||
return nil // we only cache verified subset=pid handles
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disarm (*Handle).Close() to stop someone from accidentally closing
|
|
||||||
// the global handle.
|
|
||||||
procRoot.Inner = fd.NopCloser(procRoot.Inner)
|
|
||||||
return procRoot
|
|
||||||
})
|
|
||||||
|
|
||||||
// OpenProcRoot tries to open a "safer" handle to "/proc".
|
|
||||||
func OpenProcRoot() (*Handle, error) {
|
|
||||||
if proc := getCachedProcRoot(); proc != nil {
|
|
||||||
return proc, nil
|
|
||||||
}
|
|
||||||
return getProcRoot(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or
|
|
||||||
// masked paths (but also without "subset=pid").
|
|
||||||
func OpenUnsafeProcRoot() (*Handle, error) { return getProcRoot(false) }
|
|
||||||
|
|
||||||
func getProcRoot(subset bool) (*Handle, error) {
|
|
||||||
proc, err := privateProcRoot(subset)
|
|
||||||
if err != nil {
|
|
||||||
// Fall back to using a /proc handle if making a private mount failed.
|
|
||||||
// If we have openat2, at least we can avoid some kinds of over-mount
|
|
||||||
// attacks, but without openat2 there's not much we can do.
|
|
||||||
proc, err = unsafeHostProcRoot()
|
|
||||||
}
|
|
||||||
return proc, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasProcThreadSelf = gocompat.SyncOnceValue(func() bool {
|
|
||||||
return unix.Access("/proc/thread-self/", unix.F_OK) == nil
|
|
||||||
})
|
|
||||||
|
|
||||||
var errUnsafeProcfs = errors.New("unsafe procfs detected")
|
|
||||||
|
|
||||||
// lookup is a very minimal wrapper around [procfsLookupInRoot] which is
|
|
||||||
// intended to be called from the external API.
|
|
||||||
func (proc *Handle) lookup(subpath string) (*os.File, error) {
|
|
||||||
handle, err := procfsLookupInRoot(proc.Inner, subpath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return handle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// procfsBase is an enum indicating the prefix of a subpath in operations
|
|
||||||
// involving [Handle]s.
|
|
||||||
type procfsBase string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ProcRoot refers to the root of the procfs (i.e., "/proc/<subpath>").
|
|
||||||
ProcRoot procfsBase = "/proc"
|
|
||||||
// ProcSelf refers to the current process' subdirectory (i.e.,
|
|
||||||
// "/proc/self/<subpath>").
|
|
||||||
ProcSelf procfsBase = "/proc/self"
|
|
||||||
// ProcThreadSelf refers to the current thread's subdirectory (i.e.,
|
|
||||||
// "/proc/thread-self/<subpath>"). In multi-threaded programs (i.e., all Go
|
|
||||||
// programs) where one thread has a different CLONE_FS, it is possible for
|
|
||||||
// "/proc/self" to point the wrong thread and so "/proc/thread-self" may be
|
|
||||||
// necessary. Note that on pre-3.17 kernels, "/proc/thread-self" doesn't
|
|
||||||
// exist and so a fallback will be used in that case.
|
|
||||||
ProcThreadSelf procfsBase = "/proc/thread-self"
|
|
||||||
// TODO: Switch to an interface setup so we can have a more type-safe
|
|
||||||
// version of ProcPid and remove the need to worry about invalid string
|
|
||||||
// values.
|
|
||||||
)
|
|
||||||
|
|
||||||
// prefix returns a prefix that can be used with the given [Handle].
|
|
||||||
func (base procfsBase) prefix(proc *Handle) (string, error) {
|
|
||||||
switch base {
|
|
||||||
case ProcRoot:
|
|
||||||
return ".", nil
|
|
||||||
case ProcSelf:
|
|
||||||
return "self", nil
|
|
||||||
case ProcThreadSelf:
|
|
||||||
threadSelf := "thread-self"
|
|
||||||
if !hasProcThreadSelf() || hookForceProcSelfTask() {
|
|
||||||
// Pre-3.17 kernels don't have /proc/thread-self, so do it
|
|
||||||
// manually.
|
|
||||||
threadSelf = "self/task/" + strconv.Itoa(unix.Gettid())
|
|
||||||
if err := fd.Faccessat(proc.Inner, threadSelf, unix.F_OK, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() {
|
|
||||||
// In this case, we running in a pid namespace that doesn't
|
|
||||||
// match the /proc mount we have. This can happen inside runc.
|
|
||||||
//
|
|
||||||
// Unfortunately, there is no nice way to get the correct TID
|
|
||||||
// to use here because of the age of the kernel, so we have to
|
|
||||||
// just use /proc/self and hope that it works.
|
|
||||||
threadSelf = "self"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return threadSelf, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("invalid procfs base %q", base)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcThreadSelfCloser is a callback that needs to be called when you are done
|
|
||||||
// operating on an [os.File] fetched using [ProcThreadSelf].
|
|
||||||
//
|
|
||||||
// [os.File]: https://pkg.go.dev/os#File
|
|
||||||
type ProcThreadSelfCloser func()
|
|
||||||
|
|
||||||
// open is the core lookup operation for [Handle]. It returns a handle to
|
|
||||||
// "/proc/<base>/<subpath>". If the returned [ProcThreadSelfCloser] is non-nil,
|
|
||||||
// you should call it after you are done interacting with the returned handle.
|
|
||||||
//
|
|
||||||
// In general you should use prefer to use the other helpers, as they remove
|
|
||||||
// the need to interact with [procfsBase] and do not return a nil
|
|
||||||
// [ProcThreadSelfCloser] for [procfsBase] values other than [ProcThreadSelf]
|
|
||||||
// where it is necessary.
|
|
||||||
func (proc *Handle) open(base procfsBase, subpath string) (_ *os.File, closer ProcThreadSelfCloser, Err error) {
|
|
||||||
prefix, err := base.prefix(proc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
subpath = prefix + "/" + subpath
|
|
||||||
|
|
||||||
switch base {
|
|
||||||
case ProcRoot:
|
|
||||||
file, err := proc.lookup(subpath)
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
// The Handle handle in use might be a subset=pid one, which will
|
|
||||||
// result in spurious errors. In this case, just open a temporary
|
|
||||||
// unmasked procfs handle for this operation.
|
|
||||||
proc, err2 := OpenUnsafeProcRoot() // !subset=pid
|
|
||||||
if err2 != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
defer proc.Close() //nolint:errcheck // close failures aren't critical here
|
|
||||||
|
|
||||||
file, err = proc.lookup(subpath)
|
|
||||||
}
|
|
||||||
return file, nil, err
|
|
||||||
|
|
||||||
case ProcSelf:
|
|
||||||
file, err := proc.lookup(subpath)
|
|
||||||
return file, nil, err
|
|
||||||
|
|
||||||
case ProcThreadSelf:
|
|
||||||
// We need to lock our thread until the caller is done with the handle
|
|
||||||
// because between getting the handle and using it we could get
|
|
||||||
// interrupted by the Go runtime and hit the case where the underlying
|
|
||||||
// thread is swapped out and the original thread is killed, resulting
|
|
||||||
// in pull-your-hair-out-hard-to-debug issues in the caller.
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer func() {
|
|
||||||
if Err != nil {
|
|
||||||
runtime.UnlockOSThread()
|
|
||||||
closer = nil
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
file, err := proc.lookup(subpath)
|
|
||||||
return file, runtime.UnlockOSThread, err
|
|
||||||
}
|
|
||||||
// should never be reached
|
|
||||||
return nil, nil, fmt.Errorf("[internal error] invalid procfs base %q", base)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenThreadSelf returns a handle to "/proc/thread-self/<subpath>" (or an
|
|
||||||
// equivalent handle on older kernels where "/proc/thread-self" doesn't exist).
|
|
||||||
// Once finished with the handle, you must call the returned closer function
|
|
||||||
// (runtime.UnlockOSThread). You must not pass the returned *os.File to other
|
|
||||||
// Go threads or use the handle after calling the closer.
|
|
||||||
func (proc *Handle) OpenThreadSelf(subpath string) (_ *os.File, _ ProcThreadSelfCloser, Err error) {
|
|
||||||
return proc.open(ProcThreadSelf, subpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenSelf returns a handle to /proc/self/<subpath>.
|
|
||||||
func (proc *Handle) OpenSelf(subpath string) (*os.File, error) {
|
|
||||||
file, closer, err := proc.open(ProcSelf, subpath)
|
|
||||||
assert.Assert(closer == nil, "closer for ProcSelf must be nil")
|
|
||||||
return file, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenRoot returns a handle to /proc/<subpath>.
|
|
||||||
func (proc *Handle) OpenRoot(subpath string) (*os.File, error) {
|
|
||||||
file, closer, err := proc.open(ProcRoot, subpath)
|
|
||||||
assert.Assert(closer == nil, "closer for ProcRoot must be nil")
|
|
||||||
return file, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenPid returns a handle to /proc/$pid/<subpath> (pid can be a pid or tid).
|
|
||||||
// This is mainly intended for usage when operating on other processes.
|
|
||||||
func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) {
|
|
||||||
return proc.OpenRoot(strconv.Itoa(pid) + "/" + subpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkSubpathOvermount checks if the dirfd and path combination is on the
|
|
||||||
// same mount as the given root.
|
|
||||||
func checkSubpathOvermount(root, dir fd.Fd, path string) error {
|
|
||||||
// Get the mntID of our procfs handle.
|
|
||||||
expectedMountID, err := fd.GetMountID(root, "")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get root mount id: %w", err)
|
|
||||||
}
|
|
||||||
// Get the mntID of the target magic-link.
|
|
||||||
gotMountID, err := fd.GetMountID(dir, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get subpath mount id: %w", err)
|
|
||||||
}
|
|
||||||
// As long as the directory mount is alive, even with wrapping mount IDs,
|
|
||||||
// we would expect to see a different mount ID here. (Of course, if we're
|
|
||||||
// using unsafeHostProcRoot() then an attaker could change this after we
|
|
||||||
// did this check.)
|
|
||||||
if expectedMountID != gotMountID {
|
|
||||||
return fmt.Errorf("%w: subpath %s/%s has an overmount obscuring the real path (mount ids do not match %d != %d)",
|
|
||||||
errUnsafeProcfs, dir.Name(), path, expectedMountID, gotMountID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Readlink performs a readlink operation on "/proc/<base>/<subpath>" in a way
|
|
||||||
// that should be free from race attacks. This is most commonly used to get the
|
|
||||||
// real path of a file by looking at "/proc/self/fd/$n", with the same safety
|
|
||||||
// protections as [Open] (as well as some additional checks against
|
|
||||||
// overmounts).
|
|
||||||
func (proc *Handle) Readlink(base procfsBase, subpath string) (string, error) {
|
|
||||||
link, closer, err := proc.open(base, subpath)
|
|
||||||
if closer != nil {
|
|
||||||
defer closer()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get safe %s/%s handle: %w", base, subpath, err)
|
|
||||||
}
|
|
||||||
defer link.Close() //nolint:errcheck // close failures aren't critical here
|
|
||||||
|
|
||||||
// Try to detect if there is a mount on top of the magic-link. This should
|
|
||||||
// be safe in general (a mount on top of the path afterwards would not
|
|
||||||
// affect the handle itself) and will definitely be safe if we are using
|
|
||||||
// privateProcRoot() (at least since Linux 5.12[1], when anonymous mount
|
|
||||||
// namespaces were completely isolated from external mounts including mount
|
|
||||||
// propagation events).
|
|
||||||
//
|
|
||||||
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
|
||||||
// onto targets that reside on shared mounts").
|
|
||||||
if err := checkSubpathOvermount(proc.Inner, link, ""); err != nil {
|
|
||||||
return "", fmt.Errorf("check safety of %s/%s magiclink: %w", base, subpath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit
|
|
||||||
// 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty
|
|
||||||
// relative pathnames").
|
|
||||||
return fd.Readlinkat(link, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcSelfFdReadlink gets the real path of the given file by looking at
|
|
||||||
// readlink(/proc/thread-self/fd/$n).
|
|
||||||
//
|
|
||||||
// This is just a wrapper around [Handle.Readlink].
|
|
||||||
func ProcSelfFdReadlink(fd fd.Fd) (string, error) {
|
|
||||||
procRoot, err := OpenProcRoot() // subset=pid
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer procRoot.Close() //nolint:errcheck // close failures aren't critical here
|
|
||||||
|
|
||||||
fdPath := "fd/" + strconv.Itoa(int(fd.Fd()))
|
|
||||||
return procRoot.Readlink(ProcThreadSelf, fdPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckProcSelfFdPath returns whether the given file handle matches the
|
|
||||||
// expected path. (This is inherently racy.)
|
|
||||||
func CheckProcSelfFdPath(path string, file fd.Fd) error {
|
|
||||||
if err := fd.IsDeadInode(file); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
actualPath, err := ProcSelfFdReadlink(file)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get path of handle: %w", err)
|
|
||||||
}
|
|
||||||
if actualPath != path {
|
|
||||||
return fmt.Errorf("%w: handle path %q doesn't match expected path %q", internal.ErrPossibleBreakout, actualPath, path)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReopenFd takes an existing file descriptor and "re-opens" it through
|
|
||||||
// /proc/thread-self/fd/<fd>. This allows for O_PATH file descriptors to be
|
|
||||||
// upgraded to regular file descriptors, as well as changing the open mode of a
|
|
||||||
// regular file descriptor. Some filesystems have unique handling of open(2)
|
|
||||||
// which make this incredibly useful (such as /dev/ptmx).
|
|
||||||
func ReopenFd(handle fd.Fd, flags int) (*os.File, error) {
|
|
||||||
procRoot, err := OpenProcRoot() // subset=pid
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer procRoot.Close() //nolint:errcheck // close failures aren't critical here
|
|
||||||
|
|
||||||
// We can't operate on /proc/thread-self/fd/$n directly when doing a
|
|
||||||
// re-open, so we need to open /proc/thread-self/fd and then open a single
|
|
||||||
// final component.
|
|
||||||
procFdDir, closer, err := procRoot.OpenThreadSelf("fd/")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get safe /proc/thread-self/fd handle: %w", err)
|
|
||||||
}
|
|
||||||
defer procFdDir.Close() //nolint:errcheck // close failures aren't critical here
|
|
||||||
defer closer()
|
|
||||||
|
|
||||||
// Try to detect if there is a mount on top of the magic-link we are about
|
|
||||||
// to open. If we are using unsafeHostProcRoot(), this could change after
|
|
||||||
// we check it (and there's nothing we can do about that) but for
|
|
||||||
// privateProcRoot() this should be guaranteed to be safe (at least since
|
|
||||||
// Linux 5.12[1], when anonymous mount namespaces were completely isolated
|
|
||||||
// from external mounts including mount propagation events).
|
|
||||||
//
|
|
||||||
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
|
||||||
// onto targets that reside on shared mounts").
|
|
||||||
fdStr := strconv.Itoa(int(handle.Fd()))
|
|
||||||
if err := checkSubpathOvermount(procRoot.Inner, procFdDir, fdStr); err != nil {
|
|
||||||
return nil, fmt.Errorf("check safety of /proc/thread-self/fd/%s magiclink: %w", fdStr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
flags |= unix.O_CLOEXEC
|
|
||||||
// Rather than just wrapping fd.Openat, open-code it so we can copy
|
|
||||||
// handle.Name().
|
|
||||||
reopenFd, err := unix.Openat(int(procFdDir.Fd()), fdStr, flags, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("reopen fd %d: %w", handle.Fd(), err)
|
|
||||||
}
|
|
||||||
return os.NewFile(uintptr(reopenFd), handle.Name()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test hooks used in the procfs tests to verify that the fallback logic works.
|
|
||||||
// See testing_mocks_linux_test.go and procfs_linux_test.go for more details.
|
|
||||||
var (
|
|
||||||
hookForcePrivateProcRootOpenTree = hookDummyFile
|
|
||||||
hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile
|
|
||||||
hookForceGetProcRootUnsafe = hookDummy
|
|
||||||
|
|
||||||
hookForceProcSelfTask = hookDummy
|
|
||||||
hookForceProcSelf = hookDummy
|
|
||||||
)
|
|
||||||
|
|
||||||
func hookDummy() bool { return false }
|
|
||||||
func hookDummyFile(_ io.Closer) bool { return false }
|
|
|
@ -1,222 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// This code is adapted to be a minimal version of the libpathrs proc resolver
|
|
||||||
// <https://github.com/opensuse/libpathrs/blob/v0.1.3/src/resolvers/procfs.rs>.
|
|
||||||
// As we only need O_PATH|O_NOFOLLOW support, this is not too much to port.
|
|
||||||
|
|
||||||
package procfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/internal/consts"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// procfsLookupInRoot is a stripped down version of completeLookupInRoot,
|
|
||||||
// entirely designed to support the very small set of features necessary to
|
|
||||||
// make procfs handling work. Unlike completeLookupInRoot, we always have
|
|
||||||
// O_PATH|O_NOFOLLOW behaviour for trailing symlinks.
|
|
||||||
//
|
|
||||||
// The main restrictions are:
|
|
||||||
//
|
|
||||||
// - ".." is not supported (as it requires either os.Root-style replays,
|
|
||||||
// which is more bug-prone; or procfs verification, which is not possible
|
|
||||||
// due to re-entrancy issues).
|
|
||||||
// - Absolute symlinks for the same reason (and all absolute symlinks in
|
|
||||||
// procfs are magic-links, which we want to skip anyway).
|
|
||||||
// - If statx is supported (checkSymlinkOvermount), any mount-point crossings
|
|
||||||
// (which is the main attack of concern against /proc).
|
|
||||||
// - Partial lookups are not supported, so the symlink stack is not needed.
|
|
||||||
// - Trailing slash special handling is not necessary in most cases (if we
|
|
||||||
// operating on procfs, it's usually with programmer-controlled strings
|
|
||||||
// that will then be re-opened), so we skip it since whatever re-opens it
|
|
||||||
// can deal with it. It's a creature comfort anyway.
|
|
||||||
//
|
|
||||||
// If the system supports openat2(), this is implemented using equivalent flags
|
|
||||||
// (RESOLVE_BENEATH | RESOLVE_NO_XDEV | RESOLVE_NO_MAGICLINKS).
|
|
||||||
func procfsLookupInRoot(procRoot fd.Fd, unsafePath string) (Handle *os.File, _ error) {
|
|
||||||
unsafePath = filepath.ToSlash(unsafePath) // noop
|
|
||||||
|
|
||||||
// Make sure that an empty unsafe path still returns something sane, even
|
|
||||||
// with openat2 (which doesn't have AT_EMPTY_PATH semantics yet).
|
|
||||||
if unsafePath == "" {
|
|
||||||
unsafePath = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is already checked by getProcRoot, but make sure here since the
|
|
||||||
// core security of this lookup is based on this assumption.
|
|
||||||
if err := verifyProcRoot(procRoot); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if linux.HasOpenat2() {
|
|
||||||
// We prefer being able to use RESOLVE_NO_XDEV if we can, to be
|
|
||||||
// absolutely sure we are operating on a clean /proc handle that
|
|
||||||
// doesn't have any cheeky overmounts that could trick us (including
|
|
||||||
// symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't
|
|
||||||
// strictly needed, but just use it since we have it.
|
|
||||||
//
|
|
||||||
// NOTE: /proc/self is technically a magic-link (the contents of the
|
|
||||||
// symlink are generated dynamically), but it doesn't use
|
|
||||||
// nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it.
|
|
||||||
//
|
|
||||||
// TODO: It would be nice to have RESOLVE_NO_DOTDOT, purely for
|
|
||||||
// self-consistency with the backup O_PATH resolver.
|
|
||||||
handle, err := fd.Openat2(procRoot, unsafePath, &unix.OpenHow{
|
|
||||||
Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC,
|
|
||||||
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
|
||||||
// multiple %w verbs for this wrapping. For now we need to use a
|
|
||||||
// compatibility shim for older Go versions.
|
|
||||||
// err = fmt.Errorf("%w: %w", errUnsafeProcfs, err)
|
|
||||||
return nil, gocompat.WrapBaseError(err, errUnsafeProcfs)
|
|
||||||
}
|
|
||||||
return handle, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// To mirror openat2(RESOLVE_BENEATH), we need to return an error if the
|
|
||||||
// path is absolute.
|
|
||||||
if path.IsAbs(unsafePath) {
|
|
||||||
return nil, fmt.Errorf("%w: cannot resolve absolute paths in procfs resolver", internal.ErrPossibleBreakout)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDir, err := fd.Dup(procRoot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("clone root fd: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
// If a handle is not returned, close the internal handle.
|
|
||||||
if Handle == nil {
|
|
||||||
_ = currentDir.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var (
|
|
||||||
linksWalked int
|
|
||||||
currentPath string
|
|
||||||
remainingPath = unsafePath
|
|
||||||
)
|
|
||||||
for remainingPath != "" {
|
|
||||||
// Get the next path component.
|
|
||||||
var part string
|
|
||||||
if i := strings.IndexByte(remainingPath, '/'); i == -1 {
|
|
||||||
part, remainingPath = remainingPath, ""
|
|
||||||
} else {
|
|
||||||
part, remainingPath = remainingPath[:i], remainingPath[i+1:]
|
|
||||||
}
|
|
||||||
if part == "" {
|
|
||||||
// no-op component, but treat it the same as "."
|
|
||||||
part = "."
|
|
||||||
}
|
|
||||||
if part == ".." {
|
|
||||||
// not permitted
|
|
||||||
return nil, fmt.Errorf("%w: cannot walk into '..' in procfs resolver", internal.ErrPossibleBreakout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the component lexically to the path we are building.
|
|
||||||
// currentPath does not contain any symlinks, and we are lexically
|
|
||||||
// dealing with a single component, so it's okay to do a filepath.Clean
|
|
||||||
// here. (Not to mention that ".." isn't allowed.)
|
|
||||||
nextPath := path.Join("/", currentPath, part)
|
|
||||||
// If we logically hit the root, just clone the root rather than
|
|
||||||
// opening the part and doing all of the other checks.
|
|
||||||
if nextPath == "/" {
|
|
||||||
// Jump to root.
|
|
||||||
rootClone, err := fd.Dup(procRoot)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("clone root fd: %w", err)
|
|
||||||
}
|
|
||||||
_ = currentDir.Close()
|
|
||||||
currentDir = rootClone
|
|
||||||
currentPath = nextPath
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to open the next component.
|
|
||||||
nextDir, err := fd.Openat(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure we are still on procfs and haven't crossed mounts.
|
|
||||||
if err := verifyProcHandle(nextDir); err != nil {
|
|
||||||
_ = nextDir.Close()
|
|
||||||
return nil, fmt.Errorf("check %q component is on procfs: %w", part, err)
|
|
||||||
}
|
|
||||||
if err := checkSubpathOvermount(procRoot, nextDir, ""); err != nil {
|
|
||||||
_ = nextDir.Close()
|
|
||||||
return nil, fmt.Errorf("check %q component is not overmounted: %w", part, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are emulating O_PATH|O_NOFOLLOW, so we only need to traverse into
|
|
||||||
// trailing symlinks if we are not the final component. Otherwise we
|
|
||||||
// can just return the currentDir.
|
|
||||||
if remainingPath != "" {
|
|
||||||
st, err := nextDir.Stat()
|
|
||||||
if err != nil {
|
|
||||||
_ = nextDir.Close()
|
|
||||||
return nil, fmt.Errorf("stat component %q: %w", part, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if st.Mode()&os.ModeType == os.ModeSymlink {
|
|
||||||
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See
|
|
||||||
// Linux commit 65cfc6722361 ("readlinkat(), fchownat() and
|
|
||||||
// fstatat() with empty relative pathnames").
|
|
||||||
linkDest, err := fd.Readlinkat(nextDir, "")
|
|
||||||
// We don't need the handle anymore.
|
|
||||||
_ = nextDir.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
linksWalked++
|
|
||||||
if linksWalked > consts.MaxSymlinkLimit {
|
|
||||||
return nil, &os.PathError{Op: "securejoin.procfsLookupInRoot", Path: "/proc/" + unsafePath, Err: unix.ELOOP}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update our logical remaining path.
|
|
||||||
remainingPath = linkDest + "/" + remainingPath
|
|
||||||
// Absolute symlinks are probably magiclinks, we reject them.
|
|
||||||
if path.IsAbs(linkDest) {
|
|
||||||
return nil, fmt.Errorf("%w: cannot jump to / in procfs resolver -- possible magiclink", internal.ErrPossibleBreakout)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk into the next component.
|
|
||||||
_ = currentDir.Close()
|
|
||||||
currentDir = nextDir
|
|
||||||
currentPath = nextPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// One final sanity-check.
|
|
||||||
if err := verifyProcHandle(currentDir); err != nil {
|
|
||||||
return nil, fmt.Errorf("check final handle is on procfs: %w", err)
|
|
||||||
}
|
|
||||||
if err := checkSubpathOvermount(procRoot, currentDir, ""); err != nil {
|
|
||||||
return nil, fmt.Errorf("check final handle is not overmounted: %w", err)
|
|
||||||
}
|
|
||||||
return currentDir, nil
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package pathrs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd"
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/procfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func openat2(dir fd.Fd, path string, how *unix.OpenHow) (*os.File, error) {
|
|
||||||
file, err := fd.Openat2(dir, path, how)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// If we are using RESOLVE_IN_ROOT, the name we generated may be wrong.
|
|
||||||
if how.Resolve&unix.RESOLVE_IN_ROOT == unix.RESOLVE_IN_ROOT {
|
|
||||||
if actualPath, err := procfs.ProcSelfFdReadlink(file); err == nil {
|
|
||||||
// TODO: Ideally we would not need to dup the fd, but you cannot
|
|
||||||
// easily just swap an *os.File with one from the same fd
|
|
||||||
// (the GC will close the old one, and you cannot clear the
|
|
||||||
// finaliser easily because it is associated with an internal
|
|
||||||
// field of *os.File not *os.File itself).
|
|
||||||
newFile, err := fd.DupWithName(file, actualPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
file = newFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupOpenat2(root fd.Fd, unsafePath string, partial bool) (*os.File, string, error) {
|
|
||||||
if !partial {
|
|
||||||
file, err := openat2(root, unsafePath, &unix.OpenHow{
|
|
||||||
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
|
||||||
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
|
||||||
})
|
|
||||||
return file, "", err
|
|
||||||
}
|
|
||||||
return partialLookupOpenat2(root, unsafePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// partialLookupOpenat2 is an alternative implementation of
|
|
||||||
// partialLookupInRoot, using openat2(RESOLVE_IN_ROOT) to more safely get a
|
|
||||||
// handle to the deepest existing child of the requested path within the root.
|
|
||||||
func partialLookupOpenat2(root fd.Fd, unsafePath string) (*os.File, string, error) {
|
|
||||||
// TODO: Implement this as a git-bisect-like binary search.
|
|
||||||
|
|
||||||
unsafePath = filepath.ToSlash(unsafePath) // noop
|
|
||||||
endIdx := len(unsafePath)
|
|
||||||
var lastError error
|
|
||||||
for endIdx > 0 {
|
|
||||||
subpath := unsafePath[:endIdx]
|
|
||||||
|
|
||||||
handle, err := openat2(root, subpath, &unix.OpenHow{
|
|
||||||
Flags: unix.O_PATH | unix.O_CLOEXEC,
|
|
||||||
Resolve: unix.RESOLVE_IN_ROOT | unix.RESOLVE_NO_MAGICLINKS,
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
// Jump over the slash if we have a non-"" remainingPath.
|
|
||||||
if endIdx < len(unsafePath) {
|
|
||||||
endIdx++
|
|
||||||
}
|
|
||||||
// We found a subpath!
|
|
||||||
return handle, unsafePath[endIdx:], lastError
|
|
||||||
}
|
|
||||||
if errors.Is(err, unix.ENOENT) || errors.Is(err, unix.ENOTDIR) {
|
|
||||||
// That path doesn't exist, let's try the next directory up.
|
|
||||||
endIdx = strings.LastIndexByte(subpath, '/')
|
|
||||||
lastError = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, "", fmt.Errorf("open subpath: %w", err)
|
|
||||||
}
|
|
||||||
// If we couldn't open anything, the whole subpath is missing. Return a
|
|
||||||
// copy of the root fd so that the caller doesn't close this one by
|
|
||||||
// accident.
|
|
||||||
rootClone, err := fd.Dup(root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
return rootClone, unsafePath, lastError
|
|
||||||
}
|
|
157
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_linux.go
generated
vendored
157
vendor/github.com/cyphar/filepath-securejoin/pathrs-lite/procfs/procfs_linux.go
generated
vendored
|
@ -1,157 +0,0 @@
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
// Copyright (C) 2024-2025 Aleksa Sarai <cyphar@cyphar.com>
|
|
||||||
// Copyright (C) 2024-2025 SUSE LLC
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
// Package procfs provides a safe API for operating on /proc on Linux.
|
|
||||||
package procfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This package mostly just wraps internal/procfs APIs. This is necessary
|
|
||||||
// because we are forced to export some things from internal/procfs in order to
|
|
||||||
// avoid some dependency cycle issues, but we don't want users to see or use
|
|
||||||
// them.
|
|
||||||
|
|
||||||
// ProcThreadSelfCloser is a callback that needs to be called when you are done
|
|
||||||
// operating on an [os.File] fetched using [Handle.OpenThreadSelf].
|
|
||||||
//
|
|
||||||
// [os.File]: https://pkg.go.dev/os#File
|
|
||||||
type ProcThreadSelfCloser = procfs.ProcThreadSelfCloser
|
|
||||||
|
|
||||||
// Handle is a wrapper around an *os.File handle to "/proc", which can be used
|
|
||||||
// to do further procfs-related operations in a safe way.
|
|
||||||
type Handle struct {
|
|
||||||
inner *procfs.Handle
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close close the resources associated with this [Handle]. Note that if this
|
|
||||||
// [Handle] was created with [OpenProcRoot], on some kernels the underlying
|
|
||||||
// procfs handle is cached and so this Close operation may be a no-op. However,
|
|
||||||
// you should always call Close on [Handle]s once you are done with them.
|
|
||||||
func (proc *Handle) Close() error { return proc.inner.Close() }
|
|
||||||
|
|
||||||
// OpenProcRoot tries to open a "safer" handle to "/proc" (i.e., one with the
|
|
||||||
// "subset=pid" mount option applied, available from Linux 5.8). Unless you
|
|
||||||
// plan to do many [Handle.OpenRoot] operations, users should prefer to use
|
|
||||||
// this over [OpenUnsafeProcRoot] which is far more dangerous to keep open.
|
|
||||||
//
|
|
||||||
// If a safe handle cannot be opened, OpenProcRoot will fall back to opening a
|
|
||||||
// regular "/proc" handle.
|
|
||||||
//
|
|
||||||
// Note that using [Handle.OpenRoot] will still work with handles returned by
|
|
||||||
// this function. If a subpath cannot be operated on with a safe "/proc"
|
|
||||||
// handle, then [OpenUnsafeProcRoot] will be called internally and a temporary
|
|
||||||
// unsafe handle will be used.
|
|
||||||
func OpenProcRoot() (*Handle, error) {
|
|
||||||
proc, err := procfs.OpenProcRoot()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Handle{inner: proc}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenUnsafeProcRoot opens a handle to "/proc" without any overmounts or
|
|
||||||
// masked paths. You must be extremely careful to make sure this handle is
|
|
||||||
// never leaked to a container and that you program cannot be tricked into
|
|
||||||
// writing to arbitrary paths within it.
|
|
||||||
//
|
|
||||||
// This is not necessary if you just wish to use [Handle.OpenRoot], as handles
|
|
||||||
// returned by [OpenProcRoot] will fall back to using a *temporary* unsafe
|
|
||||||
// handle in that case. You should only really use this if you need to do many
|
|
||||||
// operations with [Handle.OpenRoot] and the performance overhead of making
|
|
||||||
// many procfs handles is an issue. If you do use OpenUnsafeProcRoot, you
|
|
||||||
// should make sure to close the handle as soon as possible to avoid
|
|
||||||
// known-fd-number attacks.
|
|
||||||
func OpenUnsafeProcRoot() (*Handle, error) {
|
|
||||||
proc, err := procfs.OpenUnsafeProcRoot()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Handle{inner: proc}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenThreadSelf returns a handle to "/proc/thread-self/<subpath>" (or an
|
|
||||||
// equivalent handle on older kernels where "/proc/thread-self" doesn't exist).
|
|
||||||
// Once finished with the handle, you must call the returned closer function
|
|
||||||
// ([runtime.UnlockOSThread]). You must not pass the returned *os.File to other
|
|
||||||
// Go threads or use the handle after calling the closer.
|
|
||||||
//
|
|
||||||
// [runtime.UnlockOSThread]: https://pkg.go.dev/runtime#UnlockOSThread
|
|
||||||
func (proc *Handle) OpenThreadSelf(subpath string) (*os.File, ProcThreadSelfCloser, error) {
|
|
||||||
return proc.inner.OpenThreadSelf(subpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenSelf returns a handle to /proc/self/<subpath>.
|
|
||||||
//
|
|
||||||
// Note that in Go programs with non-homogenous threads, this may result in
|
|
||||||
// spurious errors. If you are monkeying around with APIs that are
|
|
||||||
// thread-specific, you probably want to use [Handle.OpenThreadSelf] instead
|
|
||||||
// which will guarantee that the handle refers to the same thread as the caller
|
|
||||||
// is executing on.
|
|
||||||
func (proc *Handle) OpenSelf(subpath string) (*os.File, error) {
|
|
||||||
return proc.inner.OpenSelf(subpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenRoot returns a handle to /proc/<subpath>.
|
|
||||||
//
|
|
||||||
// You should only use this when you need to operate on global procfs files
|
|
||||||
// (such as sysctls in /proc/sys). Unlike [Handle.OpenThreadSelf],
|
|
||||||
// [Handle.OpenSelf], and [Handle.OpenPid], the procfs handle used internally
|
|
||||||
// for this operation will never use "subset=pid", which makes it a more juicy
|
|
||||||
// target for [CVE-2024-21626]-style attacks (and doing something like opening
|
|
||||||
// a directory with OpenRoot effectively leaks [OpenUnsafeProcRoot] as long as
|
|
||||||
// the file descriptor is open).
|
|
||||||
//
|
|
||||||
// [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv
|
|
||||||
func (proc *Handle) OpenRoot(subpath string) (*os.File, error) {
|
|
||||||
return proc.inner.OpenRoot(subpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenPid returns a handle to /proc/$pid/<subpath> (pid can be a pid or tid).
|
|
||||||
// This is mainly intended for usage when operating on other processes.
|
|
||||||
//
|
|
||||||
// You should not use this for the current thread, as special handling is
|
|
||||||
// needed for /proc/thread-self (or /proc/self/task/<tid>) when dealing with
|
|
||||||
// goroutine scheduling -- use [Handle.OpenThreadSelf] instead.
|
|
||||||
//
|
|
||||||
// To refer to the current thread-group, you should use prefer
|
|
||||||
// [Handle.OpenSelf] to passing os.Getpid as the pid argument.
|
|
||||||
func (proc *Handle) OpenPid(pid int, subpath string) (*os.File, error) {
|
|
||||||
return proc.inner.OpenPid(pid, subpath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcSelfFdReadlink gets the real path of the given file by looking at
|
|
||||||
// /proc/self/fd/<fd> with [readlink]. It is effectively just shorthand for
|
|
||||||
// something along the lines of:
|
|
||||||
//
|
|
||||||
// proc, err := procfs.OpenProcRoot()
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// link, err := proc.OpenThreadSelf(fmt.Sprintf("fd/%d", f.Fd()))
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// defer link.Close()
|
|
||||||
// var buf [4096]byte
|
|
||||||
// n, err := unix.Readlinkat(int(link.Fd()), "", buf[:])
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// pathname := buf[:n]
|
|
||||||
//
|
|
||||||
// [readlink]: https://pkg.go.dev/golang.org/x/sys/unix#Readlinkat
|
|
||||||
func ProcSelfFdReadlink(f *os.File) (string, error) {
|
|
||||||
return procfs.ProcSelfFdReadlink(f)
|
|
||||||
}
|
|
|
@ -0,0 +1,452 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
// Copyright (C) 2024 SUSE LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package securejoin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fstat(f *os.File) (unix.Stat_t, error) {
|
||||||
|
var stat unix.Stat_t
|
||||||
|
if err := unix.Fstat(int(f.Fd()), &stat); err != nil {
|
||||||
|
return stat, &os.PathError{Op: "fstat", Path: f.Name(), Err: err}
|
||||||
|
}
|
||||||
|
return stat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fstatfs(f *os.File) (unix.Statfs_t, error) {
|
||||||
|
var statfs unix.Statfs_t
|
||||||
|
if err := unix.Fstatfs(int(f.Fd()), &statfs); err != nil {
|
||||||
|
return statfs, &os.PathError{Op: "fstatfs", Path: f.Name(), Err: err}
|
||||||
|
}
|
||||||
|
return statfs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The kernel guarantees that the root inode of a procfs mount has an
|
||||||
|
// f_type of PROC_SUPER_MAGIC and st_ino of PROC_ROOT_INO.
|
||||||
|
const (
|
||||||
|
procSuperMagic = 0x9fa0 // PROC_SUPER_MAGIC
|
||||||
|
procRootIno = 1 // PROC_ROOT_INO
|
||||||
|
)
|
||||||
|
|
||||||
|
func verifyProcRoot(procRoot *os.File) error {
|
||||||
|
if statfs, err := fstatfs(procRoot); err != nil {
|
||||||
|
return err
|
||||||
|
} else if statfs.Type != procSuperMagic {
|
||||||
|
return fmt.Errorf("%w: incorrect procfs root filesystem type 0x%x", errUnsafeProcfs, statfs.Type)
|
||||||
|
}
|
||||||
|
if stat, err := fstat(procRoot); err != nil {
|
||||||
|
return err
|
||||||
|
} else if stat.Ino != procRootIno {
|
||||||
|
return fmt.Errorf("%w: incorrect procfs root inode number %d", errUnsafeProcfs, stat.Ino)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasNewMountApi = sync_OnceValue(func() bool {
|
||||||
|
// All of the pieces of the new mount API we use (fsopen, fsconfig,
|
||||||
|
// fsmount, open_tree) were added together in Linux 5.1[1,2], so we can
|
||||||
|
// just check for one of the syscalls and the others should also be
|
||||||
|
// available.
|
||||||
|
//
|
||||||
|
// Just try to use open_tree(2) to open a file without OPEN_TREE_CLONE.
|
||||||
|
// This is equivalent to openat(2), but tells us if open_tree is
|
||||||
|
// available (and thus all of the other basic new mount API syscalls).
|
||||||
|
// open_tree(2) is most light-weight syscall to test here.
|
||||||
|
//
|
||||||
|
// [1]: merge commit 400913252d09
|
||||||
|
// [2]: <https://lore.kernel.org/lkml/153754740781.17872.7869536526927736855.stgit@warthog.procyon.org.uk/>
|
||||||
|
fd, err := unix.OpenTree(-int(unix.EBADF), "/", unix.OPEN_TREE_CLOEXEC)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ = unix.Close(fd)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
func fsopen(fsName string, flags int) (*os.File, error) {
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.FSOPEN_CLOEXEC
|
||||||
|
fd, err := unix.Fsopen(fsName, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("fsopen "+fsName, err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), "fscontext:"+fsName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fsmount(ctx *os.File, flags, mountAttrs int) (*os.File, error) {
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.FSMOUNT_CLOEXEC
|
||||||
|
fd, err := unix.Fsmount(int(ctx.Fd()), flags, mountAttrs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("fsmount "+ctx.Name(), err)
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), "fsmount:"+ctx.Name()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrivateProcMount() (*os.File, error) {
|
||||||
|
procfsCtx, err := fsopen("proc", unix.FSOPEN_CLOEXEC)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer procfsCtx.Close()
|
||||||
|
|
||||||
|
// Try to configure hidepid=ptraceable,subset=pid if possible, but ignore errors.
|
||||||
|
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "hidepid", "ptraceable")
|
||||||
|
_ = unix.FsconfigSetString(int(procfsCtx.Fd()), "subset", "pid")
|
||||||
|
|
||||||
|
// Get an actual handle.
|
||||||
|
if err := unix.FsconfigCreate(int(procfsCtx.Fd())); err != nil {
|
||||||
|
return nil, os.NewSyscallError("fsconfig create procfs", err)
|
||||||
|
}
|
||||||
|
return fsmount(procfsCtx, unix.FSMOUNT_CLOEXEC, unix.MS_RDONLY|unix.MS_NODEV|unix.MS_NOEXEC|unix.MS_NOSUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openTree(dir *os.File, path string, flags uint) (*os.File, error) {
|
||||||
|
dirFd := -int(unix.EBADF)
|
||||||
|
dirName := "."
|
||||||
|
if dir != nil {
|
||||||
|
dirFd = int(dir.Fd())
|
||||||
|
dirName = dir.Name()
|
||||||
|
}
|
||||||
|
// Make sure we always set O_CLOEXEC.
|
||||||
|
flags |= unix.OPEN_TREE_CLOEXEC
|
||||||
|
fd, err := unix.OpenTree(dirFd, path, flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &os.PathError{Op: "open_tree", Path: path, Err: err}
|
||||||
|
}
|
||||||
|
return os.NewFile(uintptr(fd), dirName+"/"+path), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clonePrivateProcMount() (_ *os.File, Err error) {
|
||||||
|
// Try to make a clone without using AT_RECURSIVE if we can. If this works,
|
||||||
|
// we can be sure there are no over-mounts and so if the root is valid then
|
||||||
|
// we're golden. Otherwise, we have to deal with over-mounts.
|
||||||
|
procfsHandle, err := openTree(nil, "/proc", unix.OPEN_TREE_CLONE)
|
||||||
|
if err != nil || hookForcePrivateProcRootOpenTreeAtRecursive(procfsHandle) {
|
||||||
|
procfsHandle, err = openTree(nil, "/proc", unix.OPEN_TREE_CLONE|unix.AT_RECURSIVE)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating a detached procfs clone: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
_ = procfsHandle.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := verifyProcRoot(procfsHandle); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return procfsHandle, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privateProcRoot() (*os.File, error) {
|
||||||
|
if !hasNewMountApi() || hookForceGetProcRootUnsafe() {
|
||||||
|
return nil, fmt.Errorf("new mount api: %w", unix.ENOTSUP)
|
||||||
|
}
|
||||||
|
// Try to create a new procfs mount from scratch if we can. This ensures we
|
||||||
|
// can get a procfs mount even if /proc is fake (for whatever reason).
|
||||||
|
procRoot, err := newPrivateProcMount()
|
||||||
|
if err != nil || hookForcePrivateProcRootOpenTree(procRoot) {
|
||||||
|
// Try to clone /proc then...
|
||||||
|
procRoot, err = clonePrivateProcMount()
|
||||||
|
}
|
||||||
|
return procRoot, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsafeHostProcRoot() (_ *os.File, Err error) {
|
||||||
|
procRoot, err := os.OpenFile("/proc", unix.O_PATH|unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
_ = procRoot.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := verifyProcRoot(procRoot); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return procRoot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doGetProcRoot() (*os.File, error) {
|
||||||
|
procRoot, err := privateProcRoot()
|
||||||
|
if err != nil {
|
||||||
|
// Fall back to using a /proc handle if making a private mount failed.
|
||||||
|
// If we have openat2, at least we can avoid some kinds of over-mount
|
||||||
|
// attacks, but without openat2 there's not much we can do.
|
||||||
|
procRoot, err = unsafeHostProcRoot()
|
||||||
|
}
|
||||||
|
return procRoot, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var getProcRoot = sync_OnceValues(func() (*os.File, error) {
|
||||||
|
return doGetProcRoot()
|
||||||
|
})
|
||||||
|
|
||||||
|
var hasProcThreadSelf = sync_OnceValue(func() bool {
|
||||||
|
return unix.Access("/proc/thread-self/", unix.F_OK) == nil
|
||||||
|
})
|
||||||
|
|
||||||
|
var errUnsafeProcfs = errors.New("unsafe procfs detected")
|
||||||
|
|
||||||
|
type procThreadSelfCloser func()
|
||||||
|
|
||||||
|
// procThreadSelf returns a handle to /proc/thread-self/<subpath> (or an
|
||||||
|
// equivalent handle on older kernels where /proc/thread-self doesn't exist).
|
||||||
|
// Once finished with the handle, you must call the returned closer function
|
||||||
|
// (runtime.UnlockOSThread). You must not pass the returned *os.File to other
|
||||||
|
// Go threads or use the handle after calling the closer.
|
||||||
|
//
|
||||||
|
// This is similar to ProcThreadSelf from runc, but with extra hardening
|
||||||
|
// applied and using *os.File.
|
||||||
|
func procThreadSelf(procRoot *os.File, subpath string) (_ *os.File, _ procThreadSelfCloser, Err error) {
|
||||||
|
// We need to lock our thread until the caller is done with the handle
|
||||||
|
// because between getting the handle and using it we could get interrupted
|
||||||
|
// by the Go runtime and hit the case where the underlying thread is
|
||||||
|
// swapped out and the original thread is killed, resulting in
|
||||||
|
// pull-your-hair-out-hard-to-debug issues in the caller.
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
runtime.UnlockOSThread()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Figure out what prefix we want to use.
|
||||||
|
threadSelf := "thread-self/"
|
||||||
|
if !hasProcThreadSelf() || hookForceProcSelfTask() {
|
||||||
|
/// Pre-3.17 kernels don't have /proc/thread-self, so do it manually.
|
||||||
|
threadSelf = "self/task/" + strconv.Itoa(unix.Gettid()) + "/"
|
||||||
|
if _, err := fstatatFile(procRoot, threadSelf, unix.AT_SYMLINK_NOFOLLOW); err != nil || hookForceProcSelf() {
|
||||||
|
// In this case, we running in a pid namespace that doesn't match
|
||||||
|
// the /proc mount we have. This can happen inside runc.
|
||||||
|
//
|
||||||
|
// Unfortunately, there is no nice way to get the correct TID to
|
||||||
|
// use here because of the age of the kernel, so we have to just
|
||||||
|
// use /proc/self and hope that it works.
|
||||||
|
threadSelf = "self/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab the handle.
|
||||||
|
var (
|
||||||
|
handle *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if hasOpenat2() {
|
||||||
|
// We prefer being able to use RESOLVE_NO_XDEV if we can, to be
|
||||||
|
// absolutely sure we are operating on a clean /proc handle that
|
||||||
|
// doesn't have any cheeky overmounts that could trick us (including
|
||||||
|
// symlink mounts on top of /proc/thread-self). RESOLVE_BENEATH isn't
|
||||||
|
// strictly needed, but just use it since we have it.
|
||||||
|
//
|
||||||
|
// NOTE: /proc/self is technically a magic-link (the contents of the
|
||||||
|
// symlink are generated dynamically), but it doesn't use
|
||||||
|
// nd_jump_link() so RESOLVE_NO_MAGICLINKS allows it.
|
||||||
|
//
|
||||||
|
// NOTE: We MUST NOT use RESOLVE_IN_ROOT here, as openat2File uses
|
||||||
|
// procSelfFdReadlink to clean up the returned f.Name() if we use
|
||||||
|
// RESOLVE_IN_ROOT (which would lead to an infinite recursion).
|
||||||
|
handle, err = openat2File(procRoot, threadSelf+subpath, &unix.OpenHow{
|
||||||
|
Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC,
|
||||||
|
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_MAGICLINKS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||||
|
// multiple %w verbs for this wrapping. For now we need to use a
|
||||||
|
// compatibility shim for older Go versions.
|
||||||
|
//err = fmt.Errorf("%w: %w", errUnsafeProcfs, err)
|
||||||
|
return nil, nil, wrapBaseError(err, errUnsafeProcfs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handle, err = openatFile(procRoot, threadSelf+subpath, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
||||||
|
if err != nil {
|
||||||
|
// TODO: Once we bump the minimum Go version to 1.20, we can use
|
||||||
|
// multiple %w verbs for this wrapping. For now we need to use a
|
||||||
|
// compatibility shim for older Go versions.
|
||||||
|
//err = fmt.Errorf("%w: %w", errUnsafeProcfs, err)
|
||||||
|
return nil, nil, wrapBaseError(err, errUnsafeProcfs)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if Err != nil {
|
||||||
|
_ = handle.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// We can't detect bind-mounts of different parts of procfs on top of
|
||||||
|
// /proc (a-la RESOLVE_NO_XDEV), but we can at least be sure that we
|
||||||
|
// aren't on the wrong filesystem here.
|
||||||
|
if statfs, err := fstatfs(handle); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else if statfs.Type != procSuperMagic {
|
||||||
|
return nil, nil, fmt.Errorf("%w: incorrect /proc/self/fd filesystem type 0x%x", errUnsafeProcfs, statfs.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handle, runtime.UnlockOSThread, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATX_MNT_ID_UNIQUE is provided in golang.org/x/sys@v0.20.0, but in order to
|
||||||
|
// avoid bumping the requirement for a single constant we can just define it
|
||||||
|
// ourselves.
|
||||||
|
const STATX_MNT_ID_UNIQUE = 0x4000
|
||||||
|
|
||||||
|
var hasStatxMountId = sync_OnceValue(func() bool {
|
||||||
|
var (
|
||||||
|
stx unix.Statx_t
|
||||||
|
// We don't care which mount ID we get. The kernel will give us the
|
||||||
|
// unique one if it is supported.
|
||||||
|
wantStxMask uint32 = STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
|
||||||
|
)
|
||||||
|
err := unix.Statx(-int(unix.EBADF), "/", 0, int(wantStxMask), &stx)
|
||||||
|
return err == nil && stx.Mask&wantStxMask != 0
|
||||||
|
})
|
||||||
|
|
||||||
|
func getMountId(dir *os.File, path string) (uint64, error) {
|
||||||
|
// If we don't have statx(STATX_MNT_ID*) support, we can't do anything.
|
||||||
|
if !hasStatxMountId() {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
stx unix.Statx_t
|
||||||
|
// We don't care which mount ID we get. The kernel will give us the
|
||||||
|
// unique one if it is supported.
|
||||||
|
wantStxMask uint32 = STATX_MNT_ID_UNIQUE | unix.STATX_MNT_ID
|
||||||
|
)
|
||||||
|
|
||||||
|
err := unix.Statx(int(dir.Fd()), path, unix.AT_EMPTY_PATH|unix.AT_SYMLINK_NOFOLLOW, int(wantStxMask), &stx)
|
||||||
|
if stx.Mask&wantStxMask == 0 {
|
||||||
|
// It's not a kernel limitation, for some reason we couldn't get a
|
||||||
|
// mount ID. Assume it's some kind of attack.
|
||||||
|
err = fmt.Errorf("%w: could not get mount id", errUnsafeProcfs)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, &os.PathError{Op: "statx(STATX_MNT_ID_...)", Path: dir.Name() + "/" + path, Err: err}
|
||||||
|
}
|
||||||
|
return stx.Mnt_id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkSymlinkOvermount(procRoot *os.File, dir *os.File, path string) error {
|
||||||
|
// Get the mntId of our procfs handle.
|
||||||
|
expectedMountId, err := getMountId(procRoot, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Get the mntId of the target magic-link.
|
||||||
|
gotMountId, err := getMountId(dir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// As long as the directory mount is alive, even with wrapping mount IDs,
|
||||||
|
// we would expect to see a different mount ID here. (Of course, if we're
|
||||||
|
// using unsafeHostProcRoot() then an attaker could change this after we
|
||||||
|
// did this check.)
|
||||||
|
if expectedMountId != gotMountId {
|
||||||
|
return fmt.Errorf("%w: symlink %s/%s has an overmount obscuring the real link (mount ids do not match %d != %d)", errUnsafeProcfs, dir.Name(), path, expectedMountId, gotMountId)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRawProcSelfFdReadlink(procRoot *os.File, fd int) (string, error) {
|
||||||
|
fdPath := fmt.Sprintf("fd/%d", fd)
|
||||||
|
procFdLink, closer, err := procThreadSelf(procRoot, fdPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get safe /proc/thread-self/%s handle: %w", fdPath, err)
|
||||||
|
}
|
||||||
|
defer procFdLink.Close()
|
||||||
|
defer closer()
|
||||||
|
|
||||||
|
// Try to detect if there is a mount on top of the magic-link. Since we use the handle directly
|
||||||
|
// provide to the closure. If the closure uses the handle directly, this
|
||||||
|
// should be safe in general (a mount on top of the path afterwards would
|
||||||
|
// not affect the handle itself) and will definitely be safe if we are
|
||||||
|
// using privateProcRoot() (at least since Linux 5.12[1], when anonymous
|
||||||
|
// mount namespaces were completely isolated from external mounts including
|
||||||
|
// mount propagation events).
|
||||||
|
//
|
||||||
|
// [1]: Linux commit ee2e3f50629f ("mount: fix mounting of detached mounts
|
||||||
|
// onto targets that reside on shared mounts").
|
||||||
|
if err := checkSymlinkOvermount(procRoot, procFdLink, ""); err != nil {
|
||||||
|
return "", fmt.Errorf("check safety of /proc/thread-self/fd/%d magiclink: %w", fd, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readlinkat implies AT_EMPTY_PATH since Linux 2.6.39. See Linux commit
|
||||||
|
// 65cfc6722361 ("readlinkat(), fchownat() and fstatat() with empty
|
||||||
|
// relative pathnames").
|
||||||
|
return readlinkatFile(procFdLink, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawProcSelfFdReadlink(fd int) (string, error) {
|
||||||
|
procRoot, err := getProcRoot()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return doRawProcSelfFdReadlink(procRoot, fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func procSelfFdReadlink(f *os.File) (string, error) {
|
||||||
|
return rawProcSelfFdReadlink(int(f.Fd()))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errPossibleBreakout = errors.New("possible breakout detected")
|
||||||
|
errInvalidDirectory = errors.New("wandered into deleted directory")
|
||||||
|
errDeletedInode = errors.New("cannot verify path of deleted inode")
|
||||||
|
)
|
||||||
|
|
||||||
|
func isDeadInode(file *os.File) error {
|
||||||
|
// If the nlink of a file drops to 0, there is an attacker deleting
|
||||||
|
// directories during our walk, which could result in weird /proc values.
|
||||||
|
// It's better to error out in this case.
|
||||||
|
stat, err := fstat(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("check for dead inode: %w", err)
|
||||||
|
}
|
||||||
|
if stat.Nlink == 0 {
|
||||||
|
err := errDeletedInode
|
||||||
|
if stat.Mode&unix.S_IFMT == unix.S_IFDIR {
|
||||||
|
err = errInvalidDirectory
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w %q", err, file.Name())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkProcSelfFdPath(path string, file *os.File) error {
|
||||||
|
if err := isDeadInode(file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
actualPath, err := procSelfFdReadlink(file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get path of handle: %w", err)
|
||||||
|
}
|
||||||
|
if actualPath != path {
|
||||||
|
return fmt.Errorf("%w: handle path %q doesn't match expected path %q", errPossibleBreakout, actualPath, path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test hooks used in the procfs tests to verify that the fallback logic works.
|
||||||
|
// See testing_mocks_linux_test.go and procfs_linux_test.go for more details.
|
||||||
|
var (
|
||||||
|
hookForcePrivateProcRootOpenTree = hookDummyFile
|
||||||
|
hookForcePrivateProcRootOpenTreeAtRecursive = hookDummyFile
|
||||||
|
hookForceGetProcRootUnsafe = hookDummy
|
||||||
|
|
||||||
|
hookForceProcSelfTask = hookDummy
|
||||||
|
hookForceProcSelf = hookDummy
|
||||||
|
)
|
||||||
|
|
||||||
|
func hookDummy() bool { return false }
|
||||||
|
func hookDummyFile(_ *os.File) bool { return false }
|
|
@ -1,5 +1,3 @@
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
// Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
|
@ -93,19 +93,9 @@ github.com/coreos/go-systemd/v22/dbus
|
||||||
# github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467
|
# github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467
|
||||||
## explicit
|
## explicit
|
||||||
github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer
|
github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer
|
||||||
# github.com/cyphar/filepath-securejoin v0.5.0
|
# github.com/cyphar/filepath-securejoin v0.4.1
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/cyphar/filepath-securejoin
|
github.com/cyphar/filepath-securejoin
|
||||||
github.com/cyphar/filepath-securejoin/internal/consts
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/assert
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/fd
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/gocompat
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/kernelversion
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/linux
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/internal/procfs
|
|
||||||
github.com/cyphar/filepath-securejoin/pathrs-lite/procfs
|
|
||||||
# github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
# github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||||
## explicit
|
## explicit
|
||||||
github.com/davecgh/go-spew/spew
|
github.com/davecgh/go-spew/spew
|
||||||
|
|
Loading…
Reference in New Issue