mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-03 19:58:17 +00:00 
			
		
		
		
	kubelet: use new securejoin API
Using securejoin.SecureJoin() ensures that paths are bound within a given root, but it doesn't protect from changes happening between the construction of the path and its use. securejoin 0.3 introduces a new Linux-specific API which avoids this by making rooted open operations explicit; this migrates kubelet's log retrieval to use that. Signed-off-by: Stephen Kitt <skitt@redhat.com>
This commit is contained in:
		
							
								
								
									
										4
									
								
								LICENSES/vendor/github.com/cyphar/filepath-securejoin/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								LICENSES/vendor/github.com/cyphar/filepath-securejoin/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,7 +1,7 @@
 | 
				
			|||||||
= vendor/github.com/cyphar/filepath-securejoin licensed under: =
 | 
					= vendor/github.com/cyphar/filepath-securejoin licensed under: =
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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 SUSE LLC. All rights reserved.
 | 
					Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Redistribution and use in source and binary forms, with or without
 | 
					Redistribution and use in source and binary forms, with or without
 | 
				
			||||||
modification, are permitted provided that the following conditions are
 | 
					modification, are permitted provided that the following conditions are
 | 
				
			||||||
@@ -29,4 +29,4 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 | 
				
			|||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 | 
					(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 | 
				
			||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | 
					OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
= vendor/github.com/cyphar/filepath-securejoin/LICENSE 8d322afab99e1998dbfcc712f94e824d
 | 
					= vendor/github.com/cyphar/filepath-securejoin/LICENSE 7e05df0b39896d74600ef94ab46dce89
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@@ -24,7 +24,7 @@ require (
 | 
				
			|||||||
	github.com/coreos/go-oidc v2.2.1+incompatible
 | 
						github.com/coreos/go-oidc v2.2.1+incompatible
 | 
				
			||||||
	github.com/coreos/go-systemd/v22 v22.5.0
 | 
						github.com/coreos/go-systemd/v22 v22.5.0
 | 
				
			||||||
	github.com/cpuguy83/go-md2man/v2 v2.0.4
 | 
						github.com/cpuguy83/go-md2man/v2 v2.0.4
 | 
				
			||||||
	github.com/cyphar/filepath-securejoin v0.2.4
 | 
						github.com/cyphar/filepath-securejoin v0.3.4
 | 
				
			||||||
	github.com/distribution/reference v0.6.0
 | 
						github.com/distribution/reference v0.6.0
 | 
				
			||||||
	github.com/docker/go-units v0.5.0
 | 
						github.com/docker/go-units v0.5.0
 | 
				
			||||||
	github.com/emicklei/go-restful/v3 v3.11.0
 | 
						github.com/emicklei/go-restful/v3 v3.11.0
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@@ -210,8 +210,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 | 
				
			|||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
					github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
				
			||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
 | 
					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/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
 | 
					github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
 | 
				
			||||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
 | 
					github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
 | 
				
			||||||
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=
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -361,12 +361,7 @@ func heuristicsCopyFileLogs(ctx context.Context, w io.Writer, logDir, service st
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	for _, logFileName := range logFileNames {
 | 
						for _, logFileName := range logFileNames {
 | 
				
			||||||
		var logFile string
 | 
							err = heuristicsCopyFileLog(ctx, w, logDir, logFileName)
 | 
				
			||||||
		logFile, err = securejoin.SecureJoin(logDir, logFileName)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			break
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		err = heuristicsCopyFileLog(ctx, w, logFile)
 | 
					 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
			break
 | 
								break
 | 
				
			||||||
		} else if errors.Is(err, os.ErrNotExist) {
 | 
							} else if errors.Is(err, os.ErrNotExist) {
 | 
				
			||||||
@@ -407,30 +402,6 @@ func newReaderCtx(ctx context.Context, r io.Reader) io.Reader {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// heuristicsCopyFileLog returns the contents of the given logFile
 | 
					 | 
				
			||||||
func heuristicsCopyFileLog(ctx context.Context, w io.Writer, logFile string) error {
 | 
					 | 
				
			||||||
	fInfo, err := os.Stat(logFile)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// This is to account for the heuristics where logs for service foo
 | 
					 | 
				
			||||||
	// could be in /var/log/foo/
 | 
					 | 
				
			||||||
	if fInfo.IsDir() {
 | 
					 | 
				
			||||||
		return os.ErrNotExist
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	f, err := os.Open(logFile)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	defer f.Close()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if _, err := io.Copy(w, newReaderCtx(ctx, f)); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return nil
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func safeServiceName(s string) error {
 | 
					func safeServiceName(s string) error {
 | 
				
			||||||
	// Max length of a service name is 256 across supported OSes
 | 
						// Max length of a service name is 256 across supported OSes
 | 
				
			||||||
	if len(s) > maxServiceLength {
 | 
						if len(s) > maxServiceLength {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,8 +21,13 @@ package kubelet
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						securejoin "github.com/cyphar/filepath-securejoin"
 | 
				
			||||||
 | 
						unix "golang.org/x/sys/unix"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// getLoggingCmd returns the journalctl cmd and arguments for the given nodeLogQuery and boot. Note that
 | 
					// getLoggingCmd returns the journalctl cmd and arguments for the given nodeLogQuery and boot. Note that
 | 
				
			||||||
@@ -72,3 +77,32 @@ func checkForNativeLogger(ctx context.Context, service string) bool {
 | 
				
			|||||||
	// hence we search for it in the list of services known to journalctl
 | 
						// hence we search for it in the list of services known to journalctl
 | 
				
			||||||
	return strings.Contains(string(output), service+".service")
 | 
						return strings.Contains(string(output), service+".service")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// heuristicsCopyFileLog returns the contents of the given logFile
 | 
				
			||||||
 | 
					func heuristicsCopyFileLog(ctx context.Context, w io.Writer, logDir, logFileName string) error {
 | 
				
			||||||
 | 
						f, err := securejoin.OpenInRoot(logDir, logFileName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Ignoring errors when closing a file opened read-only doesn't cause data loss
 | 
				
			||||||
 | 
						defer func() { _ = f.Close() }()
 | 
				
			||||||
 | 
						fInfo, err := f.Stat()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// This is to account for the heuristics where logs for service foo
 | 
				
			||||||
 | 
						// could be in /var/log/foo/
 | 
				
			||||||
 | 
						if fInfo.IsDir() {
 | 
				
			||||||
 | 
							return os.ErrNotExist
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rf, err := securejoin.Reopen(f, unix.O_RDONLY)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer func() { _ = rf.Close() }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := io.Copy(w, newReaderCtx(ctx, rf)); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										56
									
								
								pkg/kubelet/kubelet_server_journal_nonlinux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								pkg/kubelet/kubelet_server_journal_nonlinux.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					//go:build !linux
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2024 The Kubernetes Authors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					You may obtain a copy of the License at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					limitations under the License.
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package kubelet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						securejoin "github.com/cyphar/filepath-securejoin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// heuristicsCopyFileLog returns the contents of the given logFile
 | 
				
			||||||
 | 
					func heuristicsCopyFileLog(ctx context.Context, w io.Writer, logDir, logFileName string) error {
 | 
				
			||||||
 | 
						logFile, err := securejoin.SecureJoin(logDir, logFileName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fInfo, err := os.Stat(logFile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// This is to account for the heuristics where logs for service foo
 | 
				
			||||||
 | 
						// could be in /var/log/foo/
 | 
				
			||||||
 | 
						if fInfo.IsDir() {
 | 
				
			||||||
 | 
							return os.ErrNotExist
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						f, err := os.Open(logFile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Ignoring errors when closing a file opened read-only doesn't cause data loss
 | 
				
			||||||
 | 
						defer func() { _ = f.Close() }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if _, err := io.Copy(w, newReaderCtx(ctx, f)); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -24,11 +24,11 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// getLoggingCmd on unsupported operating systems returns the echo command and a warning message (as strings)
 | 
					// getLoggingCmd on unsupported operating systems returns the echo command and a warning message (as strings)
 | 
				
			||||||
func getLoggingCmd(n *nodeLogQuery, services []string) (string, []string, error) {
 | 
					func getLoggingCmd(_ *nodeLogQuery, _ []string) (string, []string, error) {
 | 
				
			||||||
	return "", []string{}, errors.New("Operating System Not Supported")
 | 
						return "", []string{}, errors.New("Operating System Not Supported")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// checkForNativeLogger on unsupported operating systems returns false
 | 
					// checkForNativeLogger on unsupported operating systems returns false
 | 
				
			||||||
func checkForNativeLogger(ctx context.Context, service string) bool {
 | 
					func checkForNativeLogger(_ context.Context, _ string) bool {
 | 
				
			||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										178
									
								
								vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								vendor/github.com/cyphar/filepath-securejoin/CHANGELOG.md
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
				
			|||||||
 | 
					# Changelog #
 | 
				
			||||||
 | 
					All notable changes to this project will be documented in this file.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The format is based on [Keep a Changelog](http://keepachangelog.com/)
 | 
				
			||||||
 | 
					and this project adheres to [Semantic Versioning](http://semver.org/).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [Unreleased] ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.3.4] - 2024-10-09 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed ###
 | 
				
			||||||
 | 
					- Previously, some testing mocks we had resulted in us doing `import "testing"`
 | 
				
			||||||
 | 
					  in non-`_test.go` code, which made some downstreams like Kubernetes unhappy.
 | 
				
			||||||
 | 
					  This has been fixed. (#32)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.3.3] - 2024-09-30 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed ###
 | 
				
			||||||
 | 
					- The mode and owner verification logic in `MkdirAll` has been removed. This
 | 
				
			||||||
 | 
					  was originally intended to protect against some theoretical attacks but upon
 | 
				
			||||||
 | 
					  further consideration these protections don't actually buy us anything and
 | 
				
			||||||
 | 
					  they were causing spurious errors with more complicated filesystem setups.
 | 
				
			||||||
 | 
					- The "is the created directory empty" logic in `MkdirAll` has also been
 | 
				
			||||||
 | 
					  removed. This was not causing us issues yet, but some pseudofilesystems (such
 | 
				
			||||||
 | 
					  as `cgroup`) create non-empty directories and so this logic would've been
 | 
				
			||||||
 | 
					  wrong for such cases.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.3.2] - 2024-09-13 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed ###
 | 
				
			||||||
 | 
					- Passing the `S_ISUID` or `S_ISGID` modes to `MkdirAllInRoot` will now return
 | 
				
			||||||
 | 
					  an explicit error saying that those bits are ignored by `mkdirat(2)`. In the
 | 
				
			||||||
 | 
					  past a different error was returned, but since the silent ignoring behaviour
 | 
				
			||||||
 | 
					  is codified in the man pages a more explicit error seems apt. While silently
 | 
				
			||||||
 | 
					  ignoring these bits would be the most compatible option, it could lead to
 | 
				
			||||||
 | 
					  users thinking their code sets these bits when it doesn't. Programs that need
 | 
				
			||||||
 | 
					  to deal with compatibility can mask the bits themselves. (#23, #25)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed ###
 | 
				
			||||||
 | 
					- If a directory has `S_ISGID` set, then all child directories will have
 | 
				
			||||||
 | 
					  `S_ISGID` set when created and a different gid will be used for any inode
 | 
				
			||||||
 | 
					  created under the directory. Previously, the "expected owner and mode"
 | 
				
			||||||
 | 
					  validation in `securejoin.MkdirAll` did not correctly handle this. We now
 | 
				
			||||||
 | 
					  correctly handle this case. (#24, #25)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.3.1] - 2024-07-23 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed ###
 | 
				
			||||||
 | 
					- By allowing `Open(at)InRoot` to opt-out of the extra work done by `MkdirAll`
 | 
				
			||||||
 | 
					  to do the necessary "partial lookups", `Open(at)InRoot` now does less work
 | 
				
			||||||
 | 
					  for both implementations (resulting in a many-fold decrease in the number of
 | 
				
			||||||
 | 
					  operations for `openat2`, and a modest improvement for non-`openat2`) and is
 | 
				
			||||||
 | 
					  far more guaranteed to match the correct `openat2(RESOLVE_IN_ROOT)`
 | 
				
			||||||
 | 
					  behaviour.
 | 
				
			||||||
 | 
					- We now use `readlinkat(fd, "")` where possible. For `Open(at)InRoot` this
 | 
				
			||||||
 | 
					  effectively just means that we no longer risk getting spurious errors during
 | 
				
			||||||
 | 
					  rename races. However, for our hardened procfs handler, this in theory should
 | 
				
			||||||
 | 
					  prevent mount attacks from tricking us when doing magic-link readlinks (even
 | 
				
			||||||
 | 
					  when using the unsafe host `/proc` handle). Unfortunately `Reopen` is still
 | 
				
			||||||
 | 
					  potentially vulnerable to those kinds of somewhat-esoteric attacks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Technically this [will only work on post-2.6.39 kernels][linux-readlinkat-emptypath]
 | 
				
			||||||
 | 
					  but it seems incredibly unlikely anyone is using `filepath-securejoin` on a
 | 
				
			||||||
 | 
					  pre-2011 kernel.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed ###
 | 
				
			||||||
 | 
					- Several improvements were made to the errors returned by `Open(at)InRoot` and
 | 
				
			||||||
 | 
					  `MkdirAll` when dealing with invalid paths under the emulated (ie.
 | 
				
			||||||
 | 
					  non-`openat2`) implementation. Previously, some paths would return the wrong
 | 
				
			||||||
 | 
					  error (`ENOENT` when the last component was a non-directory), and other paths
 | 
				
			||||||
 | 
					  would be returned as though they were acceptable (trailing-slash components
 | 
				
			||||||
 | 
					  after a non-directory would be ignored by `Open(at)InRoot`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  These changes were done to match `openat2`'s behaviour and purely is a
 | 
				
			||||||
 | 
					  consistency fix (most users are going to be using `openat2` anyway).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[linux-readlinkat-emptypath]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=65cfc6722361570bfe255698d9cd4dccaf47570d
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.3.0] - 2024-07-11 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Added ###
 | 
				
			||||||
 | 
					- A new set of `*os.File`-based APIs have been added. These are adapted from
 | 
				
			||||||
 | 
					  [libpathrs][] and we strongly suggest using them if possible (as they provide
 | 
				
			||||||
 | 
					  far more protection against attacks than `SecureJoin`):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   - `Open(at)InRoot` resolves a path inside a rootfs and returns an `*os.File`
 | 
				
			||||||
 | 
					     handle to the path. Note that the handle returned is an `O_PATH` handle,
 | 
				
			||||||
 | 
					     which cannot be used for reading or writing (as well as some other
 | 
				
			||||||
 | 
					     operations -- [see open(2) for more details][open.2])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   - `Reopen` takes an `O_PATH` file handle and safely re-opens it to upgrade
 | 
				
			||||||
 | 
					     it to a regular handle. This can also be used with non-`O_PATH` handles,
 | 
				
			||||||
 | 
					     but `O_PATH` is the most obvious application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   - `MkdirAll` is an implementation of `os.MkdirAll` that is safe to use to
 | 
				
			||||||
 | 
					     create a directory tree within a rootfs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  As these are new APIs, they may change in the future. However, they should be
 | 
				
			||||||
 | 
					  safe to start migrating to as we have extensive tests ensuring they behave
 | 
				
			||||||
 | 
					  correctly and are safe against various races and other attacks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[libpathrs]: https://github.com/openSUSE/libpathrs
 | 
				
			||||||
 | 
					[open.2]: https://www.man7.org/linux/man-pages/man2/open.2.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.5] - 2024-05-03 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed ###
 | 
				
			||||||
 | 
					- Some minor changes were made to how lexical components (like `..` and `.`)
 | 
				
			||||||
 | 
					  are handled during path generation in `SecureJoin`. There is no behaviour
 | 
				
			||||||
 | 
					  change as a result of this fix (the resulting paths are the same).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed ###
 | 
				
			||||||
 | 
					- The error returned when we hit a symlink loop now references the correct
 | 
				
			||||||
 | 
					  path. (#10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.4] - 2023-09-06 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Security ###
 | 
				
			||||||
 | 
					- This release fixes a potential security issue in filepath-securejoin when
 | 
				
			||||||
 | 
					  used on Windows ([GHSA-6xv5-86q9-7xr8][], which could be used to generate
 | 
				
			||||||
 | 
					  paths outside of the provided rootfs in certain cases), as well as improving
 | 
				
			||||||
 | 
					  the overall behaviour of filepath-securejoin when dealing with Windows paths
 | 
				
			||||||
 | 
					  that contain volume names. Thanks to Paulo Gomes for discovering and fixing
 | 
				
			||||||
 | 
					  these issues.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed ###
 | 
				
			||||||
 | 
					- Switch to GitHub Actions for CI so we can test on Windows as well as Linux
 | 
				
			||||||
 | 
					  and MacOS.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[GHSA-6xv5-86q9-7xr8]: https://github.com/advisories/GHSA-6xv5-86q9-7xr8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.3] - 2021-06-04 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed ###
 | 
				
			||||||
 | 
					- Switch to Go 1.13-style `%w` error wrapping, letting us drop the dependency
 | 
				
			||||||
 | 
					  on `github.com/pkg/errors`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.2] - 2018-09-05 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed ###
 | 
				
			||||||
 | 
					- Use `syscall.ELOOP` as the base error for symlink loops, rather than our own
 | 
				
			||||||
 | 
					  (internal) error. This allows callers to more easily use `errors.Is` to check
 | 
				
			||||||
 | 
					  for this case.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.1] - 2018-09-05 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed ###
 | 
				
			||||||
 | 
					- Use our own `IsNotExist` implementation, which lets us handle `ENOTDIR`
 | 
				
			||||||
 | 
					  properly within `SecureJoin`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [0.2.0] - 2017-07-19 ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					We now have 100% test coverage!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Added ###
 | 
				
			||||||
 | 
					- Add a `SecureJoinVFS` API that can be used for mocking (as we do in our new
 | 
				
			||||||
 | 
					  tests) or for implementing custom handling of lookup operations (such as for
 | 
				
			||||||
 | 
					  rootless containers, where work is necessary to access directories with weird
 | 
				
			||||||
 | 
					  modes because we don't have `CAP_DAC_READ_SEARCH` or `CAP_DAC_OVERRIDE`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 0.1.0 - 2017-07-19
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is our first release of `github.com/cyphar/filepath-securejoin`,
 | 
				
			||||||
 | 
					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).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Unreleased]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.4...HEAD
 | 
				
			||||||
 | 
					[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.3...v0.3.4
 | 
				
			||||||
 | 
					[0.3.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.2...v0.3.3
 | 
				
			||||||
 | 
					[0.3.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.1...v0.3.2
 | 
				
			||||||
 | 
					[0.3.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.3.0...v0.3.1
 | 
				
			||||||
 | 
					[0.3.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.5...v0.3.0
 | 
				
			||||||
 | 
					[0.2.5]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.4...v0.2.5
 | 
				
			||||||
 | 
					[0.2.4]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.3...v0.2.4
 | 
				
			||||||
 | 
					[0.2.3]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.2...v0.2.3
 | 
				
			||||||
 | 
					[0.2.2]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.1...v0.2.2
 | 
				
			||||||
 | 
					[0.2.1]: https://github.com/cyphar/filepath-securejoin/compare/v0.2.0...v0.2.1
 | 
				
			||||||
 | 
					[0.2.0]: https://github.com/cyphar/filepath-securejoin/compare/v0.1.0...v0.2.0
 | 
				
			||||||
							
								
								
									
										2
									
								
								vendor/github.com/cyphar/filepath-securejoin/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/cyphar/filepath-securejoin/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,5 +1,5 @@
 | 
				
			|||||||
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 SUSE LLC. All rights reserved.
 | 
					Copyright (C) 2017-2024 SUSE LLC. All rights reserved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Redistribution and use in source and binary forms, with or without
 | 
					Redistribution and use in source and binary forms, with or without
 | 
				
			||||||
modification, are permitted provided that the following conditions are
 | 
					modification, are permitted provided that the following conditions are
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										136
									
								
								vendor/github.com/cyphar/filepath-securejoin/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										136
									
								
								vendor/github.com/cyphar/filepath-securejoin/README.md
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,32 +1,26 @@
 | 
				
			|||||||
## `filepath-securejoin` ##
 | 
					## `filepath-securejoin` ##
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](https://pkg.go.dev/github.com/cyphar/filepath-securejoin)
 | 
				
			||||||
[](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
 | 
					[](https://github.com/cyphar/filepath-securejoin/actions/workflows/ci.yml)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
An implementation of `SecureJoin`, a [candidate for inclusion in the Go
 | 
					### Old API ###
 | 
				
			||||||
standard library][go#20126]. The purpose of this function is to be a "secure"
 | 
					 | 
				
			||||||
alternative to `filepath.Join`, and in particular it provides certain
 | 
					 | 
				
			||||||
guarantees that are not provided by `filepath.Join`.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
> **NOTE**: This code is *only* safe if you are not at risk of other processes
 | 
					This library was originally just an implementation of `SecureJoin` which was
 | 
				
			||||||
> modifying path components after you've used `SecureJoin`. If it is possible
 | 
					[intended to be included in the Go standard library][go#20126] as a safer
 | 
				
			||||||
> for a malicious process to modify path components of the resolved path, then
 | 
					`filepath.Join` that would restrict the path lookup to be inside a root
 | 
				
			||||||
> you will be vulnerable to some fairly trivial TOCTOU race conditions. [There
 | 
					directory.
 | 
				
			||||||
> are some Linux kernel patches I'm working on which might allow for a better
 | 
					 | 
				
			||||||
> solution.][lwn-obeneath]
 | 
					 | 
				
			||||||
>
 | 
					 | 
				
			||||||
> In addition, with a slightly modified API it might be possible to use
 | 
					 | 
				
			||||||
> `O_PATH` and verify that the opened path is actually the resolved one -- but
 | 
					 | 
				
			||||||
> I have not done that yet. I might add it in the future as a helper function
 | 
					 | 
				
			||||||
> to help users verify the path (we can't just return `/proc/self/fd/<foo>`
 | 
					 | 
				
			||||||
> because that doesn't always work transparently for all users).
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
This is the function prototype:
 | 
					The implementation was based on code that existed in several container
 | 
				
			||||||
 | 
					runtimes. Unfortunately, this API is **fundamentally unsafe** against attackers
 | 
				
			||||||
 | 
					that can modify path components after `SecureJoin` returns and before the
 | 
				
			||||||
 | 
					caller uses the path, allowing for some fairly trivial TOCTOU attacks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```go
 | 
					`SecureJoin` (and `SecureJoinVFS`) are still provided by this library to
 | 
				
			||||||
func SecureJoin(root, unsafePath string) (string, error)
 | 
					support legacy users, but new users are strongly suggested to avoid using
 | 
				
			||||||
```
 | 
					`SecureJoin` and instead use the [new api](#new-api) or switch to
 | 
				
			||||||
 | 
					[libpathrs][libpathrs].
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This library **guarantees** the following:
 | 
					With the above limitations in mind, this library guarantees the following:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* If no error is set, the resulting string **must** be a child path of
 | 
					* If no error is set, the resulting string **must** be a child path of
 | 
				
			||||||
  `root` and will not contain any symlink path components (they will all be
 | 
					  `root` and will not contain any symlink path components (they will all be
 | 
				
			||||||
@@ -47,7 +41,7 @@ This library **guarantees** the following:
 | 
				
			|||||||
A (trivial) implementation of this function on GNU/Linux systems could be done
 | 
					A (trivial) implementation of this function on GNU/Linux systems could be done
 | 
				
			||||||
with the following (note that this requires root privileges and is far more
 | 
					with the following (note that this requires root privileges and is far more
 | 
				
			||||||
opaque than the implementation in this library, and also requires that
 | 
					opaque than the implementation in this library, and also requires that
 | 
				
			||||||
`readlink` is inside the `root` path):
 | 
					`readlink` is inside the `root` path and is trustworthy):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```go
 | 
					```go
 | 
				
			||||||
package securejoin
 | 
					package securejoin
 | 
				
			||||||
@@ -70,9 +64,105 @@ func SecureJoin(root, unsafePath string) (string, error) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[lwn-obeneath]: https://lwn.net/Articles/767547/
 | 
					[libpathrs]: https://github.com/openSUSE/libpathrs
 | 
				
			||||||
[go#20126]: https://github.com/golang/go/issues/20126
 | 
					[go#20126]: https://github.com/golang/go/issues/20126
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### New API ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					library to ease the transition. These APIs are only supported on Linux.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					These APIs are implemented such that `filepath-securejoin` will
 | 
				
			||||||
 | 
					opportunistically use certain newer kernel APIs that make these operations far
 | 
				
			||||||
 | 
					more secure. In particular:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* All of the lookup operations will use [`openat2`][openat2.2] on new enough
 | 
				
			||||||
 | 
					  kernels (Linux 5.6 or later) to restrict lookups through magic-links and
 | 
				
			||||||
 | 
					  bind-mounts (for certain operations) and to make use of `RESOLVE_IN_ROOT` to
 | 
				
			||||||
 | 
					  efficiently resolve symlinks within a rootfs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* The APIs provide hardening against a malicious `/proc` mount to either detect
 | 
				
			||||||
 | 
					  or avoid being tricked by a `/proc` that is not legitimate. This is done
 | 
				
			||||||
 | 
					  using [`openat2`][openat2.2] for all users, and privileged users will also be
 | 
				
			||||||
 | 
					  further protected by using [`fsopen`][fsopen.2] and [`open_tree`][open_tree.2]
 | 
				
			||||||
 | 
					  (Linux 5.2 or later).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html
 | 
				
			||||||
 | 
					[fsopen.2]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md
 | 
				
			||||||
 | 
					[open_tree.2]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### `OpenInRoot` ####
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```go
 | 
				
			||||||
 | 
					func OpenInRoot(root, unsafePath string) (*os.File, error)
 | 
				
			||||||
 | 
					func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error)
 | 
				
			||||||
 | 
					func Reopen(handle *os.File, flags int) (*os.File, error)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`OpenInRoot` is a much safer version of
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```go
 | 
				
			||||||
 | 
					path, err := securejoin.SecureJoin(root, unsafePath)
 | 
				
			||||||
 | 
					file, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					that protects against various race attacks that could lead to serious security
 | 
				
			||||||
 | 
					issues, depending on the application. Note that the returned `*os.File` is an
 | 
				
			||||||
 | 
					`O_PATH` file descriptor, which is quite restricted. Callers will probably need
 | 
				
			||||||
 | 
					to use `Reopen` to get a more usable handle (this split is done to provide
 | 
				
			||||||
 | 
					useful features like PTY spawning and to avoid users accidentally opening bad
 | 
				
			||||||
 | 
					inodes that could cause a DoS).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Callers need to be careful in how they use the returned `*os.File`. Usually it
 | 
				
			||||||
 | 
					is only safe to operate on the handle directly, and it is very easy to create a
 | 
				
			||||||
 | 
					security issue. [libpathrs][libpathrs] provides far more helpers to make using
 | 
				
			||||||
 | 
					these handles safer -- there is currently no plan to port them to
 | 
				
			||||||
 | 
					`filepath-securejoin`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`OpenatInRoot` is like `OpenInRoot` except that the root is provided using an
 | 
				
			||||||
 | 
					`*os.File`. This allows you to ensure that multiple `OpenatInRoot` (or
 | 
				
			||||||
 | 
					`MkdirAllHandle`) calls are operating on the same rootfs.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> **NOTE**: Unlike `SecureJoin`, `OpenInRoot` will error out as soon as it hits
 | 
				
			||||||
 | 
					> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
 | 
				
			||||||
 | 
					> which treated non-existent components as though they were real directories,
 | 
				
			||||||
 | 
					> and would allow for partial resolution of dangling symlinks. These behaviours
 | 
				
			||||||
 | 
					> are at odds with how Linux treats non-existent paths and dangling symlinks,
 | 
				
			||||||
 | 
					> and so these are no longer allowed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### `MkdirAll` ####
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```go
 | 
				
			||||||
 | 
					func MkdirAll(root, unsafePath string, mode int) error
 | 
				
			||||||
 | 
					func MkdirAllHandle(root *os.File, unsafePath string, mode int) (*os.File, error)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`MkdirAll` is a much safer version of
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```go
 | 
				
			||||||
 | 
					path, err := securejoin.SecureJoin(root, unsafePath)
 | 
				
			||||||
 | 
					err = os.MkdirAll(path, mode)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					that protects against the same kinds of races that `OpenInRoot` protects
 | 
				
			||||||
 | 
					against.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`MkdirAllHandle` is like `MkdirAll` except that the root is provided using an
 | 
				
			||||||
 | 
					`*os.File` (the reason for this is the same as with `OpenatInRoot`) and an
 | 
				
			||||||
 | 
					`*os.File` of the final created directory is returned (this directory is
 | 
				
			||||||
 | 
					guaranteed to be effectively identical to the directory created by
 | 
				
			||||||
 | 
					`MkdirAllHandle`, which is not possible to ensure by just using `OpenatInRoot`
 | 
				
			||||||
 | 
					after `MkdirAll`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> **NOTE**: Unlike `SecureJoin`, `MkdirAll` will error out as soon as it hits
 | 
				
			||||||
 | 
					> a dangling symlink or non-existent path. This is in contrast to `SecureJoin`
 | 
				
			||||||
 | 
					> which treated non-existent components as though they were real directories,
 | 
				
			||||||
 | 
					> and would allow for partial resolution of dangling symlinks. These behaviours
 | 
				
			||||||
 | 
					> are at odds with how Linux treats non-existent paths and dangling symlinks,
 | 
				
			||||||
 | 
					> and so these are no longer allowed. This means that `MkdirAll` will not
 | 
				
			||||||
 | 
					> create non-existent directories referenced by a dangling symlink.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### License ###
 | 
					### License ###
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The license of this project is the same as Go, which is a BSD 3-clause license
 | 
					The license of this project is the same as Go, which is a BSD 3-clause license
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								vendor/github.com/cyphar/filepath-securejoin/VERSION
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/cyphar/filepath-securejoin/VERSION
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1 +1 @@
 | 
				
			|||||||
0.2.4
 | 
					0.3.4
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								vendor/github.com/cyphar/filepath-securejoin/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								vendor/github.com/cyphar/filepath-securejoin/doc.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					// Copyright (C) 2014-2015 Docker Inc & Go Authors. All rights reserved.
 | 
				
			||||||
 | 
					// Copyright (C) 2017-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 implements a set of helpers to make it easier to write Go
 | 
				
			||||||
 | 
					// code that is safe against symlink-related escape attacks. The primary idea
 | 
				
			||||||
 | 
					// is to let you resolve a path within a rootfs directory as if the rootfs was
 | 
				
			||||||
 | 
					// a chroot.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// securejoin has two APIs, a "legacy" API and a "modern" API.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The legacy API is [SecureJoin] and [SecureJoinVFS]. These methods are
 | 
				
			||||||
 | 
					// **not** safe against race conditions where an attacker changes the
 | 
				
			||||||
 | 
					// filesystem after (or during) the [SecureJoin] operation.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// The new API is made up of [OpenInRoot] and [MkdirAll] (and derived
 | 
				
			||||||
 | 
					// functions). These are safe against racing attackers and have several other
 | 
				
			||||||
 | 
					// protections that are not provided by the legacy API. There are many more
 | 
				
			||||||
 | 
					// operations that most programs expect to be able to do safely, but we do not
 | 
				
			||||||
 | 
					// provide explicit support for them because we want to encourage users to
 | 
				
			||||||
 | 
					// switch to [libpathrs](https://github.com/openSUSE/libpathrs) which is a
 | 
				
			||||||
 | 
					// cross-language next-generation library that is entirely designed around
 | 
				
			||||||
 | 
					// operating on paths safely.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// securejoin has been used by several container runtimes (Docker, runc,
 | 
				
			||||||
 | 
					// Kubernetes, etc) for quite a few years as a de-facto standard for operating
 | 
				
			||||||
 | 
					// on container filesystem paths "safely". However, most users still use the
 | 
				
			||||||
 | 
					// legacy API which is unsafe against various attacks (there is a fairly long
 | 
				
			||||||
 | 
					// history of CVEs in dependent as a result). Users should switch to the modern
 | 
				
			||||||
 | 
					// API as soon as possible (or even better, switch to libpathrs).
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// This project was initially intended to be included in the Go standard
 | 
				
			||||||
 | 
					// library, but [it was rejected](https://go.dev/issue/20126). There is now a
 | 
				
			||||||
 | 
					// [new Go proposal](https://go.dev/issue/67002) for a safe path resolution API
 | 
				
			||||||
 | 
					// that shares some of the goals of filepath-securejoin. However, that design
 | 
				
			||||||
 | 
					// is intended to work like `openat2(RESOLVE_BENEATH)` which does not fit the
 | 
				
			||||||
 | 
					// usecase of container runtimes and most system tools.
 | 
				
			||||||
 | 
					package securejoin
 | 
				
			||||||
							
								
								
									
										104
									
								
								vendor/github.com/cyphar/filepath-securejoin/join.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										104
									
								
								vendor/github.com/cyphar/filepath-securejoin/join.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,17 +1,11 @@
 | 
				
			|||||||
// 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 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Package securejoin is an implementation of the hopefully-soon-to-be-included
 | 
					 | 
				
			||||||
// SecureJoin helper that is meant to be part of the "path/filepath" package.
 | 
					 | 
				
			||||||
// The purpose of this project is to provide a PoC implementation to make the
 | 
					 | 
				
			||||||
// SecureJoin proposal (https://github.com/golang/go/issues/20126) more
 | 
					 | 
				
			||||||
// tangible.
 | 
					 | 
				
			||||||
package securejoin
 | 
					package securejoin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
@@ -19,26 +13,34 @@ import (
 | 
				
			|||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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].
 | 
				
			||||||
func IsNotExist(err error) bool {
 | 
					func IsNotExist(err error) bool {
 | 
				
			||||||
	// Check that it's not actually an ENOTDIR, which in some cases is a more
 | 
						// Check that it's not actually an ENOTDIR, which in some cases is a more
 | 
				
			||||||
	// convoluted case of ENOENT (usually involving weird paths).
 | 
						// convoluted case of ENOENT (usually involving weird paths).
 | 
				
			||||||
	return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
 | 
						return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR) || errors.Is(err, syscall.ENOENT)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SecureJoinVFS joins the two given path components (similar to Join) except
 | 
					// SecureJoinVFS joins the two given path components (similar to [filepath.Join]) except
 | 
				
			||||||
// that the returned path is guaranteed to be scoped inside the provided root
 | 
					// that the returned path is guaranteed to be scoped inside the provided root
 | 
				
			||||||
// path (when evaluated). Any symbolic links in the path are evaluated with the
 | 
					// path (when evaluated). Any symbolic links in the path are evaluated with the
 | 
				
			||||||
// given root treated as the root of the filesystem, similar to a chroot. The
 | 
					// given root treated as the root of the filesystem, similar to a chroot. The
 | 
				
			||||||
// filesystem state is evaluated through the given VFS interface (if nil, the
 | 
					// filesystem state is evaluated through the given [VFS] interface (if nil, the
 | 
				
			||||||
// standard os.* family of functions are used).
 | 
					// standard [os].* family of functions 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
 | 
				
			||||||
// replaced with symlinks on the filesystem) after this function has returned.
 | 
					// replaced with symlinks on the filesystem) after this function has returned.
 | 
				
			||||||
// Such a symlink race is necessarily out-of-scope of SecureJoin.
 | 
					// Such a symlink race is necessarily out-of-scope of SecureJoinVFS.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// NOTE: Due to the above limitation, Linux users are strongly encouraged to
 | 
				
			||||||
 | 
					// use [OpenInRoot] instead, which does safely protect against these kinds of
 | 
				
			||||||
 | 
					// attacks. There is no way to solve this problem with SecureJoinVFS because
 | 
				
			||||||
 | 
					// the API is fundamentally wrong (you cannot return a "safe" path string and
 | 
				
			||||||
 | 
					// guarantee it won't be modified afterwards).
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Volume names in unsafePath are always discarded, regardless if they are
 | 
					// Volume names in unsafePath are always discarded, regardless if they are
 | 
				
			||||||
// provided via direct input or when evaluating symlinks. Therefore:
 | 
					// provided via direct input or when evaluating symlinks. Therefore:
 | 
				
			||||||
@@ -51,75 +53,73 @@ func SecureJoinVFS(root, unsafePath string, vfs VFS) (string, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unsafePath = filepath.FromSlash(unsafePath)
 | 
						unsafePath = filepath.FromSlash(unsafePath)
 | 
				
			||||||
	var path bytes.Buffer
 | 
						var (
 | 
				
			||||||
	n := 0
 | 
							currentPath   string
 | 
				
			||||||
	for unsafePath != "" {
 | 
							remainingPath = unsafePath
 | 
				
			||||||
		if n > 255 {
 | 
							linksWalked   int
 | 
				
			||||||
			return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
 | 
						)
 | 
				
			||||||
 | 
						for remainingPath != "" {
 | 
				
			||||||
 | 
							if v := filepath.VolumeName(remainingPath); v != "" {
 | 
				
			||||||
 | 
								remainingPath = remainingPath[len(v):]
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if v := filepath.VolumeName(unsafePath); v != "" {
 | 
							// Get the next path component.
 | 
				
			||||||
			unsafePath = unsafePath[len(v):]
 | 
							var part string
 | 
				
			||||||
		}
 | 
							if i := strings.IndexRune(remainingPath, filepath.Separator); i == -1 {
 | 
				
			||||||
 | 
								part, remainingPath = remainingPath, ""
 | 
				
			||||||
		// Next path component, p.
 | 
					 | 
				
			||||||
		i := strings.IndexRune(unsafePath, filepath.Separator)
 | 
					 | 
				
			||||||
		var p string
 | 
					 | 
				
			||||||
		if i == -1 {
 | 
					 | 
				
			||||||
			p, unsafePath = unsafePath, ""
 | 
					 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			p, unsafePath = unsafePath[:i], unsafePath[i+1:]
 | 
								part, remainingPath = remainingPath[:i], remainingPath[i+1:]
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create a cleaned path, using the lexical semantics of /../a, to
 | 
							// Apply the component lexically to the path we are building.
 | 
				
			||||||
		// create a "scoped" path component which can safely be joined to fullP
 | 
							// currentPath does not contain any symlinks, and we are lexically
 | 
				
			||||||
		// for evaluation. At this point, path.String() doesn't contain any
 | 
							// dealing with a single component, so it's okay to do a filepath.Clean
 | 
				
			||||||
		// symlink components.
 | 
							// here.
 | 
				
			||||||
		cleanP := filepath.Clean(string(filepath.Separator) + path.String() + p)
 | 
							nextPath := filepath.Join(string(filepath.Separator), currentPath, part)
 | 
				
			||||||
		if cleanP == string(filepath.Separator) {
 | 
							if nextPath == string(filepath.Separator) {
 | 
				
			||||||
			path.Reset()
 | 
								currentPath = ""
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		fullP := filepath.Clean(root + cleanP)
 | 
							fullPath := root + string(filepath.Separator) + nextPath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Figure out whether the path is a symlink.
 | 
							// Figure out whether the path is a symlink.
 | 
				
			||||||
		fi, err := vfs.Lstat(fullP)
 | 
							fi, err := vfs.Lstat(fullPath)
 | 
				
			||||||
		if err != nil && !IsNotExist(err) {
 | 
							if err != nil && !IsNotExist(err) {
 | 
				
			||||||
			return "", err
 | 
								return "", err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// Treat non-existent path components the same as non-symlinks (we
 | 
							// Treat non-existent path components the same as non-symlinks (we
 | 
				
			||||||
		// can't do any better here).
 | 
							// can't do any better here).
 | 
				
			||||||
		if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 {
 | 
							if IsNotExist(err) || fi.Mode()&os.ModeSymlink == 0 {
 | 
				
			||||||
			path.WriteString(p)
 | 
								currentPath = nextPath
 | 
				
			||||||
			path.WriteRune(filepath.Separator)
 | 
					 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Only increment when we actually dereference a link.
 | 
							// It's a symlink, so get its contents and expand it by prepending it
 | 
				
			||||||
		n++
 | 
							// to the yet-unparsed path.
 | 
				
			||||||
 | 
							linksWalked++
 | 
				
			||||||
 | 
							if linksWalked > maxSymlinkLimit {
 | 
				
			||||||
 | 
								return "", &os.PathError{Op: "SecureJoin", Path: root + string(filepath.Separator) + unsafePath, Err: syscall.ELOOP}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// It's a symlink, expand it by prepending it to the yet-unparsed path.
 | 
							dest, err := vfs.Readlink(fullPath)
 | 
				
			||||||
		dest, err := vfs.Readlink(fullP)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return "", err
 | 
								return "", err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							remainingPath = dest + string(filepath.Separator) + remainingPath
 | 
				
			||||||
		// Absolute symlinks reset any work we've already done.
 | 
							// Absolute symlinks reset any work we've already done.
 | 
				
			||||||
		if filepath.IsAbs(dest) {
 | 
							if filepath.IsAbs(dest) {
 | 
				
			||||||
			path.Reset()
 | 
								currentPath = ""
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		unsafePath = dest + string(filepath.Separator) + unsafePath
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// We have to clean path.String() here because it may contain '..'
 | 
						// There should be no lexical components like ".." left in the path here,
 | 
				
			||||||
	// components that are entirely lexical, but would be misleading otherwise.
 | 
						// but for safety clean up the path before joining it to the root.
 | 
				
			||||||
	// And finally do a final clean to ensure that root is also lexically
 | 
						finalPath := filepath.Join(string(filepath.Separator), currentPath)
 | 
				
			||||||
	// clean.
 | 
						return filepath.Join(root, finalPath), nil
 | 
				
			||||||
	fullP := filepath.Clean(string(filepath.Separator) + path.String())
 | 
					 | 
				
			||||||
	return filepath.Clean(root + fullP), nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SecureJoin is a wrapper around SecureJoinVFS that just uses the os.* library
 | 
					// SecureJoin is a wrapper around [SecureJoinVFS] that just uses the [os].* library
 | 
				
			||||||
// of functions as the VFS. If in doubt, use this function over SecureJoinVFS.
 | 
					// of functions as the [VFS]. If in doubt, use this function over [SecureJoinVFS].
 | 
				
			||||||
func SecureJoin(root, unsafePath string) (string, error) {
 | 
					func SecureJoin(root, unsafePath string) (string, error) {
 | 
				
			||||||
	return SecureJoinVFS(root, unsafePath, nil)
 | 
						return SecureJoinVFS(root, unsafePath, nil)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										389
									
								
								vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										389
									
								
								vendor/github.com/cyphar/filepath-securejoin/lookup_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,389 @@
 | 
				
			|||||||
 | 
					//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"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/sys/unix"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type symlinkStackEntry struct {
 | 
				
			||||||
 | 
						// (dir, remainingPath) is what we would've returned if the link didn't
 | 
				
			||||||
 | 
						// exist. This matches what openat2(RESOLVE_IN_ROOT) would return in
 | 
				
			||||||
 | 
						// this case.
 | 
				
			||||||
 | 
						dir           *os.File
 | 
				
			||||||
 | 
						remainingPath string
 | 
				
			||||||
 | 
						// linkUnwalked is the remaining path components from the original
 | 
				
			||||||
 | 
						// Readlink which we have yet to walk. When this slice is empty, we
 | 
				
			||||||
 | 
						// drop the link from the stack.
 | 
				
			||||||
 | 
						linkUnwalked []string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (se symlinkStackEntry) String() string {
 | 
				
			||||||
 | 
						return fmt.Sprintf("<%s>/%s [->%s]", se.dir.Name(), se.remainingPath, strings.Join(se.linkUnwalked, "/"))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (se symlinkStackEntry) Close() {
 | 
				
			||||||
 | 
						_ = se.dir.Close()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type symlinkStack []*symlinkStackEntry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *symlinkStack) IsEmpty() bool {
 | 
				
			||||||
 | 
						return s == nil || len(*s) == 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *symlinkStack) Close() {
 | 
				
			||||||
 | 
						if s != nil {
 | 
				
			||||||
 | 
							for _, link := range *s {
 | 
				
			||||||
 | 
								link.Close()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// TODO: Switch to clear once we switch to Go 1.21.
 | 
				
			||||||
 | 
							*s = nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						errEmptyStack         = errors.New("[internal] stack is empty")
 | 
				
			||||||
 | 
						errBrokenSymlinkStack = errors.New("[internal error] broken symlink stack")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *symlinkStack) popPart(part string) error {
 | 
				
			||||||
 | 
						if s == nil || s.IsEmpty() {
 | 
				
			||||||
 | 
							// If there is nothing in the symlink stack, then the part was from the
 | 
				
			||||||
 | 
							// real path provided by the user, and this is a no-op.
 | 
				
			||||||
 | 
							return errEmptyStack
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if part == "." {
 | 
				
			||||||
 | 
							// "." components are no-ops -- we drop them when doing SwapLink.
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tailEntry := (*s)[len(*s)-1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Double-check that we are popping the component we expect.
 | 
				
			||||||
 | 
						if len(tailEntry.linkUnwalked) == 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("%w: trying to pop component %q of empty stack entry %s", errBrokenSymlinkStack, part, tailEntry)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						headPart := tailEntry.linkUnwalked[0]
 | 
				
			||||||
 | 
						if headPart != part {
 | 
				
			||||||
 | 
							return fmt.Errorf("%w: trying to pop component %q but the last stack entry is %s (%q)", errBrokenSymlinkStack, part, tailEntry, headPart)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Drop the component, but keep the entry around in case we are dealing
 | 
				
			||||||
 | 
						// with a "tail-chained" symlink.
 | 
				
			||||||
 | 
						tailEntry.linkUnwalked = tailEntry.linkUnwalked[1:]
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *symlinkStack) PopPart(part string) error {
 | 
				
			||||||
 | 
						if err := s.popPart(part); err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, errEmptyStack) {
 | 
				
			||||||
 | 
								// Skip empty stacks.
 | 
				
			||||||
 | 
								err = nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Clean up any of the trailing stack entries that are empty.
 | 
				
			||||||
 | 
						for lastGood := len(*s) - 1; lastGood >= 0; lastGood-- {
 | 
				
			||||||
 | 
							entry := (*s)[lastGood]
 | 
				
			||||||
 | 
							if len(entry.linkUnwalked) > 0 {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							entry.Close()
 | 
				
			||||||
 | 
							(*s) = (*s)[:lastGood]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *symlinkStack) push(dir *os.File, remainingPath, linkTarget string) error {
 | 
				
			||||||
 | 
						if s == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Split the link target and clean up any "" parts.
 | 
				
			||||||
 | 
						linkTargetParts := slices.DeleteFunc(
 | 
				
			||||||
 | 
							strings.Split(linkTarget, "/"),
 | 
				
			||||||
 | 
							func(part string) bool { return part == "" || part == "." })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Copy the directory so the caller doesn't close our copy.
 | 
				
			||||||
 | 
						dirCopy, err := dupFile(dir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add to the stack.
 | 
				
			||||||
 | 
						*s = append(*s, &symlinkStackEntry{
 | 
				
			||||||
 | 
							dir:           dirCopy,
 | 
				
			||||||
 | 
							remainingPath: remainingPath,
 | 
				
			||||||
 | 
							linkUnwalked:  linkTargetParts,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *symlinkStack) SwapLink(linkPart string, dir *os.File, remainingPath, linkTarget string) error {
 | 
				
			||||||
 | 
						// If we are currently inside a symlink resolution, remove the symlink
 | 
				
			||||||
 | 
						// component from the last symlink entry, but don't remove the entry even
 | 
				
			||||||
 | 
						// if it's empty. If we are a "tail-chained" symlink (a trailing symlink we
 | 
				
			||||||
 | 
						// hit during a symlink resolution) we need to keep the old symlink until
 | 
				
			||||||
 | 
						// we finish the resolution.
 | 
				
			||||||
 | 
						if err := s.popPart(linkPart); err != nil {
 | 
				
			||||||
 | 
							if !errors.Is(err, errEmptyStack) {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Push the component regardless of whether the stack was empty.
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s.push(dir, remainingPath, linkTarget)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *symlinkStack) PopTopSymlink() (*os.File, string, bool) {
 | 
				
			||||||
 | 
						if s == nil || s.IsEmpty() {
 | 
				
			||||||
 | 
							return nil, "", false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tailEntry := (*s)[0]
 | 
				
			||||||
 | 
						*s = (*s)[1:]
 | 
				
			||||||
 | 
						return tailEntry.dir, tailEntry.remainingPath, true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// partialLookupInRoot tries to lookup as much of the request path as possible
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					// existing component and a string containing the remaining path components.
 | 
				
			||||||
 | 
					func partialLookupInRoot(root *os.File, unsafePath string) (*os.File, string, error) {
 | 
				
			||||||
 | 
						return lookupInRoot(root, unsafePath, true)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func completeLookupInRoot(root *os.File, unsafePath string) (*os.File, error) {
 | 
				
			||||||
 | 
						handle, remainingPath, err := lookupInRoot(root, unsafePath, false)
 | 
				
			||||||
 | 
						if remainingPath != "" && err == nil {
 | 
				
			||||||
 | 
							// should never happen
 | 
				
			||||||
 | 
							err = fmt.Errorf("[bug] non-empty remaining path when doing a non-partial lookup: %q", remainingPath)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// lookupInRoot(partial=false) will always close the handle if an error is
 | 
				
			||||||
 | 
						// returned, so no need to double-check here.
 | 
				
			||||||
 | 
						return handle, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func lookupInRoot(root *os.File, unsafePath string, partial bool) (Handle *os.File, _ string, _ error) {
 | 
				
			||||||
 | 
						unsafePath = filepath.ToSlash(unsafePath) // noop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// This is very similar to SecureJoin, except that we operate on the
 | 
				
			||||||
 | 
						// components using file descriptors. We then return the last component we
 | 
				
			||||||
 | 
						// managed open, along with the remaining path components not opened.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try to use openat2 if possible.
 | 
				
			||||||
 | 
						if hasOpenat2() {
 | 
				
			||||||
 | 
							return lookupOpenat2(root, unsafePath, partial)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 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
 | 
				
			||||||
 | 
						// make sure when we do checkProcSelfFdPath that we are using the correct
 | 
				
			||||||
 | 
						// root path.
 | 
				
			||||||
 | 
						logicalRootPath, err := procSelfFdReadlink(root)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, "", fmt.Errorf("get real root path: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						currentDir, err := dupFile(root)
 | 
				
			||||||
 | 
						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()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// symlinkStack is used to emulate how openat2(RESOLVE_IN_ROOT) treats
 | 
				
			||||||
 | 
						// dangling symlinks. If we hit a non-existent path while resolving a
 | 
				
			||||||
 | 
						// symlink, we need to return the (dir, remainingPath) that we had when we
 | 
				
			||||||
 | 
						// hit the symlink (treating the symlink as though it were a regular file).
 | 
				
			||||||
 | 
						// The set of (dir, remainingPath) sets is stored within the symlinkStack
 | 
				
			||||||
 | 
						// and we add and remove parts when we hit symlink and non-symlink
 | 
				
			||||||
 | 
						// components respectively. We need a stack because of recursive symlinks
 | 
				
			||||||
 | 
						// (symlinks that contain symlink components in their target).
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Note that the stack is ONLY used for book-keeping. All of the actual
 | 
				
			||||||
 | 
						// path walking logic is still based on currentPath/remainingPath and
 | 
				
			||||||
 | 
						// currentDir (as in SecureJoin).
 | 
				
			||||||
 | 
						var symStack *symlinkStack
 | 
				
			||||||
 | 
						if partial {
 | 
				
			||||||
 | 
							symStack = new(symlinkStack)
 | 
				
			||||||
 | 
							defer symStack.Close()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							linksWalked   int
 | 
				
			||||||
 | 
							currentPath   string
 | 
				
			||||||
 | 
							remainingPath = unsafePath
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						for remainingPath != "" {
 | 
				
			||||||
 | 
							// Save the current remaining path so if the part is not real we can
 | 
				
			||||||
 | 
							// return the path including the component.
 | 
				
			||||||
 | 
							oldRemainingPath := 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 we hit an empty component, we need to treat it as though it is
 | 
				
			||||||
 | 
							// "." so that trailing "/" and "//" components on a non-directory
 | 
				
			||||||
 | 
							// correctly return the right error code.
 | 
				
			||||||
 | 
							if part == "" {
 | 
				
			||||||
 | 
								part = "."
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 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.
 | 
				
			||||||
 | 
							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 == "/" {
 | 
				
			||||||
 | 
								if err := symStack.PopPart(part); err != nil {
 | 
				
			||||||
 | 
									return nil, "", fmt.Errorf("walking into root with part %q failed: %w", part, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// Jump to root.
 | 
				
			||||||
 | 
								rootClone, err := dupFile(root)
 | 
				
			||||||
 | 
								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 := openatFile(currentDir, part, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
							switch {
 | 
				
			||||||
 | 
							case err == nil:
 | 
				
			||||||
 | 
								st, err := nextDir.Stat()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									_ = nextDir.Close()
 | 
				
			||||||
 | 
									return nil, "", fmt.Errorf("stat component %q: %w", part, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								switch st.Mode() & os.ModeType {
 | 
				
			||||||
 | 
								case 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 := readlinkatFile(nextDir, "")
 | 
				
			||||||
 | 
									// We don't need the handle anymore.
 | 
				
			||||||
 | 
									_ = nextDir.Close()
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return nil, "", err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									linksWalked++
 | 
				
			||||||
 | 
									if linksWalked > maxSymlinkLimit {
 | 
				
			||||||
 | 
										return nil, "", &os.PathError{Op: "securejoin.lookupInRoot", Path: logicalRootPath + "/" + unsafePath, Err: unix.ELOOP}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Swap out the symlink's component for the link entry itself.
 | 
				
			||||||
 | 
									if err := symStack.SwapLink(part, currentDir, oldRemainingPath, linkDest); err != nil {
 | 
				
			||||||
 | 
										return nil, "", fmt.Errorf("walking into symlink %q failed: push symlink: %w", part, err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Update our logical remaining path.
 | 
				
			||||||
 | 
									remainingPath = linkDest + "/" + remainingPath
 | 
				
			||||||
 | 
									// Absolute symlinks reset any work we've already done.
 | 
				
			||||||
 | 
									if path.IsAbs(linkDest) {
 | 
				
			||||||
 | 
										// Jump to root.
 | 
				
			||||||
 | 
										rootClone, err := dupFile(root)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											return nil, "", fmt.Errorf("clone root fd: %w", err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										_ = currentDir.Close()
 | 
				
			||||||
 | 
										currentDir = rootClone
 | 
				
			||||||
 | 
										currentPath = "/"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									// If we are dealing with a directory, simply walk into it.
 | 
				
			||||||
 | 
									_ = currentDir.Close()
 | 
				
			||||||
 | 
									currentDir = nextDir
 | 
				
			||||||
 | 
									currentPath = nextPath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// The part was real, so drop it from the symlink stack.
 | 
				
			||||||
 | 
									if err := symStack.PopPart(part); err != nil {
 | 
				
			||||||
 | 
										return nil, "", fmt.Errorf("walking into directory %q failed: %w", part, err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// If we are operating on a .., make sure we haven't escaped.
 | 
				
			||||||
 | 
									// We only have to check for ".." here because walking down
 | 
				
			||||||
 | 
									// into a regular component component cannot cause you to
 | 
				
			||||||
 | 
									// escape. This mirrors the logic in RESOLVE_IN_ROOT, except we
 | 
				
			||||||
 | 
									// have to check every ".." rather than only checking after a
 | 
				
			||||||
 | 
									// rename or mount on the system.
 | 
				
			||||||
 | 
									if part == ".." {
 | 
				
			||||||
 | 
										// Make sure the root hasn't moved.
 | 
				
			||||||
 | 
										if err := checkProcSelfFdPath(logicalRootPath, root); err != nil {
 | 
				
			||||||
 | 
											return nil, "", fmt.Errorf("root path moved during lookup: %w", err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										// Make sure the path is what we expect.
 | 
				
			||||||
 | 
										fullPath := logicalRootPath + nextPath
 | 
				
			||||||
 | 
										if err := checkProcSelfFdPath(fullPath, currentDir); err != nil {
 | 
				
			||||||
 | 
											return nil, "", fmt.Errorf("walking into %q had unexpected result: %w", part, err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								if !partial {
 | 
				
			||||||
 | 
									return nil, "", err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// If there are any remaining components in the symlink stack, we
 | 
				
			||||||
 | 
								// are still within a symlink resolution and thus we hit a dangling
 | 
				
			||||||
 | 
								// symlink. So pretend that the first symlink in the stack we hit
 | 
				
			||||||
 | 
								// was an ENOENT (to match openat2).
 | 
				
			||||||
 | 
								if oldDir, remainingPath, ok := symStack.PopTopSymlink(); ok {
 | 
				
			||||||
 | 
									_ = currentDir.Close()
 | 
				
			||||||
 | 
									return oldDir, remainingPath, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// We have hit a final component that doesn't exist, so we have our
 | 
				
			||||||
 | 
								// partial open result. Note that we have to use the OLD remaining
 | 
				
			||||||
 | 
								// path, since the lookup failed.
 | 
				
			||||||
 | 
								return currentDir, oldRemainingPath, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If the unsafePath had a trailing slash, we need to make sure we try to
 | 
				
			||||||
 | 
						// do a relative "." open so that we will correctly return an error when
 | 
				
			||||||
 | 
						// the final component is a non-directory (to match openat2). In the
 | 
				
			||||||
 | 
						// context of openat2, a trailing slash and a trailing "/." are completely
 | 
				
			||||||
 | 
						// equivalent.
 | 
				
			||||||
 | 
						if strings.HasSuffix(unsafePath, "/") {
 | 
				
			||||||
 | 
							nextDir, err := openatFile(currentDir, ".", unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if !partial {
 | 
				
			||||||
 | 
									_ = currentDir.Close()
 | 
				
			||||||
 | 
									currentDir = nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return currentDir, "", err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_ = currentDir.Close()
 | 
				
			||||||
 | 
							currentDir = nextDir
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// All of the components existed!
 | 
				
			||||||
 | 
						return currentDir, "", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										207
									
								
								vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								vendor/github.com/cyphar/filepath-securejoin/mkdir_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
				
			|||||||
 | 
					//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"
 | 
				
			||||||
 | 
						"slices"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/sys/unix"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						errInvalidMode    = errors.New("invalid permission mode")
 | 
				
			||||||
 | 
						errPossibleAttack = errors.New("possible attack detected")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MkdirAllHandle is equivalent to [MkdirAll], except that it is safer to use
 | 
				
			||||||
 | 
					// in two respects:
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//   - The caller provides the root directory as an *[os.File] (preferably O_PATH)
 | 
				
			||||||
 | 
					//     handle. This means that the caller can be sure which root directory is
 | 
				
			||||||
 | 
					//     being used. Note that this can be emulated by using /proc/self/fd/... as
 | 
				
			||||||
 | 
					//     the root path with [os.MkdirAll].
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//   - Once all of the directories have been created, an *[os.File] O_PATH handle
 | 
				
			||||||
 | 
					//     to the directory at unsafePath is returned to the caller. This is done in
 | 
				
			||||||
 | 
					//     an effectively-race-free way (an attacker would only be able to swap the
 | 
				
			||||||
 | 
					//     final directory component), which is not possible to emulate with
 | 
				
			||||||
 | 
					//     [MkdirAll].
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// In addition, the returned handle is obtained far more efficiently than doing
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					// should use MkdirAllHandle.
 | 
				
			||||||
 | 
					func MkdirAllHandle(root *os.File, unsafePath string, mode int) (_ *os.File, Err error) {
 | 
				
			||||||
 | 
						// Make sure there are no os.FileMode bits set.
 | 
				
			||||||
 | 
						if mode&^0o7777 != 0 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("%w for mkdir 0o%.3o", errInvalidMode, mode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// On Linux, mkdirat(2) (and os.Mkdir) silently ignore the suid and sgid
 | 
				
			||||||
 | 
						// bits. We could also silently ignore them but since we have very few
 | 
				
			||||||
 | 
						// users it seems more prudent to return an error so users notice that
 | 
				
			||||||
 | 
						// these bits will not be set.
 | 
				
			||||||
 | 
						if mode&^0o1777 != 0 {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("%w for mkdir 0o%.3o: suid and sgid are ignored by mkdir", errInvalidMode, mode)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try to open as much of the path as possible.
 | 
				
			||||||
 | 
						currentDir, remainingPath, err := partialLookupInRoot(root, unsafePath)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if Err != nil {
 | 
				
			||||||
 | 
								_ = currentDir.Close()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
						if err != nil && !errors.Is(err, unix.ENOENT) {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("find existing subpath of %q: %w", unsafePath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If there is an attacker deleting directories as we walk into them,
 | 
				
			||||||
 | 
						// detect this proactively. Note this is guaranteed to detect if the
 | 
				
			||||||
 | 
						// attacker deleted any part of the tree up to currentDir.
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// Once we walk into a dead directory, partialLookupInRoot would not be
 | 
				
			||||||
 | 
						// able to walk further down the tree (directories must be empty before
 | 
				
			||||||
 | 
						// they are deleted), and if the attacker has removed the entire tree we
 | 
				
			||||||
 | 
						// can be sure that anything that was originally inside a dead directory
 | 
				
			||||||
 | 
						// must also be deleted and thus is a dead directory in its own right.
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						// This is mostly a quality-of-life check, because mkdir will simply fail
 | 
				
			||||||
 | 
						// later if the attacker deletes the tree after this check.
 | 
				
			||||||
 | 
						if err := isDeadInode(currentDir); err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("finding existing subpath of %q: %w", unsafePath, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Re-open the path to match the O_DIRECTORY reopen loop later (so that we
 | 
				
			||||||
 | 
						// always return a non-O_PATH handle). We also check that we actually got a
 | 
				
			||||||
 | 
						// directory.
 | 
				
			||||||
 | 
						if reopenDir, err := Reopen(currentDir, unix.O_DIRECTORY|unix.O_CLOEXEC); errors.Is(err, unix.ENOTDIR) {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("cannot create subdirectories in %q: %w", currentDir.Name(), unix.ENOTDIR)
 | 
				
			||||||
 | 
						} else if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("re-opening handle to %q: %w", currentDir.Name(), err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							_ = currentDir.Close()
 | 
				
			||||||
 | 
							currentDir = reopenDir
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						remainingParts := strings.Split(remainingPath, string(filepath.Separator))
 | 
				
			||||||
 | 
						if slices.Contains(remainingParts, "..") {
 | 
				
			||||||
 | 
							// The path contained ".." components after the end of the "real"
 | 
				
			||||||
 | 
							// 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
 | 
				
			||||||
 | 
							// needs to be supported. So just return an error.
 | 
				
			||||||
 | 
							//
 | 
				
			||||||
 | 
							// If we do filepath.Clean(remainingPath) then we end up with the
 | 
				
			||||||
 | 
							// problem that ".." can erase a trailing dangling symlink and produce
 | 
				
			||||||
 | 
							// a path that doesn't quite match what the user asked for.
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("%w: yet-to-be-created path %q contains '..' components", unix.ENOENT, remainingPath)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make sure the mode doesn't have any type bits.
 | 
				
			||||||
 | 
						mode &^= unix.S_IFMT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create the remaining components.
 | 
				
			||||||
 | 
						for _, part := range remainingParts {
 | 
				
			||||||
 | 
							switch part {
 | 
				
			||||||
 | 
							case "", ".":
 | 
				
			||||||
 | 
								// Skip over no-op paths.
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// NOTE: mkdir(2) will not follow trailing symlinks, so we can safely
 | 
				
			||||||
 | 
							// create the final component without worrying about symlink-exchange
 | 
				
			||||||
 | 
							// attacks.
 | 
				
			||||||
 | 
							if err := unix.Mkdirat(int(currentDir.Fd()), part, uint32(mode)); err != nil {
 | 
				
			||||||
 | 
								err = &os.PathError{Op: "mkdirat", Path: currentDir.Name() + "/" + part, Err: err}
 | 
				
			||||||
 | 
								// Make the error a bit nicer if the directory is dead.
 | 
				
			||||||
 | 
								if err2 := isDeadInode(currentDir); err2 != nil {
 | 
				
			||||||
 | 
									err = fmt.Errorf("%w (%w)", err, err2)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get a handle to the next component. O_DIRECTORY means we don't need
 | 
				
			||||||
 | 
							// to use O_PATH.
 | 
				
			||||||
 | 
							var nextDir *os.File
 | 
				
			||||||
 | 
							if hasOpenat2() {
 | 
				
			||||||
 | 
								nextDir, err = openat2File(currentDir, part, &unix.OpenHow{
 | 
				
			||||||
 | 
									Flags:   unix.O_NOFOLLOW | unix.O_DIRECTORY | unix.O_CLOEXEC,
 | 
				
			||||||
 | 
									Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								nextDir, err = openatFile(currentDir, part, unix.O_NOFOLLOW|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_ = currentDir.Close()
 | 
				
			||||||
 | 
							currentDir = nextDir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// It's possible that the directory we just opened was swapped by an
 | 
				
			||||||
 | 
							// attacker. Unfortunately there isn't much we can do to protect
 | 
				
			||||||
 | 
							// against this, and MkdirAll's behaviour is that we will reuse
 | 
				
			||||||
 | 
							// existing directories anyway so the need to protect against this is
 | 
				
			||||||
 | 
							// incredibly limited (and arguably doesn't even deserve mention here).
 | 
				
			||||||
 | 
							//
 | 
				
			||||||
 | 
							// Ideally we might want to check that the owner and mode match what we
 | 
				
			||||||
 | 
							// would've created -- unfortunately, it is non-trivial to verify that
 | 
				
			||||||
 | 
							// the owner and mode of the created directory match. While plain Unix
 | 
				
			||||||
 | 
							// DAC rules seem simple enough to emulate, there are a bunch of other
 | 
				
			||||||
 | 
							// factors that can change the mode or owner of created directories
 | 
				
			||||||
 | 
							// (default POSIX ACLs, mount options like uid=1,gid=2,umask=0 on
 | 
				
			||||||
 | 
							// filesystems like vfat, etc etc). We used to try to verify this but
 | 
				
			||||||
 | 
							// it just lead to a series of spurious errors.
 | 
				
			||||||
 | 
							//
 | 
				
			||||||
 | 
							// We could also check that the directory is non-empty, but
 | 
				
			||||||
 | 
							// unfortunately some pseduofilesystems (like cgroupfs) create
 | 
				
			||||||
 | 
							// non-empty directories, which would result in different spurious
 | 
				
			||||||
 | 
							// errors.
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return currentDir, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// MkdirAll is a race-safe alternative to the [os.MkdirAll] function,
 | 
				
			||||||
 | 
					// where the new directory is guaranteed to be within the root directory (if an
 | 
				
			||||||
 | 
					// attacker can move directories from inside the root to outside the root, the
 | 
				
			||||||
 | 
					// created directory tree might be outside of the root but the key constraint
 | 
				
			||||||
 | 
					// is that at no point will we walk outside of the directory tree we are
 | 
				
			||||||
 | 
					// creating).
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Effectively, MkdirAll(root, unsafePath, mode) is equivalent to
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	path, _ := securejoin.SecureJoin(root, unsafePath)
 | 
				
			||||||
 | 
					//	err := os.MkdirAll(path, mode)
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// But is much safer. The above implementation is unsafe because if an attacker
 | 
				
			||||||
 | 
					// can modify the filesystem tree between [SecureJoin] and [os.MkdirAll], it is
 | 
				
			||||||
 | 
					// possible for MkdirAll to resolve unsafe symlink components and create
 | 
				
			||||||
 | 
					// directories outside of the root.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// 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.
 | 
				
			||||||
 | 
					// This function is a wrapper around [MkdirAllHandle].
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// NOTE: The mode argument must be set the unix mode bits (unix.S_I...), not
 | 
				
			||||||
 | 
					// the Go generic mode bits ([os.FileMode]...).
 | 
				
			||||||
 | 
					func MkdirAll(root, unsafePath string, mode int) error {
 | 
				
			||||||
 | 
						rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rootDir.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						f, err := MkdirAllHandle(rootDir, unsafePath, mode)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_ = f.Close()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										103
									
								
								vendor/github.com/cyphar/filepath-securejoin/open_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								vendor/github.com/cyphar/filepath-securejoin/open_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					//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 (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/sys/unix"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// OpenatInRoot is equivalent to [OpenInRoot], except that the root is provided
 | 
				
			||||||
 | 
					// using an *[os.File] handle, to ensure that the correct root directory is used.
 | 
				
			||||||
 | 
					func OpenatInRoot(root *os.File, unsafePath string) (*os.File, error) {
 | 
				
			||||||
 | 
						handle, err := completeLookupInRoot(root, unsafePath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, &os.PathError{Op: "securejoin.OpenInRoot", Path: unsafePath, Err: err}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return handle, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// OpenInRoot safely opens the provided unsafePath within the root.
 | 
				
			||||||
 | 
					// Effectively, OpenInRoot(root, unsafePath) is equivalent to
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	path, _ := securejoin.SecureJoin(root, unsafePath)
 | 
				
			||||||
 | 
					//	handle, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC)
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// But is much safer. The above implementation is unsafe because if an attacker
 | 
				
			||||||
 | 
					// can modify the filesystem tree between [SecureJoin] and [os.OpenFile], it is
 | 
				
			||||||
 | 
					// possible for the returned file to be outside of the root.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Note that the returned handle is an O_PATH handle, meaning that only a very
 | 
				
			||||||
 | 
					// limited set of operations will work on the handle. This is done to avoid
 | 
				
			||||||
 | 
					// accidentally opening an untrusted file that could cause issues (such as a
 | 
				
			||||||
 | 
					// 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
 | 
				
			||||||
 | 
					// [Reopen].
 | 
				
			||||||
 | 
					func OpenInRoot(root, unsafePath string) (*os.File, error) {
 | 
				
			||||||
 | 
						rootDir, err := os.OpenFile(root, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer rootDir.Close()
 | 
				
			||||||
 | 
						return OpenatInRoot(rootDir, unsafePath)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Reopen takes an *[os.File] handle and re-opens it through /proc/self/fd.
 | 
				
			||||||
 | 
					// Reopen(file, flags) is effectively equivalent to
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	fdPath := fmt.Sprintf("/proc/self/fd/%d", file.Fd())
 | 
				
			||||||
 | 
					//	os.OpenFile(fdPath, flags|unix.O_CLOEXEC)
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// But with some extra hardenings to ensure that we are not tricked by a
 | 
				
			||||||
 | 
					// maliciously-configured /proc mount. While this attack scenario is not
 | 
				
			||||||
 | 
					// common, in container runtimes it is possible for higher-level runtimes to be
 | 
				
			||||||
 | 
					// tricked into configuring an unsafe /proc that can be used to attack file
 | 
				
			||||||
 | 
					// operations. See [CVE-2019-19921] for more details.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// [CVE-2019-19921]: https://github.com/advisories/GHSA-fh74-hm69-rqjw
 | 
				
			||||||
 | 
					func Reopen(handle *os.File, flags int) (*os.File, error) {
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										128
									
								
								vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								vendor/github.com/cyphar/filepath-securejoin/openat2_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					//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"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										59
									
								
								vendor/github.com/cyphar/filepath-securejoin/openat_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								vendor/github.com/cyphar/filepath-securejoin/openat_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										440
									
								
								vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										440
									
								
								vendor/github.com/cyphar/filepath-securejoin/procfs_linux.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,440 @@
 | 
				
			|||||||
 | 
					//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"
 | 
				
			||||||
 | 
						"sync"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"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 {
 | 
				
			||||||
 | 
								return nil, nil, fmt.Errorf("%w: %w", errUnsafeProcfs, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							handle, err = openatFile(procRoot, threadSelf+subpath, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, nil, fmt.Errorf("%w: %w", errUnsafeProcfs, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 = unix.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 = unix.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 }
 | 
				
			||||||
							
								
								
									
										26
									
								
								vendor/github.com/cyphar/filepath-securejoin/vfs.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								vendor/github.com/cyphar/filepath-securejoin/vfs.go
									
									
									
										generated
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
// Copyright (C) 2017 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,19 +10,19 @@ import "os"
 | 
				
			|||||||
// are several projects (umoci and go-mtree) that are using this sort of
 | 
					// are several projects (umoci and go-mtree) that are using this sort of
 | 
				
			||||||
// interface.
 | 
					// interface.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// VFS is the minimal interface necessary to use SecureJoinVFS. A nil VFS is
 | 
					// VFS is the minimal interface necessary to use [SecureJoinVFS]. A nil VFS is
 | 
				
			||||||
// equivalent to using the standard os.* family of functions. This is mainly
 | 
					// equivalent to using the standard [os].* family of functions. This is mainly
 | 
				
			||||||
// used for the purposes of mock testing, but also can be used to otherwise use
 | 
					// used for the purposes of mock testing, but also can be used to otherwise use
 | 
				
			||||||
// SecureJoin with VFS-like system.
 | 
					// [SecureJoinVFS] with VFS-like system.
 | 
				
			||||||
type VFS interface {
 | 
					type VFS interface {
 | 
				
			||||||
	// Lstat returns a FileInfo describing the named file. If the file is a
 | 
						// Lstat returns an [os.FileInfo] describing the named file. If the
 | 
				
			||||||
	// symbolic link, the returned FileInfo describes the symbolic link. Lstat
 | 
						// file is a symbolic link, the returned [os.FileInfo] describes the
 | 
				
			||||||
	// makes no attempt to follow the link. These semantics are identical to
 | 
						// symbolic link. Lstat makes no attempt to follow the link.
 | 
				
			||||||
	// os.Lstat.
 | 
						// The semantics are identical to [os.Lstat].
 | 
				
			||||||
	Lstat(name string) (os.FileInfo, error)
 | 
						Lstat(name string) (os.FileInfo, error)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Readlink returns the destination of the named symbolic link. These
 | 
						// Readlink returns the destination of the named symbolic link.
 | 
				
			||||||
	// semantics are identical to os.Readlink.
 | 
						// The semantics are identical to [os.Readlink].
 | 
				
			||||||
	Readlink(name string) (string, error)
 | 
						Readlink(name string) (string, error)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,12 +30,6 @@ type VFS interface {
 | 
				
			|||||||
// module.
 | 
					// module.
 | 
				
			||||||
type osVFS struct{}
 | 
					type osVFS struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Lstat returns a FileInfo describing the named file. If the file is a
 | 
					 | 
				
			||||||
// symbolic link, the returned FileInfo describes the symbolic link. Lstat
 | 
					 | 
				
			||||||
// makes no attempt to follow the link. These semantics are identical to
 | 
					 | 
				
			||||||
// os.Lstat.
 | 
					 | 
				
			||||||
func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) }
 | 
					func (o osVFS) Lstat(name string) (os.FileInfo, error) { return os.Lstat(name) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Readlink returns the destination of the named symbolic link. These
 | 
					 | 
				
			||||||
// semantics are identical to os.Readlink.
 | 
					 | 
				
			||||||
func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) }
 | 
					func (o osVFS) Readlink(name string) (string, error) { return os.Readlink(name) }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							@@ -127,8 +127,8 @@ github.com/coreos/go-systemd/v22/util
 | 
				
			|||||||
# github.com/cpuguy83/go-md2man/v2 v2.0.4
 | 
					# github.com/cpuguy83/go-md2man/v2 v2.0.4
 | 
				
			||||||
## explicit; go 1.11
 | 
					## explicit; go 1.11
 | 
				
			||||||
github.com/cpuguy83/go-md2man/v2/md2man
 | 
					github.com/cpuguy83/go-md2man/v2/md2man
 | 
				
			||||||
# github.com/cyphar/filepath-securejoin v0.2.4
 | 
					# github.com/cyphar/filepath-securejoin v0.3.4
 | 
				
			||||||
## explicit; go 1.13
 | 
					## explicit; go 1.21
 | 
				
			||||||
github.com/cyphar/filepath-securejoin
 | 
					github.com/cyphar/filepath-securejoin
 | 
				
			||||||
# github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 | 
					# github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
 | 
				
			||||||
## explicit
 | 
					## explicit
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user