Confine package ownership changes to private mounts

This commit is contained in:
ms2k 2026-07-27 03:31:58 +09:00
commit 8334fd379f

View file

@ -0,0 +1,167 @@
diff --git a/runsc/cmd/gofer.go b/runsc/cmd/gofer.go
index 2cfbae6..8d7bc35 100644
--- a/runsc/cmd/gofer.go
+++ b/runsc/cmd/gofer.go
@@ -16,6 +16,7 @@ package cmd
import (
"context"
+ "encoding/json"
"fmt"
"os"
"path/filepath"
@@ -42,6 +43,8 @@ import (
"gvisor.dev/gvisor/runsc/specutils"
)
+const ownerChangeMountsAnnotation = "dev.lazaret.runsc.owner-change-mounts"
+
var caps = []string{
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
@@ -398,6 +401,30 @@ func (g *Gofer) serve(spec *specs.Spec, conf *config.Config, root string, ruid i
log.Infof("Serving /dev mapped on FD %d (ro: false)", g.devIoFD)
}
+ servedMounts := make(map[string]bool, len(cfgs))
+ for _, cfg := range cfgs {
+ servedMounts[cfg.mountPath] = true
+ }
+ ownerChangeMounts := make(map[string]bool)
+ if encoded, ok := spec.Annotations[ownerChangeMountsAnnotation]; ok {
+ var destinations []string
+ if err := json.Unmarshal([]byte(encoded), &destinations); err != nil {
+ util.Fatalf("parsing %s annotation: %v", ownerChangeMountsAnnotation, err)
+ }
+ for _, destination := range destinations {
+ if !filepath.IsAbs(destination) || filepath.Clean(destination) != destination {
+ util.Fatalf("invalid owner-change mount destination: %q", destination)
+ }
+ if !servedMounts[destination] {
+ util.Fatalf("owner-change destination is not a gofer mount: %q", destination)
+ }
+ if ownerChangeMounts[destination] {
+ util.Fatalf("duplicate owner-change mount destination: %q", destination)
+ }
+ ownerChangeMounts[destination] = true
+ }
+ }
+
// These are global fsgofer configurations.
fsgoferConf := &fsgofer.Config{
HostUDS: conf.GetHostUDS(),
@@ -430,7 +457,10 @@ func (g *Gofer) serve(spec *specs.Spec, conf *config.Config, root string, ruid i
}
}
if connImpl == nil {
- connImpl = fsgofer.NewConnectionImpl(fsgoferConf)
+ mountConf := *fsgoferConf
+ mountConf.DenyChown = !ownerChangeMounts[cfg.mountPath]
+ mountConf.DonateMountPointFD = mountConf.DonateMountPointFD && ownerChangeMounts[cfg.mountPath]
+ connImpl = fsgofer.NewConnectionImpl(&mountConf)
connOpts = fsgofer.ConnectionOpts(cfg.readonly)
}
conn, err := server.CreateConnection(cfg.sock, cfg.mountPath, connOpts, connImpl)
diff --git a/runsc/fsgofer/lisafs.go b/runsc/fsgofer/lisafs.go
index ff6aeff..e5829b2 100644
--- a/runsc/fsgofer/lisafs.go
+++ b/runsc/fsgofer/lisafs.go
@@ -61,6 +61,9 @@ type Config struct {
// be donated to the client on Mount RPC.
DonateMountPointFD bool
+ // DenyChown rejects owner changes before applying any requested attributes.
+ DenyChown bool
+
// Gofer process's RUID.
RUID int
@@ -148,6 +151,7 @@ func (i *connectionImpl) Mount(c *lisafs.Connection, mountNode *lisafs.Node) (*l
hostFD: rootHostFD,
writableHostFD: atomicbitops.FromInt32(-1),
isMountPoint: true,
+ denyChown: i.config.DenyChown,
}
mountNode.IncRef() // Ref is transferred to ControlFD.
rootFD.ControlFD.Init(c, mountNode, linux.FileMode(stat.Mode), rootFD)
@@ -201,6 +205,7 @@ func (i *connectionImpl) SupportedMessages() []lisafs.MID {
// controlFDLisa implements lisafs.ControlFDImpl.
type controlFDLisa struct {
lisafs.ControlFD
+ denyChown bool
// hostFD is the file descriptor which can be used to make host syscalls.
hostFD int
@@ -245,6 +250,7 @@ func newControlFDLisa(hostFD int, parent *controlFDLisa, name string, mode linux
})
childFD.hostFD = hostFD
childFD.writableHostFD = atomicbitops.FromInt32(-1)
+ childFD.denyChown = parent.denyChown
childFD.ControlFD.Init(parent.Conn(), childNode, mode, childFD)
return childFD
}
@@ -304,6 +310,9 @@ func (fd *controlFDLisa) Stat() (lisafs.Statx, error) {
// SetStat implements lisafs.ControlFDImpl.SetStat.
func (fd *controlFDLisa) SetStat(stat lisafs.SetStatReq) (failureMask uint32, failureErr error) {
+ if fd.denyChown && stat.Mask&(unix.STATX_UID|unix.STATX_GID) != 0 {
+ return stat.Mask, unix.EPERM
+ }
if stat.Mask&unix.STATX_MODE != 0 {
switch fd.FileType() {
case unix.S_IFLNK:
@@ -705,9 +719,11 @@ func (fd *controlFDLisa) Symlink(name string, target string, uid lisafs.UID, gid
if err != nil {
return nil, lisafs.Statx{}, err
}
- if err := fchown(symlinkFD, uid, gid); err != nil {
- unix.Close(symlinkFD)
- return nil, lisafs.Statx{}, err
+ if !fd.denyChown {
+ if err := fchown(symlinkFD, uid, gid); err != nil {
+ unix.Close(symlinkFD)
+ return nil, lisafs.Statx{}, err
+ }
}
symlinkStat, err := fstatTo(symlinkFD)
@@ -1072,4 +1083,2 @@ func (fd *controlFDLisa) ListXattr(size uint64) (lisafs.StringArray, error) {
- if size > linux.XATTR_LIST_MAX || size == 0 {
- size = linux.XATTR_LIST_MAX
- }
+ size = linux.XATTR_LIST_MAX
bPtr := listXattrBufPool.Get().(*[]byte)
diff --git a/runsc/fsgofer/lisafs_test.go b/runsc/fsgofer/lisafs_test.go
index b10eefc..676960c 100644
--- a/runsc/fsgofer/lisafs_test.go
+++ b/runsc/fsgofer/lisafs_test.go
@@ -61,3 +61,19 @@ func (tester) BindSupported() bool {
func TestFSGofer(t *testing.T) {
testsuite.RunAllLocalFSTests(t, tester{})
}
+
+type denyChownTester struct {
+ tester
+}
+
+func (denyChownTester) NewConnImpl(t *testing.T) lisafs.ConnectionImpl {
+ return fsgofer.NewConnectionImpl(&fsgofer.Config{DenyChown: true})
+}
+
+func (denyChownTester) SetUserGroupIDSupported() bool {
+ return false
+}
+
+func TestFSGoferDenyChown(t *testing.T) {
+ testsuite.RunAllLocalFSTests(t, denyChownTester{})
+}
diff --git a/pkg/sentry/fsimpl/gofer/directfs_inode.go b/pkg/sentry/fsimpl/gofer/directfs_inode.go
index a7c1ab1..d3a0d96 100644
--- a/pkg/sentry/fsimpl/gofer/directfs_inode.go
+++ b/pkg/sentry/fsimpl/gofer/directfs_inode.go
@@ -612,4 +612,2 @@ func (i *directfsInode) listXattr(ctx context.Context, size uint64, d *dentry) ([]string, error) {
- if size > linux.XATTR_LIST_MAX || size == 0 {
- size = linux.XATTR_LIST_MAX
- }
+ size = linux.XATTR_LIST_MAX
bPtr := xattrBufPool.Get().(*[]byte)