Add an internal/open package
Add a package that lets us open a directory in a chroot, pass its descriptor up, and then bind mount that directory to a specified location. Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
parent
250174ede2
commit
c6d1064cee
|
|
@ -0,0 +1,39 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// InChroot opens the file at `path` after chrooting to `root` and then
|
||||
// changing its working directory to `wd`. Both `wd` and `path` are evaluated
|
||||
// in the chroot.
|
||||
// Returns a file handle, an Errno value if there was an error and the
|
||||
// underlying error was a standard library error code, and a non-empty error if
|
||||
// one was detected.
|
||||
func InChroot(root, wd, path string, mode int, perm uint32) (fd int, errno syscall.Errno, err error) {
|
||||
requests := requests{
|
||||
Root: root,
|
||||
Wd: wd,
|
||||
Open: []request{
|
||||
{
|
||||
Path: path,
|
||||
Mode: mode,
|
||||
Perms: perm,
|
||||
},
|
||||
},
|
||||
}
|
||||
results := inChroot(requests)
|
||||
if len(results.Open) != 1 {
|
||||
return -1, 0, fmt.Errorf("got %d results back instead of 1", len(results.Open))
|
||||
}
|
||||
if results.Open[0].Err != "" {
|
||||
if results.Open[0].Errno != 0 {
|
||||
err = fmt.Errorf("%s: %w", results.Open[0].Err, results.Open[0].Errno)
|
||||
} else {
|
||||
err = errors.New(results.Open[0].Err)
|
||||
}
|
||||
}
|
||||
return int(results.Open[0].Fd), results.Open[0].Errno, err
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
bindFdToPathCommand = "buildah-bind-fd-to-path"
|
||||
)
|
||||
|
||||
func init() {
|
||||
reexec.Register(bindFdToPathCommand, bindFdToPathMain)
|
||||
}
|
||||
|
||||
// BindFdToPath creates a bind mount from the open file (which is actually a
|
||||
// directory) to the specified location. If it succeeds, the caller will need
|
||||
// to unmount the targetPath when it's finished using it. Regardless, it
|
||||
// closes the passed-in descriptor.
|
||||
func BindFdToPath(fd uintptr, targetPath string) error {
|
||||
f := os.NewFile(fd, "passed-in directory descriptor")
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logrus.Debugf("closing descriptor %d after attempting to bind to %q: %v", fd, targetPath, err)
|
||||
}
|
||||
}()
|
||||
pipeReader, pipeWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := reexec.Command(bindFdToPathCommand)
|
||||
cmd.Stdin = pipeReader
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, f)
|
||||
|
||||
err = cmd.Start()
|
||||
pipeReader.Close()
|
||||
if err != nil {
|
||||
pipeWriter.Close()
|
||||
return fmt.Errorf("starting child: %w", err)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(pipeWriter)
|
||||
if err := encoder.Encode(&targetPath); err != nil {
|
||||
return fmt.Errorf("sending target path to child: %w", err)
|
||||
}
|
||||
pipeWriter.Close()
|
||||
err = cmd.Wait()
|
||||
trimmedOutput := strings.TrimSpace(stdout.String()) + strings.TrimSpace(stderr.String())
|
||||
if err != nil {
|
||||
if len(trimmedOutput) > 0 {
|
||||
err = fmt.Errorf("%s: %w", trimmedOutput, err)
|
||||
}
|
||||
} else {
|
||||
if len(trimmedOutput) > 0 {
|
||||
err = errors.New(trimmedOutput)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func bindFdToPathMain() {
|
||||
var targetPath string
|
||||
decoder := json.NewDecoder(os.Stdin)
|
||||
if err := decoder.Decode(&targetPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error decoding target path")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Fchdir(3); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fchdir(): %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Mount(".", targetPath, "bind", unix.MS_BIND, ""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "bind-mounting passed-in directory to %q: %v", targetPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestBindFdToPath(t *testing.T) {
|
||||
first := t.TempDir()
|
||||
sampleData := []byte("sample data")
|
||||
err := os.WriteFile(filepath.Join(first, "testfile"), sampleData, 0o600)
|
||||
require.NoError(t, err, "writing sample data to first directory")
|
||||
fd, err := unix.Open(first, unix.O_DIRECTORY, 0)
|
||||
require.NoError(t, err, "opening descriptor for first directory")
|
||||
second := t.TempDir()
|
||||
err = BindFdToPath(uintptr(fd), second)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := unix.Unmount(second, unix.MNT_DETACH)
|
||||
require.NoError(t, err, "unmounting as part of cleanup")
|
||||
})
|
||||
readBack, err := os.ReadFile(filepath.Join(second, "testfile"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, sampleData, readBack, "expected to read back data via the bind mount")
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if reexec.Init() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestOpenInChroot(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
firstContents := []byte{0, 1, 2, 3}
|
||||
secondContents := []byte{4, 5, 6, 7}
|
||||
require.NoErrorf(t, os.WriteFile(filepath.Join(tmpdir, "a"), firstContents, 0o644), "creating first test file")
|
||||
require.NoErrorf(t, os.MkdirAll(filepath.Join(tmpdir, tmpdir), 0o755), "creating test subdirectory")
|
||||
require.NoErrorf(t, os.WriteFile(filepath.Join(tmpdir, tmpdir, "a"), secondContents, 0o644), "creating second test file")
|
||||
|
||||
result := inChroot(requests{
|
||||
Open: []request{
|
||||
{
|
||||
Path: filepath.Join(tmpdir, "a"),
|
||||
Mode: unix.O_RDONLY,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Empty(t, result.Err, "result from first client")
|
||||
require.Equal(t, 1, len(result.Open), "results from first client")
|
||||
require.Empty(t, result.Open[0].Err, "first (only) result from first client")
|
||||
f := os.NewFile(result.Open[0].Fd, "file from first subprocess")
|
||||
contents, err := io.ReadAll(f)
|
||||
require.NoErrorf(t, err, "reading from file from first subprocess")
|
||||
require.Equalf(t, firstContents, contents, "contents of file from first subprocess")
|
||||
f.Close()
|
||||
|
||||
result = inChroot(requests{
|
||||
Root: tmpdir,
|
||||
Open: []request{
|
||||
{
|
||||
Path: filepath.Join(tmpdir, "a"),
|
||||
Mode: unix.O_RDONLY,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Empty(t, result.Err, "result from second client")
|
||||
require.Equal(t, 1, len(result.Open), "results from second client")
|
||||
require.Empty(t, result.Open[0].Err, "first (only) result from second client")
|
||||
f = os.NewFile(result.Open[0].Fd, "file from second subprocess")
|
||||
contents, err = io.ReadAll(f)
|
||||
require.NoErrorf(t, err, "reading from file from second subprocess")
|
||||
require.Equalf(t, secondContents, contents, "contents of file from second subprocess")
|
||||
f.Close()
|
||||
|
||||
fd, errno, err := InChroot(tmpdir, "", filepath.Join(tmpdir, "a"), unix.O_RDONLY, 0)
|
||||
require.NoErrorf(t, err, "wrapper for opening just one item")
|
||||
require.Zero(t, errno, "errno from open file")
|
||||
f = os.NewFile(uintptr(fd), "file from third subprocess")
|
||||
require.NoErrorf(t, err, "reading from file from third subprocess")
|
||||
require.Equalf(t, secondContents, contents, "contents of file from third subprocess")
|
||||
f.Close()
|
||||
|
||||
fd, errno, err = InChroot(tmpdir, "", filepath.Join(tmpdir, "b"), unix.O_RDONLY, 0)
|
||||
require.Errorf(t, err, "attempting to open a non-existent file")
|
||||
require.NotZero(t, errno, "attempting to open a non-existent file")
|
||||
require.Equal(t, -1, fd, "returned descriptor when open fails")
|
||||
f.Close()
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package open
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
Path string
|
||||
Mode int
|
||||
Perms uint32
|
||||
}
|
||||
|
||||
type requests struct {
|
||||
Root string
|
||||
Wd string
|
||||
Open []request
|
||||
}
|
||||
|
||||
type result struct {
|
||||
Fd uintptr // as returned by open()
|
||||
Err string // if err was not `nil`, err.Error()
|
||||
Errno syscall.Errno // if err was not `nil` and included a syscall.Errno, its value
|
||||
}
|
||||
|
||||
type results struct {
|
||||
Err string
|
||||
Open []result
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
//go:build linux || freebsd || darwin
|
||||
|
||||
package open
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
inChrootCommand = "buildah-open-in-chroot"
|
||||
)
|
||||
|
||||
func init() {
|
||||
reexec.Register(inChrootCommand, inChrootMain)
|
||||
}
|
||||
|
||||
func inChroot(requests requests) results {
|
||||
sock, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("creating socket pair: %w", err).Error()}
|
||||
}
|
||||
parentSock := sock[0]
|
||||
childSock := sock[1]
|
||||
parentEnd := os.NewFile(uintptr(parentSock), "parent end of socket pair")
|
||||
childEnd := os.NewFile(uintptr(childSock), "child end of socket pair")
|
||||
cmd := reexec.Command(inChrootCommand)
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, childEnd)
|
||||
err = cmd.Start()
|
||||
childEnd.Close()
|
||||
defer parentEnd.Close()
|
||||
if err != nil {
|
||||
return results{Err: err.Error()}
|
||||
}
|
||||
encoder := json.NewEncoder(parentEnd)
|
||||
if err := encoder.Encode(&requests); err != nil {
|
||||
return results{Err: fmt.Errorf("sending request down socket: %w", err).Error()}
|
||||
}
|
||||
if err := unix.Shutdown(parentSock, unix.SHUT_WR); err != nil {
|
||||
return results{Err: fmt.Errorf("finishing sending request down socket: %w", err).Error()}
|
||||
}
|
||||
b := make([]byte, 65536)
|
||||
oob := make([]byte, 65536)
|
||||
n, oobn, _, _, err := unix.Recvmsg(parentSock, b, oob, 0)
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("receiving message: %w", err).Error()}
|
||||
}
|
||||
if err := unix.Shutdown(parentSock, unix.SHUT_RD); err != nil {
|
||||
return results{Err: fmt.Errorf("finishing socket: %w", err).Error()}
|
||||
}
|
||||
if n > len(b) {
|
||||
return results{Err: fmt.Errorf("too much regular data: %d > %d", n, len(b)).Error()}
|
||||
}
|
||||
if oobn > len(oob) {
|
||||
return results{Err: fmt.Errorf("too much OOB data: %d > %d", oobn, len(oob)).Error()}
|
||||
}
|
||||
scms, err := unix.ParseSocketControlMessage(oob[:oobn])
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("parsing control message: %w", err).Error()}
|
||||
}
|
||||
var receivedFds []int
|
||||
for i := range scms {
|
||||
fds, err := unix.ParseUnixRights(&scms[i])
|
||||
if err != nil {
|
||||
return results{Err: fmt.Errorf("parsing rights message %d: %w", i, err).Error()}
|
||||
}
|
||||
receivedFds = append(receivedFds, fds...)
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(b[:n]))
|
||||
var result results
|
||||
if err := decoder.Decode(&result); err != nil {
|
||||
return results{Err: fmt.Errorf("decoding results: %w", err).Error()}
|
||||
}
|
||||
j := 0
|
||||
for i := range result.Open {
|
||||
if result.Open[i].Err == "" {
|
||||
if j >= len(receivedFds) {
|
||||
for _, fd := range receivedFds {
|
||||
unix.Close(fd)
|
||||
}
|
||||
return results{Err: fmt.Errorf("didn't receive enough FDs").Error()}
|
||||
}
|
||||
result.Open[i].Fd = uintptr(receivedFds[j])
|
||||
j++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func inChrootMain() {
|
||||
var theseRequests requests
|
||||
var theseResults results
|
||||
sockFd := 3
|
||||
sock := os.NewFile(uintptr(sockFd), "socket connection to parent process")
|
||||
defer sock.Close()
|
||||
encoder := json.NewEncoder(sock)
|
||||
decoder := json.NewDecoder(sock)
|
||||
if err := decoder.Decode(&theseRequests); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("decoding request: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if theseRequests.Root != "" {
|
||||
if err := os.Chdir(theseRequests.Root); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("changing to %q: %w", theseRequests.Root, err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Chroot(theseRequests.Root); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("chrooting to %q: %w", theseRequests.Root, err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("changing to new root: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if theseRequests.Wd != "" {
|
||||
if err := os.Chdir(theseRequests.Wd); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("changing to %q in chroot: %w", theseRequests.Wd, err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
var fds []int
|
||||
for _, request := range theseRequests.Open {
|
||||
fd, err := unix.Open(request.Path, request.Mode, request.Perms)
|
||||
thisResult := result{Fd: uintptr(fd)}
|
||||
if err == nil {
|
||||
fds = append(fds, fd)
|
||||
} else {
|
||||
var errno syscall.Errno
|
||||
thisResult.Err = err.Error()
|
||||
if errors.As(err, &errno) {
|
||||
thisResult.Errno = errno
|
||||
}
|
||||
}
|
||||
theseResults.Open = append(theseResults.Open, thisResult)
|
||||
}
|
||||
rights := unix.UnixRights(fds...)
|
||||
inband, err := json.Marshal(&theseResults)
|
||||
if err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("sending response: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := unix.Sendmsg(sockFd, inband, rights, nil, 0); err != nil {
|
||||
if err := encoder.Encode(results{Err: fmt.Errorf("sending response: %w", err).Error()}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
//go:build !linux && !freebsd && !darwin
|
||||
|
||||
package open
|
||||
|
||||
func inChroot(requests requests) results {
|
||||
return results{Err: "open-in-chroot not available on this platform"}
|
||||
}
|
||||
Loading…
Reference in New Issue