-
Notifications
You must be signed in to change notification settings - Fork 10
Add support for mounting other filesystems in user namespaces #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
//go:build linux && cgo | ||
// +build linux,cgo | ||
Comment on lines
+15
to
16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we require go 1.18 (see go.mod), the new directive There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll send a follow up PR, seems sensible. |
||
|
||
package handlers | ||
|
@@ -28,6 +29,7 @@ import ( | |
"github.com/kinvolk/seccompagent/pkg/nsenter" | ||
"github.com/kinvolk/seccompagent/pkg/readarg" | ||
"github.com/kinvolk/seccompagent/pkg/registry" | ||
"github.com/kinvolk/seccompagent/pkg/userns" | ||
) | ||
|
||
var _ = nsenter.RegisterModule("mount", runMountInNamespaces) | ||
|
@@ -37,6 +39,8 @@ type mountModuleParams struct { | |
Source string `json:"source,omitempty"` | ||
Dest string `json:"dest,omitempty"` | ||
Filesystem string `json:"filesystem,omitempty"` | ||
Flags int64 `json:"flags,omitempty"` | ||
Options string `json:"options,omitempty"` | ||
} | ||
|
||
func runMountInNamespaces(param []byte) string { | ||
|
@@ -46,14 +50,14 @@ func runMountInNamespaces(param []byte) string { | |
return fmt.Sprintf("%d", int(unix.ENOSYS)) | ||
} | ||
|
||
err = unix.Mount(params.Source, params.Dest, params.Filesystem, 0, "") | ||
err = unix.Mount(params.Source, params.Dest, params.Filesystem, 0, params.Options) | ||
if err != nil { | ||
return fmt.Sprintf("%d", int(err.(unix.Errno))) | ||
} | ||
return "0" | ||
} | ||
|
||
func Mount(allowedFilesystems map[string]struct{}) registry.HandlerFunc { | ||
func Mount(allowedFilesystems map[string]struct{}, requireUserNamespaceAdmin bool) registry.HandlerFunc { | ||
return func(fd libseccomp.ScmpFd, req *libseccomp.ScmpNotifReq) (result registry.HandlerResult) { | ||
memFile, err := readarg.OpenMem(req.Pid) | ||
if err != nil { | ||
|
@@ -96,12 +100,17 @@ func Mount(allowedFilesystems map[string]struct{}) registry.HandlerFunc { | |
return registry.HandlerResultErrno(unix.EFAULT) | ||
} | ||
|
||
// We don't handle flags, we may want to consider allowing a few. | ||
// This is here so the debug logging makes it possible to see flags used. | ||
flags := int64(req.Data.Args[3]) | ||
|
||
log.WithFields(log.Fields{ | ||
"fd": fd, | ||
"pid": req.Pid, | ||
"source": source, | ||
"dest": dest, | ||
"filesystem": filesystem, | ||
"flags": flags, | ||
}).Debug("Mount") | ||
|
||
if _, ok := allowedFilesystems[filesystem]; !ok { | ||
|
@@ -110,11 +119,70 @@ func Mount(allowedFilesystems map[string]struct{}) registry.HandlerFunc { | |
return registry.HandlerResultContinue() | ||
} | ||
|
||
var options string | ||
if req.Data.Args[4] != 0/* NULL */ && filesystem != "sysfs" { | ||
// Get options, we assume because this is specified in | ||
// allowedFilesystems that the data argument to mount(2) | ||
// is a string so this is safe now. We ignore options for sysfs, as it | ||
// doesn't define options. | ||
rata marked this conversation as resolved.
Show resolved
Hide resolved
|
||
options, err = readarg.ReadString(memFile, int64(req.Data.Args[4])) | ||
if err != nil { | ||
log.WithFields(log.Fields{ | ||
"fd": fd, | ||
"pid": req.Pid, | ||
"arg": 4, | ||
"err": err, | ||
}).Error("Cannot read argument") | ||
return registry.HandlerResultErrno(unix.EFAULT) | ||
} | ||
|
||
// Log this at trace level only as it could have user credentials. | ||
log.WithFields(log.Fields{ | ||
"fd": fd, | ||
"pid": req.Pid, | ||
"source": source, | ||
"dest": dest, | ||
"filesystem": filesystem, | ||
"flags": flags, | ||
"options": options, | ||
}).Trace("Handle mount") | ||
} | ||
|
||
if requireUserNamespaceAdmin { | ||
ok, err := userns.IsPIDAdminCapable(req.Pid) | ||
if err != nil { | ||
log.WithFields(log.Fields{ | ||
"fd": fd, | ||
"pid": req.Pid, | ||
"err": err, | ||
}).Error("Cannot check user namespace capabilities") | ||
return registry.HandlerResultErrno(unix.EFAULT) | ||
} | ||
if !ok { | ||
log.WithFields(log.Fields{ | ||
"fd": fd, | ||
"pid": req.Pid, | ||
}).Info("Mount attempted without CAP_SYS_ADMIN") | ||
return registry.HandlerResultErrno(unix.EPERM) | ||
} | ||
|
||
// Ensure the notification is still valid after checking user namespace capabilities. | ||
if err := libseccomp.NotifIDValid(fd, req.ID); err != nil { | ||
log.WithFields(log.Fields{ | ||
"fd": fd, | ||
"req": req, | ||
"err": err, | ||
}).Debug("Notification no longer valid") | ||
return registry.HandlerResultIntr() | ||
} | ||
} | ||
|
||
params := mountModuleParams{ | ||
Module: "mount", | ||
Source: source, | ||
Dest: dest, | ||
Filesystem: filesystem, | ||
Options: options, | ||
} | ||
|
||
mntns, err := nsenter.OpenNamespace(req.Pid, "mnt") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package userns | ||
|
||
import ( | ||
"fmt" | ||
|
||
"golang.org/x/sys/unix" | ||
"kernel.org/pub/linux/libs/security/libcap/cap" | ||
) | ||
|
||
// IsPIDAdminCapable returns true if the PID is considered an admin of a user | ||
// namespace, that is, it's in either in the init user namespace or one created | ||
// by the host root and has CAP_SYS_ADMIN. The protects against a less | ||
// privileged user either mounting a directory over a tree that gives them more | ||
// access (e.g. /etc/sudoers.d) or hiding files. | ||
func IsPIDAdminCapable(pid uint32) (bool, error) { | ||
// We unfortunately need to reimplement some of the kernel's user namespace logic. | ||
// Our goal is to allow a user with CAP_SYS_ADMIN inside the first user | ||
// namespace to call mount(). If the user nests a user namespace below that, | ||
// we don't want to allow that process to call mount. | ||
|
||
// This is security sensitive code, however TOCTOU isn't a concern in this case | ||
// as this is designed to be used while blocked on a syscall and the kernel | ||
// does not let multi-threaded processes change their user namespace (see | ||
// setns() and unshare() docs). | ||
dgl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fd, err := unix.Open(fmt.Sprintf("/proc/%d/ns/user", pid), unix.O_RDONLY, 0) | ||
if err != nil { | ||
return false, err | ||
} | ||
defer unix.Close(fd) | ||
|
||
uid, err := unix.IoctlGetInt(fd, unix.NS_GET_OWNER_UID) | ||
if err != nil { | ||
return false, err | ||
} | ||
if uid != 0 { | ||
return false, err | ||
} | ||
set, err := cap.GetPID(int(pid)) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
return set.GetFlag(cap.Effective, cap.SYS_ADMIN) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.