The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/6252
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === Includes https://github.com/lxc/lxd/pull/6246
From bdb28a2ce35cb9e45b7ce2a920b8011bf593a5b9 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Thu, 26 Sep 2019 17:42:12 +0100 Subject: [PATCH 1/8] lxd/seccomp: Adds seccomp package - Cleans up existing seccomp code to pass go lint Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/{ => seccomp}/seccomp.go | 256 +++++++++++++++++++++-------------- lxd/seccomp/seccomp_empty.go | 3 + 2 files changed, 161 insertions(+), 98 deletions(-) rename lxd/{ => seccomp}/seccomp.go (78%) create mode 100644 lxd/seccomp/seccomp_empty.go diff --git a/lxd/seccomp.go b/lxd/seccomp/seccomp.go similarity index 78% rename from lxd/seccomp.go rename to lxd/seccomp/seccomp.go index a314e52cfc..182da123a9 100644 --- a/lxd/seccomp.go +++ b/lxd/seccomp/seccomp.go @@ -1,7 +1,7 @@ // +build linux // +build cgo -package main +package seccomp import ( "fmt" @@ -17,11 +17,14 @@ import ( "unsafe" "golang.org/x/sys/unix" + lxc "gopkg.in/lxc/go-lxc.v2" "github.com/lxc/lxd/lxd/device/config" + "github.com/lxc/lxd/lxd/state" "github.com/lxc/lxd/lxd/ucred" "github.com/lxc/lxd/lxd/util" "github.com/lxc/lxd/shared" + "github.com/lxc/lxd/shared/idmap" log "github.com/lxc/lxd/shared/log15" "github.com/lxc/lxd/shared/logger" "github.com/lxc/lxd/shared/netutils" @@ -273,14 +276,14 @@ static void prepare_seccomp_iovec(struct iovec *iov, // #cgo CFLAGS: -std=gnu11 -Wvla import "C" -const LxdSeccompNotifyMknod = C.LXD_SECCOMP_NOTIFY_MKNOD -const LxdSeccompNotifyMknodat = C.LXD_SECCOMP_NOTIFY_MKNODAT -const LxdSeccompNotifySetxattr = C.LXD_SECCOMP_NOTIFY_SETXATTR +const lxdSeccompNotifyMknod = C.LXD_SECCOMP_NOTIFY_MKNOD +const lxdSeccompNotifyMknodat = C.LXD_SECCOMP_NOTIFY_MKNODAT +const lxdSeccompNotifySetxattr = C.LXD_SECCOMP_NOTIFY_SETXATTR -const SECCOMP_HEADER = `2 +const seccompHeader = `2 ` -const DEFAULT_SECCOMP_POLICY = `reject_force_umount # comment this to allow umount -f; not recommended +const defaultSeccompPolicy = `reject_force_umount # comment this to allow umount -f; not recommended [all] kexec_load errno 38 open_by_handle_at errno 38 @@ -289,15 +292,15 @@ finit_module errno 38 delete_module errno 38 ` -const SECCOMP_NOTIFY_MKNOD = `mknod notify [1,8192,SCMP_CMP_MASKED_EQ,61440] +const seccompNotifyMknod = `mknod notify [1,8192,SCMP_CMP_MASKED_EQ,61440] mknod notify [1,24576,SCMP_CMP_MASKED_EQ,61440] mknodat notify [2,8192,SCMP_CMP_MASKED_EQ,61440] mknodat notify [2,24576,SCMP_CMP_MASKED_EQ,61440] ` -const SECCOMP_NOTIFY_SETXATTR = `setxattr notify [3,1,SCMP_CMP_EQ] +const seccompNotifySetxattr = `setxattr notify [3,1,SCMP_CMP_EQ] ` -const COMPAT_BLOCKING_POLICY = `[%s] +const compatBlockingPolicy = `[%s] compat_sys_rt_sigaction errno 38 stub_x32_rt_sigreturn errno 38 compat_sys_ioctl errno 38 @@ -334,13 +337,28 @@ compat_sys_io_submit errno 38 stub_x32_execveat errno 38 ` +// Instance is a seccomp specific instance interface. +type Instance interface { + Name() string + Project() string + ExpandedConfig() map[string]string + IsPrivileged() bool + DaemonState() *state.State + Architecture() int + RootfsPath() string + CurrentIdmap() (*idmap.IdmapSet, error) + InsertSeccompUnixDevice(prefix string, m config.Device, pid int) error +} + var seccompPath = shared.VarPath("security", "seccomp") -func SeccompProfilePath(c container) string { +// ProfilePath returns the seccomp path for the instance. +func ProfilePath(c Instance) string { return path.Join(seccompPath, c.Name()) } -func seccompContainerNeedsPolicy(c container) bool { +// InstanceNeedsPolicy returns whether the instance needs a policy or not. +func InstanceNeedsPolicy(c Instance) bool { config := c.ExpandedConfig() // Check for text keys @@ -385,7 +403,8 @@ func seccompContainerNeedsPolicy(c container) bool { return false } -func seccompContainerNeedsIntercept(c container) (bool, error) { +// InstanceNeedsIntercept returns whether instance needs intercept. +func InstanceNeedsIntercept(c Instance) (bool, error) { // No need if privileged if c.IsPrivileged() { return false, nil @@ -420,7 +439,7 @@ func seccompContainerNeedsIntercept(c container) (bool, error) { return needed, nil } -func seccompGetPolicyContent(c container) (string, error) { +func seccompGetPolicyContent(c Instance) (string, error) { config := c.ExpandedConfig() // Full policy override @@ -430,7 +449,7 @@ func seccompGetPolicyContent(c container) (string, error) { } // Policy header - policy := SECCOMP_HEADER + policy := seccompHeader whitelist := config["security.syscalls.whitelist"] if whitelist != "" { policy += "whitelist\n[all]\n" @@ -438,25 +457,25 @@ func seccompGetPolicyContent(c container) (string, error) { } else { policy += "blacklist\n" - default_, ok := config["security.syscalls.blacklist_default"] - if !ok || shared.IsTrue(default_) { - policy += DEFAULT_SECCOMP_POLICY + defaultFlag, ok := config["security.syscalls.blacklist_default"] + if !ok || shared.IsTrue(defaultFlag) { + policy += defaultSeccompPolicy } } // Syscall interception - ok, err := seccompContainerNeedsIntercept(c) + ok, err := InstanceNeedsIntercept(c) if err != nil { return "", err } if ok { if shared.IsTrue(config["security.syscalls.intercept.mknod"]) { - policy += SECCOMP_NOTIFY_MKNOD + policy += seccompNotifyMknod } if shared.IsTrue(config["security.syscalls.intercept.setxattr"]) { - policy += SECCOMP_NOTIFY_SETXATTR + policy += seccompNotifySetxattr } } @@ -471,7 +490,7 @@ func seccompGetPolicyContent(c container) (string, error) { if err != nil { return "", err } - policy += fmt.Sprintf(COMPAT_BLOCKING_POLICY, arch) + policy += fmt.Sprintf(compatBlockingPolicy, arch) } blacklist := config["security.syscalls.blacklist"] @@ -482,14 +501,15 @@ func seccompGetPolicyContent(c container) (string, error) { return policy, nil } -func SeccompCreateProfile(c container) error { +// CreateProfile creates a seccomp profile. +func CreateProfile(c Instance) error { /* Unlike apparmor, there is no way to "cache" profiles, and profiles * are automatically unloaded when a task dies. Thus, we don't need to * unload them when a container stops, and we don't have to worry about * the mtime on the file for any compiler purpose, so let's just write * out the profile. */ - if !seccompContainerNeedsPolicy(c) { + if !InstanceNeedsPolicy(c) { return nil } @@ -502,23 +522,26 @@ func SeccompCreateProfile(c container) error { return err } - return ioutil.WriteFile(SeccompProfilePath(c), []byte(profile), 0600) + return ioutil.WriteFile(ProfilePath(c), []byte(profile), 0600) } -func SeccompDeleteProfile(c container) { +// DeleteProfile removes a seccomp profile. +func DeleteProfile(c Instance) { /* similar to AppArmor, if we've never started this container, the * delete can fail and that's ok. */ - os.Remove(SeccompProfilePath(c)) + os.Remove(ProfilePath(c)) } -type SeccompServer struct { - d *Daemon +// Server defines a seccomp server. +type Server struct { + s *state.State path string l net.Listener } -type SeccompIovec struct { +// Iovec defines an iovec to move data between kernel and userspace. +type Iovec struct { ucred *ucred.UCred memFd int procFd int @@ -529,30 +552,31 @@ type SeccompIovec struct { iov *C.struct_iovec } -func NewSeccompIovec(ucred *ucred.UCred) *SeccompIovec { - msg_ptr := C.malloc(C.sizeof_struct_seccomp_notify_proxy_msg) - msg := (*C.struct_seccomp_notify_proxy_msg)(msg_ptr) - C.memset(msg_ptr, 0, C.sizeof_struct_seccomp_notify_proxy_msg) +// NewSeccompIovec creates a new seccomp iovec. +func NewSeccompIovec(ucred *ucred.UCred) *Iovec { + msgPtr := C.malloc(C.sizeof_struct_seccomp_notify_proxy_msg) + msg := (*C.struct_seccomp_notify_proxy_msg)(msgPtr) + C.memset(msgPtr, 0, C.sizeof_struct_seccomp_notify_proxy_msg) - req_ptr := C.malloc(C.sizeof_struct_seccomp_notif) - req := (*C.struct_seccomp_notif)(req_ptr) - C.memset(req_ptr, 0, C.sizeof_struct_seccomp_notif) + regPtr := C.malloc(C.sizeof_struct_seccomp_notif) + req := (*C.struct_seccomp_notif)(regPtr) + C.memset(regPtr, 0, C.sizeof_struct_seccomp_notif) - resp_ptr := C.malloc(C.sizeof_struct_seccomp_notif_resp) - resp := (*C.struct_seccomp_notif_resp)(resp_ptr) - C.memset(resp_ptr, 0, C.sizeof_struct_seccomp_notif_resp) + respPtr := C.malloc(C.sizeof_struct_seccomp_notif_resp) + resp := (*C.struct_seccomp_notif_resp)(respPtr) + C.memset(respPtr, 0, C.sizeof_struct_seccomp_notif_resp) - cookie_ptr := C.malloc(64 * C.sizeof_char) - cookie := (*C.char)(cookie_ptr) - C.memset(cookie_ptr, 0, 64*C.sizeof_char) + cookiePtr := C.malloc(64 * C.sizeof_char) + cookie := (*C.char)(cookiePtr) + C.memset(cookiePtr, 0, 64*C.sizeof_char) - iov_unsafe_ptr := C.malloc(4 * C.sizeof_struct_iovec) - iov := (*C.struct_iovec)(iov_unsafe_ptr) - C.memset(iov_unsafe_ptr, 0, 4*C.sizeof_struct_iovec) + iovUnsafePtr := C.malloc(4 * C.sizeof_struct_iovec) + iov := (*C.struct_iovec)(iovUnsafePtr) + C.memset(iovUnsafePtr, 0, 4*C.sizeof_struct_iovec) C.prepare_seccomp_iovec(iov, msg, req, resp, cookie) - return &SeccompIovec{ + return &Iovec{ memFd: -1, procFd: -1, msg: msg, @@ -564,7 +588,8 @@ func NewSeccompIovec(ucred *ucred.UCred) *SeccompIovec { } } -func (siov *SeccompIovec) PutSeccompIovec() { +// PutSeccompIovec puts a seccomp iovec. +func (siov *Iovec) PutSeccompIovec() { if siov.memFd >= 0 { unix.Close(siov.memFd) } @@ -578,7 +603,8 @@ func (siov *SeccompIovec) PutSeccompIovec() { C.free(unsafe.Pointer(siov.iov)) } -func (siov *SeccompIovec) ReceiveSeccompIovec(fd int) (uint64, error) { +// ReceiveSeccompIovec receives a seccomp iovec. +func (siov *Iovec) ReceiveSeccompIovec(fd int) (uint64, error) { bytes, fds, err := netutils.AbstractUnixReceiveFdData(fd, 2, unsafe.Pointer(siov.iov), 4) if err != nil || err == io.EOF { return 0, err @@ -594,7 +620,8 @@ func (siov *SeccompIovec) ReceiveSeccompIovec(fd int) (uint64, error) { return bytes, nil } -func (siov *SeccompIovec) IsValidSeccompIovec(size uint64) bool { +// IsValidSeccompIovec checks whether a seccomp iovec is valid. +func (siov *Iovec) IsValidSeccompIovec(size uint64) bool { if size < uint64(C.SECCOMP_MSG_SIZE_MIN) { logger.Warnf("Disconnected from seccomp socket after incomplete receive") return false @@ -626,7 +653,8 @@ func (siov *SeccompIovec) IsValidSeccompIovec(size uint64) bool { return true } -func (siov *SeccompIovec) SendSeccompIovec(fd int, errno int) error { +// SendSeccompIovec sends seccomp iovec. +func (siov *Iovec) SendSeccompIovec(fd int, errno int) error { C.seccomp_notify_update_response(siov.resp, C.int(errno)) msghdr := C.struct_msghdr{} @@ -651,7 +679,8 @@ retry: return nil } -func NewSeccompServer(d *Daemon, path string) (*SeccompServer, error) { +// NewSeccompServer creates a new seccomp server. +func NewSeccompServer(s *state.State, path string, findPID func(pid int32, state *state.State) (Instance, error)) (*Server, error) { ret := C.seccomp_notify_get_sizes(&C.expected_sizes) if ret < 0 { return nil, fmt.Errorf("Failed to query kernel for seccomp notifier sizes") @@ -678,8 +707,8 @@ func NewSeccompServer(d *Daemon, path string) (*SeccompServer, error) { } // Start the server - s := SeccompServer{ - d: d, + server := Server{ + s: s, path: path, l: l, } @@ -715,101 +744,103 @@ func NewSeccompServer(d *Daemon, path string) (*SeccompServer, error) { } if siov.IsValidSeccompIovec(bytes) { - go s.Handler(int(unixFile.Fd()), siov) + go server.handler(int(unixFile.Fd()), siov, findPID) } else { - go s.InvalidHandler(int(unixFile.Fd()), siov) + go server.InvalidHandler(int(unixFile.Fd()), siov) } } }() } }() - return &s, nil + return &server, nil } -func taskIds(pid int) (error, int64, int64, int64, int64) { +// TaskIDs returns the task IDs for a process. +func TaskIDs(pid int) (int64, int64, int64, int64, error) { status, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) if err != nil { - return err, -1, -1, -1, -1 + return -1, -1, -1, -1, err } - reUid := regexp.MustCompile("Uid:\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)") - reGid := regexp.MustCompile("Gid:\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)") - var gid int64 = -1 - var uid int64 = -1 - var fsgid int64 = -1 - var fsuid int64 = -1 - uidFound := false - gidFound := false + reUID := regexp.MustCompile("Uid:\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)") + reGID := regexp.MustCompile("Gid:\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)\\s*([0-9]*)") + var UID int64 = -1 + var GID int64 = -1 + var fsUID int64 = -1 + var fsGID int64 = -1 + UIDFound := false + GIDFound := false for _, line := range strings.Split(string(status), "\n") { - if uidFound && gidFound { + if UIDFound && GIDFound { break } - if !uidFound { - m := reUid.FindStringSubmatch(line) + if !UIDFound { + m := reUID.FindStringSubmatch(line) if m != nil && len(m) > 2 { // effective uid result, err := strconv.ParseInt(m[2], 10, 64) if err != nil { - return err, -1, -1, -1, -1 + return -1, -1, -1, -1, err } - uid = result - uidFound = true + UID = result + UIDFound = true } if m != nil && len(m) > 4 { // fsuid result, err := strconv.ParseInt(m[4], 10, 64) if err != nil { - return err, -1, -1, -1, -1 + return -1, -1, -1, -1, err } - fsuid = result + fsUID = result } continue } - if !gidFound { - m := reGid.FindStringSubmatch(line) + if !GIDFound { + m := reGID.FindStringSubmatch(line) if m != nil && len(m) > 2 { // effective gid result, err := strconv.ParseInt(m[2], 10, 64) if err != nil { - return err, -1, -1, -1, -1 + return -1, -1, -1, -1, err } - gid = result - gidFound = true + GID = result + GIDFound = true } if m != nil && len(m) > 4 { // fsgid result, err := strconv.ParseInt(m[4], 10, 64) if err != nil { - return err, -1, -1, -1, -1 + return -1, -1, -1, -1, err } - fsgid = result + fsGID = result } continue } } - return nil, uid, gid, fsuid, fsgid + return UID, GID, fsUID, fsGID, nil } -func CallForkmknod(c container, dev config.Device, requestPID int) int { +// CallForkmknod executes fork mknod. +func CallForkmknod(c Instance, dev config.Device, requestPID int) int { rootLink := fmt.Sprintf("/proc/%d/root", requestPID) rootPath, err := os.Readlink(rootLink) if err != nil { return int(-C.EPERM) } - err, uid, gid, fsuid, fsgid := taskIds(requestPID) + uid, gid, fsuid, fsgid, err := TaskIDs(requestPID) if err != nil { return int(-C.EPERM) } @@ -846,12 +877,13 @@ func CallForkmknod(c container, dev config.Device, requestPID int) int { // InvalidHandler sends a dummy message to LXC. LXC will notice the short write // and send a default message to the kernel thereby avoiding a 30s hang. -func (s *SeccompServer) InvalidHandler(fd int, siov *SeccompIovec) { +func (s *Server) InvalidHandler(fd int, siov *Iovec) { msghdr := C.struct_msghdr{} C.sendmsg(C.int(fd), &msghdr, C.MSG_NOSIGNAL) siov.PutSeccompIovec() } +// MknodArgs arguments for mknod. type MknodArgs struct { cMode C.mode_t cDev C.dev_t @@ -859,7 +891,7 @@ type MknodArgs struct { path string } -func (s *SeccompServer) doDeviceSyscall(c container, args *MknodArgs, siov *SeccompIovec) int { +func (s *Server) doDeviceSyscall(c Instance, args *MknodArgs, siov *Iovec) int { dev := config.Device{} dev["type"] = "unix-char" dev["mode"] = fmt.Sprintf("%#o", args.cMode) @@ -883,7 +915,8 @@ func (s *SeccompServer) doDeviceSyscall(c container, args *MknodArgs, siov *Secc return 0 } -func (s *SeccompServer) HandleMknodSyscall(c container, siov *SeccompIovec) int { +// HandleMknodSyscall handles a mknod syscall. +func (s *Server) HandleMknodSyscall(c Instance, siov *Iovec) int { logger.Debug("Handling mknod syscall", log.Ctx{"container": c.Name(), "project": c.Project(), @@ -916,7 +949,8 @@ func (s *SeccompServer) HandleMknodSyscall(c container, siov *SeccompIovec) int return s.doDeviceSyscall(c, &args, siov) } -func (s *SeccompServer) HandleMknodatSyscall(c container, siov *SeccompIovec) int { +// HandleMknodatSyscall handles a mknodat syscall. +func (s *Server) HandleMknodatSyscall(c Instance, siov *Iovec) int { logger.Debug("Handling mknodat syscall", log.Ctx{"container": c.Name(), "project": c.Project(), @@ -956,6 +990,7 @@ func (s *SeccompServer) HandleMknodatSyscall(c container, siov *SeccompIovec) in return s.doDeviceSyscall(c, &args, siov) } +// SetxattrArgs arguments for setxattr. type SetxattrArgs struct { nsuid int64 nsgid int64 @@ -969,7 +1004,8 @@ type SetxattrArgs struct { flags C.int } -func (s *SeccompServer) HandleSetxattrSyscall(c container, siov *SeccompIovec) int { +// HandleSetxattrSyscall handles setxattr syscalls. +func (s *Server) HandleSetxattrSyscall(c Instance, siov *Iovec) int { logger.Debug("Handling setxattr syscall", log.Ctx{"container": c.Name(), "project": c.Project(), @@ -982,7 +1018,7 @@ func (s *SeccompServer) HandleSetxattrSyscall(c container, siov *SeccompIovec) i args := SetxattrArgs{} args.pid = int(siov.req.pid) - err, uid, gid, fsuid, fsgid := taskIds(args.pid) + uid, gid, fsuid, fsgid, err := TaskIDs(args.pid) if err != nil { return int(-C.EPERM) } @@ -1057,30 +1093,30 @@ func (s *SeccompServer) HandleSetxattrSyscall(c container, siov *SeccompIovec) i return 0 } -func (s *SeccompServer) HandleSyscall(c container, siov *SeccompIovec) int { +func (s *Server) handleSyscall(c Instance, siov *Iovec) int { switch int(C.seccomp_notify_get_syscall(siov.req, siov.resp)) { - case LxdSeccompNotifyMknod: + case lxdSeccompNotifyMknod: return s.HandleMknodSyscall(c, siov) - case LxdSeccompNotifyMknodat: + case lxdSeccompNotifyMknodat: return s.HandleMknodatSyscall(c, siov) - case LxdSeccompNotifySetxattr: + case lxdSeccompNotifySetxattr: return s.HandleSetxattrSyscall(c, siov) } return int(-C.EINVAL) } -func (s *SeccompServer) Handler(fd int, siov *SeccompIovec) error { +func (s *Server) handler(fd int, siov *Iovec, findPID func(pid int32, state *state.State) (Instance, error)) error { defer siov.PutSeccompIovec() - c, err := findContainerForPid(int32(siov.msg.monitor_pid), s.d) + c, err := findPID(int32(siov.msg.monitor_pid), s.s) if err != nil { siov.SendSeccompIovec(fd, int(-C.EPERM)) logger.Errorf("Failed to find container for monitor %d", siov.msg.monitor_pid) return err } - errno := s.HandleSyscall(c, siov) + errno := s.handleSyscall(c, siov) err = siov.SendSeccompIovec(fd, errno) if err != nil { @@ -1090,7 +1126,31 @@ func (s *SeccompServer) Handler(fd int, siov *SeccompIovec) error { return nil } -func (s *SeccompServer) Stop() error { +// Stop stops a seccomp server. +func (s *Server) Stop() error { os.Remove(s.path) return s.l.Close() } + +func lxcSupportSeccompNotify(state *state.State) bool { + if !state.OS.SeccompListener { + return false + } + + if !state.OS.LXCFeatures["seccomp_notify"] { + return false + } + + c, err := lxc.NewContainer("test-seccomp", state.OS.LxcPath) + if err != nil { + return false + } + + err = c.SetConfigItem("lxc.seccomp.notify.proxy", fmt.Sprintf("unix:%s", shared.VarPath("seccomp.socket"))) + if err != nil { + return false + } + + c.Release() + return true +} diff --git a/lxd/seccomp/seccomp_empty.go b/lxd/seccomp/seccomp_empty.go new file mode 100644 index 0000000000..13b55d66db --- /dev/null +++ b/lxd/seccomp/seccomp_empty.go @@ -0,0 +1,3 @@ +package seccomp + +// Placeholder for non-linux systems. From 4bb9d795b037651abc93b3c14848c5252e494425 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Thu, 26 Sep 2019 17:43:32 +0100 Subject: [PATCH 2/8] lxd/daemon: Updates to use seccomp package - Updates to use seccomp package. - Uses workaround to expose findContainerForPid to seccomp package. - Adds call to seccomp server Stop() on LXD shutdown. Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/daemon.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lxd/daemon.go b/lxd/daemon.go index b483125ae8..dc50d1adaf 100644 --- a/lxd/daemon.go +++ b/lxd/daemon.go @@ -37,6 +37,7 @@ import ( "github.com/lxc/lxd/lxd/node" "github.com/lxc/lxd/lxd/rbac" "github.com/lxc/lxd/lxd/response" + "github.com/lxc/lxd/lxd/seccomp" "github.com/lxc/lxd/lxd/state" "github.com/lxc/lxd/lxd/sys" "github.com/lxc/lxd/lxd/task" @@ -73,7 +74,7 @@ type Daemon struct { config *DaemonConfig endpoints *endpoints.Endpoints gateway *cluster.Gateway - seccomp *SeccompServer + seccomp *seccomp.Server proxy func(req *http.Request) (*url.URL, error) @@ -877,7 +878,9 @@ func (d *Daemon) init() error { // Setup seccomp handler if d.os.SeccompListener { - seccompServer, err := NewSeccompServer(d, shared.VarPath("seccomp.socket")) + seccompServer, err := seccomp.NewSeccompServer(d.State(), shared.VarPath("seccomp.socket"), func(pid int32, state *state.State) (seccomp.Instance, error) { + return findContainerForPid(pid, state) + }) if err != nil { return err } @@ -1091,6 +1094,10 @@ func (d *Daemon) Stop() error { "Not unmounting temporary filesystems (containers are still running)") } + if d.seccomp != nil { + trackError(d.seccomp.Stop()) + } + var err error if n := len(errs); n > 0 { format := "%v" From 9ff4fa52e407d761fedb02b1ae99b36ba35c3917 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Thu, 26 Sep 2019 17:44:52 +0100 Subject: [PATCH 3/8] lxd/container/lxc: Removes lxcSupportSeccompNotify - This has moved to seccomp package. Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/container_lxc.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go index 0f8fb885a3..a82fafd239 100644 --- a/lxd/container_lxc.go +++ b/lxd/container_lxc.go @@ -206,29 +206,6 @@ func lxcParseRawLXC(line string) (string, string, error) { return key, val, nil } -func lxcSupportSeccompNotify(state *state.State) bool { - if !state.OS.SeccompListener { - return false - } - - if !state.OS.LXCFeatures["seccomp_notify"] { - return false - } - - c, err := lxc.NewContainer("test-seccomp", state.OS.LxcPath) - if err != nil { - return false - } - - err = c.SetConfigItem("lxc.seccomp.notify.proxy", fmt.Sprintf("unix:%s", shared.VarPath("seccomp.socket"))) - if err != nil { - return false - } - - c.Release() - return true -} - func lxcValidConfig(rawLxc string) error { for _, line := range strings.Split(rawLxc, "\n") { key, _, err := lxcParseRawLXC(line) From 86fc392c3097c8f4d4904534a4d0f711e7aa4553 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Thu, 26 Sep 2019 17:45:20 +0100 Subject: [PATCH 4/8] lxd: Updates to use seccomp package Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/container_lxc.go | 13 +++++++------ lxd/devlxd.go | 9 +++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go index a82fafd239..cc83a3a7cb 100644 --- a/lxd/container_lxc.go +++ b/lxd/container_lxc.go @@ -34,6 +34,7 @@ import ( "github.com/lxc/lxd/lxd/instance/instancetype" "github.com/lxc/lxd/lxd/maas" "github.com/lxc/lxd/lxd/project" + "github.com/lxc/lxd/lxd/seccomp" "github.com/lxc/lxd/lxd/state" driver "github.com/lxc/lxd/lxd/storage" "github.com/lxc/lxd/lxd/template" @@ -1257,15 +1258,15 @@ func (c *containerLXC) initLXC(config bool) error { } // Setup Seccomp if necessary - if seccompContainerNeedsPolicy(c) { - err = lxcSetConfigItem(cc, "lxc.seccomp.profile", SeccompProfilePath(c)) + if seccomp.InstanceNeedsPolicy(c) { + err = lxcSetConfigItem(cc, "lxc.seccomp.profile", seccomp.ProfilePath(c)) if err != nil { return err } // Setup notification socket // System requirement errors are handled during policy generation instead of here - ok, err := seccompContainerNeedsIntercept(c) + ok, err := seccomp.InstanceNeedsIntercept(c) if err == nil && ok { err = lxcSetConfigItem(cc, "lxc.seccomp.notify.proxy", fmt.Sprintf("unix:%s", shared.VarPath("seccomp.socket"))) if err != nil { @@ -2260,7 +2261,7 @@ func (c *containerLXC) startCommon() (string, []func() error, error) { } // Generate the Seccomp profile - if err := SeccompCreateProfile(c); err != nil { + if err := seccomp.CreateProfile(c); err != nil { return "", postStartHooks, err } @@ -3606,7 +3607,7 @@ func (c *containerLXC) cleanup() { // Remove the security profiles AADeleteProfile(c) - SeccompDeleteProfile(c) + seccomp.DeleteProfile(c) // Remove the devices path os.Remove(c.DevicesPath()) @@ -6360,7 +6361,7 @@ func (c *containerLXC) InsertSeccompUnixDevice(prefix string, m config.Device, p return err } - err, uid, gid, _, _ := taskIds(pid) + uid, gid, _, _, err := seccomp.TaskIDs(pid) if err != nil { return err } diff --git a/lxd/devlxd.go b/lxd/devlxd.go index 060eafef4e..f5f2baab2d 100644 --- a/lxd/devlxd.go +++ b/lxd/devlxd.go @@ -20,6 +20,7 @@ import ( "github.com/lxc/lxd/lxd/events" "github.com/lxc/lxd/lxd/instance/instancetype" + "github.com/lxc/lxd/lxd/state" "github.com/lxc/lxd/lxd/ucred" "github.com/lxc/lxd/lxd/util" "github.com/lxc/lxd/shared" @@ -217,7 +218,7 @@ func hoistReq(f func(*Daemon, container, http.ResponseWriter, *http.Request) *de return } - c, err := findContainerForPid(cred.PID, d) + c, err := findContainerForPid(cred.PID, d.State()) if err != nil { http.Error(w, err.Error(), 500) return @@ -352,7 +353,7 @@ func extractUnderlyingConn(w http.ResponseWriter) *net.UnixConn { var pidNotInContainerErr = fmt.Errorf("pid not in container?") -func findContainerForPid(pid int32, d *Daemon) (container, error) { +func findContainerForPid(pid int32, s *state.State) (container, error) { /* * Try and figure out which container a pid is in. There is probably a * better way to do this. Based on rharper's initial performance @@ -390,7 +391,7 @@ func findContainerForPid(pid int32, d *Daemon) (container, error) { name = fields[1] } - inst, err := instanceLoadByProjectAndName(d.State(), project, name) + inst, err := instanceLoadByProjectAndName(s, project, name) if err != nil { return nil, err } @@ -428,7 +429,7 @@ func findContainerForPid(pid int32, d *Daemon) (container, error) { return nil, err } - instances, err := instanceLoadNodeAll(d.State()) + instances, err := instanceLoadNodeAll(s) if err != nil { return nil, err } From 96f27feabbba9320c76279d7a2d56c3082b6060e Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Thu, 26 Sep 2019 17:46:24 +0100 Subject: [PATCH 5/8] test: Adds seccomp package to static analysis Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- test/suites/static_analysis.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test/suites/static_analysis.sh b/test/suites/static_analysis.sh index e7ec6f686e..db632955b6 100644 --- a/test/suites/static_analysis.sh +++ b/test/suites/static_analysis.sh @@ -94,6 +94,7 @@ test_static_analysis() { golint -set_exit_status lxd/iptables/... golint -set_exit_status lxd/instance/... golint -set_exit_status lxd/unixcred/... + golint -set_exit_status lxd/seccomp/... golint -set_exit_status shared/api/ golint -set_exit_status shared/cancel/ From be95d6f9b6dad1ad7f54bbfad28d16e976aed607 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Thu, 26 Sep 2019 17:47:51 +0100 Subject: [PATCH 6/8] test: Fixes static analysis for ucred package Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- test/suites/static_analysis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/suites/static_analysis.sh b/test/suites/static_analysis.sh index db632955b6..29333c2220 100644 --- a/test/suites/static_analysis.sh +++ b/test/suites/static_analysis.sh @@ -93,7 +93,7 @@ test_static_analysis() { golint -set_exit_status lxd/dnsmasq/... golint -set_exit_status lxd/iptables/... golint -set_exit_status lxd/instance/... - golint -set_exit_status lxd/unixcred/... + golint -set_exit_status lxd/ucred/... golint -set_exit_status lxd/seccomp/... golint -set_exit_status shared/api/ From fbfdc5253ca74ddf2c79557445e0f4defef6dab0 Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Fri, 27 Sep 2019 10:26:40 +0100 Subject: [PATCH 7/8] lxd/apparmor: Moves apparmor into its own package - Unexports functions and variables where possible. - Makes go-lint happy. Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/{ => apparmor}/apparmor.go | 96 +++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 43 deletions(-) rename lxd/{ => apparmor}/apparmor.go (92%) diff --git a/lxd/apparmor.go b/lxd/apparmor/apparmor.go similarity index 92% rename from lxd/apparmor.go rename to lxd/apparmor/apparmor.go index 46037ff32f..91e413dcf0 100644 --- a/lxd/apparmor.go +++ b/lxd/apparmor/apparmor.go @@ -1,4 +1,4 @@ -package main +package apparmor import ( "crypto/sha256" @@ -10,6 +10,7 @@ import ( "strings" "github.com/lxc/lxd/lxd/project" + "github.com/lxc/lxd/lxd/state" "github.com/lxc/lxd/shared" "github.com/lxc/lxd/shared/logger" @@ -17,14 +18,14 @@ import ( ) const ( - APPARMOR_CMD_LOAD = "r" - APPARMOR_CMD_UNLOAD = "R" - APPARMOR_CMD_PARSE = "Q" + cmdLoad = "r" + cmdUnload = "R" + cmdParse = "Q" ) var aaPath = shared.VarPath("security", "apparmor") -const AA_PROFILE_BASE = ` +const profileBase = ` ### Base profile capability, dbus, @@ -417,7 +418,7 @@ const AA_PROFILE_BASE = ` deny /sys/fs?*{,/**} wklx, ` -const AA_PROFILE_NESTING = ` +const profileNesting = ` pivot_root, # Allow sending signals and tracing children namespaces @@ -444,7 +445,7 @@ const AA_PROFILE_NESTING = ` mount, ` -const AA_PROFILE_UNPRIVILEGED = ` +const profileUnprivileged = ` pivot_root, # Allow modifying mount propagation @@ -481,6 +482,15 @@ const AA_PROFILE_UNPRIVILEGED = ` mount options=(ro,remount) /**, ` +type instance interface { + Project() string + Name() string + IsNesting() bool + DaemonState() *state.State + IsPrivileged() bool + ExpandedConfig() map[string]string +} + func mkApparmorName(name string) string { if len(name)+7 >= 253 { hash := sha256.New() @@ -491,7 +501,8 @@ func mkApparmorName(name string) string { return name } -func AANamespace(c container) string { +// Namespace returns the instance's apparmor namespace. +func Namespace(c instance) string { /* / is not allowed in apparmor namespace names; let's also trim the * leading / so it doesn't look like "-var-lib-lxd" */ @@ -501,23 +512,23 @@ func AANamespace(c container) string { return fmt.Sprintf("lxd-%s_<%s>", name, lxddir) } -func AAProfileFull(c container) string { +// ProfileFull returns the instance's apparmor profile. +func ProfileFull(c instance) string { lxddir := shared.VarPath("") lxddir = mkApparmorName(lxddir) name := project.Prefix(c.Project(), c.Name()) return fmt.Sprintf("lxd-%s_<%s>", name, lxddir) } -func AAProfileShort(c container) string { +func profileShort(c instance) string { name := project.Prefix(c.Project(), c.Name()) return fmt.Sprintf("lxd-%s", name) } -// getProfileContent generates the apparmor profile template from the given -// container. This includes the stock lxc includes as well as stuff from -// raw.apparmor. -func getAAProfileContent(c container) string { - profile := strings.TrimLeft(AA_PROFILE_BASE, "\n") +// getProfileContent generates the apparmor profile template from the given container. +// This includes the stock lxc includes as well as stuff from raw.apparmor. +func getAAProfileContent(c instance) string { + profile := strings.TrimLeft(profileBase, "\n") // Apply new features if aaParserSupports("unix") { @@ -567,8 +578,8 @@ func getAAProfileContent(c container) string { deny /sys/kernel/security?*{,/**} wklx, deny /sys/kernel?*{,/**} wklx, ` - profile += fmt.Sprintf(" change_profile -> \":%s:*\",\n", AANamespace(c)) - profile += fmt.Sprintf(" change_profile -> \":%s://*\",\n", AANamespace(c)) + profile += fmt.Sprintf(" change_profile -> \":%s:*\",\n", Namespace(c)) + profile += fmt.Sprintf(" change_profile -> \":%s://*\",\n", Namespace(c)) } else { profile += "\n ### Feature: apparmor stacking (not present)\n" profile += " deny /sys/k*{,/**} wklx,\n" @@ -577,16 +588,16 @@ func getAAProfileContent(c container) string { if c.IsNesting() { // Apply nesting bits profile += "\n ### Configuration: nesting\n" - profile += strings.TrimLeft(AA_PROFILE_NESTING, "\n") + profile += strings.TrimLeft(profileNesting, "\n") if !state.OS.AppArmorStacking || state.OS.AppArmorStacked { - profile += fmt.Sprintf(" change_profile -> \"%s\",\n", AAProfileFull(c)) + profile += fmt.Sprintf(" change_profile -> \"%s\",\n", ProfileFull(c)) } } if !c.IsPrivileged() || state.OS.RunningInUserNS { // Apply unprivileged bits profile += "\n ### Configuration: unprivileged containers\n" - profile += strings.TrimLeft(AA_PROFILE_UNPRIVILEGED, "\n") + profile += strings.TrimLeft(profileUnprivileged, "\n") } // Append raw.apparmor @@ -602,10 +613,10 @@ func getAAProfileContent(c container) string { profile "%s" flags=(attach_disconnected,mediate_deleted) { %s } -`, AAProfileFull(c), strings.Trim(profile, "\n")) +`, ProfileFull(c), strings.Trim(profile, "\n")) } -func runApparmor(command string, c container) error { +func runApparmor(command string, c instance) error { state := c.DaemonState() if !state.OS.AppArmorAvailable { return nil @@ -614,7 +625,7 @@ func runApparmor(command string, c container) error { output, err := shared.RunCommand("apparmor_parser", []string{ fmt.Sprintf("-%sWL", command), path.Join(aaPath, "cache"), - path.Join(aaPath, "profiles", AAProfileShort(c)), + path.Join(aaPath, "profiles", profileShort(c)), }...) if err != nil { @@ -625,7 +636,7 @@ func runApparmor(command string, c container) error { return err } -func getAACacheDir() string { +func getCacheDir() string { basePath := path.Join(aaPath, "cache") major, minor, _, err := getAAParserVersion() @@ -646,7 +657,7 @@ func getAACacheDir() string { return strings.TrimSpace(output) } -func mkApparmorNamespace(c container, namespace string) error { +func mkApparmorNamespace(c instance, namespace string) error { state := c.DaemonState() if !state.OS.AppArmorStacking || state.OS.AppArmorStacked { return nil @@ -660,15 +671,14 @@ func mkApparmorNamespace(c container, namespace string) error { return nil } -// Ensure that the container's policy is loaded into the kernel so the -// container can boot. -func AALoadProfile(c container) error { +// LoadProfile ensures that the instances's policy is loaded into the kernel so the it can boot. +func LoadProfile(c instance) error { state := c.DaemonState() if !state.OS.AppArmorAdmin { return nil } - if err := mkApparmorNamespace(c, AANamespace(c)); err != nil { + if err := mkApparmorNamespace(c, Namespace(c)); err != nil { return err } @@ -683,7 +693,7 @@ func AALoadProfile(c container) error { * version out so that the new changes are reflected and we definitely * force a recompile. */ - profile := path.Join(aaPath, "profiles", AAProfileShort(c)) + profile := path.Join(aaPath, "profiles", profileShort(c)) content, err := ioutil.ReadFile(profile) if err != nil && !os.IsNotExist(err) { return err @@ -705,39 +715,39 @@ func AALoadProfile(c container) error { } } - return runApparmor(APPARMOR_CMD_LOAD, c) + return runApparmor(cmdLoad, c) } -// Ensure that the container's policy namespace is unloaded to free kernel -// memory. This does not delete the policy from disk or cache. -func AADestroy(c container) error { +// Destroy ensures that the instances's policy namespace is unloaded to free kernel memory. +// This does not delete the policy from disk or cache. +func Destroy(c instance) error { state := c.DaemonState() if !state.OS.AppArmorAdmin { return nil } if state.OS.AppArmorStacking && !state.OS.AppArmorStacked { - p := path.Join("/sys/kernel/security/apparmor/policy/namespaces", AANamespace(c)) + p := path.Join("/sys/kernel/security/apparmor/policy/namespaces", Namespace(c)) if err := os.Remove(p); err != nil { logger.Error("Error removing apparmor namespace", log.Ctx{"err": err, "ns": p}) } } - return runApparmor(APPARMOR_CMD_UNLOAD, c) + return runApparmor(cmdUnload, c) } -// Parse the profile without loading it into the kernel. -func AAParseProfile(c container) error { +// ParseProfile parses the profile without loading it into the kernel. +func ParseProfile(c instance) error { state := c.DaemonState() if !state.OS.AppArmorAvailable { return nil } - return runApparmor(APPARMOR_CMD_PARSE, c) + return runApparmor(cmdParse, c) } -// Delete the policy from cache/disk. -func AADeleteProfile(c container) { +// DeleteProfile removes the policy from cache/disk. +func DeleteProfile(c instance) { state := c.DaemonState() if !state.OS.AppArmorAdmin { return @@ -746,8 +756,8 @@ func AADeleteProfile(c container) { /* It's ok if these deletes fail: if the container was never started, * we'll have never written a profile or cached it. */ - os.Remove(path.Join(getAACacheDir(), AAProfileShort(c))) - os.Remove(path.Join(aaPath, "profiles", AAProfileShort(c))) + os.Remove(path.Join(getCacheDir(), profileShort(c))) + os.Remove(path.Join(aaPath, "profiles", profileShort(c))) } func aaParserSupports(feature string) bool { From 21efa047a29e86ff4edc9e7ee52d248202ded9af Mon Sep 17 00:00:00 2001 From: Thomas Parrott <thomas.parr...@canonical.com> Date: Fri, 27 Sep 2019 10:27:18 +0100 Subject: [PATCH 8/8] lxd/container/lxc: Updates use of apparmor package Signed-off-by: Thomas Parrott <thomas.parr...@canonical.com> --- lxd/container_lxc.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go index cc83a3a7cb..5e5cbb84fc 100644 --- a/lxd/container_lxc.go +++ b/lxd/container_lxc.go @@ -25,6 +25,7 @@ import ( lxc "gopkg.in/lxc/go-lxc.v2" yaml "gopkg.in/yaml.v2" + "github.com/lxc/lxd/lxd/apparmor" "github.com/lxc/lxd/lxd/cluster" "github.com/lxc/lxd/lxd/db" "github.com/lxc/lxd/lxd/db/query" @@ -1237,7 +1238,7 @@ func (c *containerLXC) initLXC(config bool) error { } } else { // If not currently confined, use the container's profile - profile := AAProfileFull(c) + profile := apparmor.ProfileFull(c) /* In the nesting case, we want to enable the inside * LXD to load its profile. Unprivileged containers can @@ -1247,7 +1248,7 @@ func (c *containerLXC) initLXC(config bool) error { * profile. */ if c.state.OS.AppArmorStacking && !c.state.OS.AppArmorStacked { - profile = fmt.Sprintf("%s//&:%s:", profile, AANamespace(c)) + profile = fmt.Sprintf("%s//&:%s:", profile, apparmor.Namespace(c)) } err := lxcSetConfigItem(cc, "lxc.apparmor.profile", profile) @@ -2684,7 +2685,7 @@ func (c *containerLXC) OnStart() error { } // Load the container AppArmor profile - err = AALoadProfile(c) + err = apparmor.LoadProfile(c) if err != nil { if ourStart { c.StorageStop() @@ -2698,7 +2699,7 @@ func (c *containerLXC) OnStart() error { // Run any template that needs running err = c.templateApplyNow(c.localConfig[key]) if err != nil { - AADestroy(c) + apparmor.Destroy(c) if ourStart { c.StorageStop() } @@ -2708,7 +2709,7 @@ func (c *containerLXC) OnStart() error { // Remove the volatile key from the DB err := c.state.Cluster.ContainerConfigRemove(c.id, key) if err != nil { - AADestroy(c) + apparmor.Destroy(c) if ourStart { c.StorageStop() } @@ -2718,7 +2719,7 @@ func (c *containerLXC) OnStart() error { err = c.templateApplyNow("start") if err != nil { - AADestroy(c) + apparmor.Destroy(c) if ourStart { c.StorageStop() } @@ -3053,7 +3054,7 @@ func (c *containerLXC) OnStop(target string) error { c.IsRunning() // Unload the apparmor profile - err = AADestroy(c) + err = apparmor.Destroy(c) if err != nil { logger.Error("Failed to destroy apparmor namespace", log.Ctx{"container": c.Name(), "err": err}) } @@ -3606,7 +3607,7 @@ func (c *containerLXC) cleanup() { c.removeDiskDevices() // Remove the security profiles - AADeleteProfile(c) + apparmor.DeleteProfile(c) seccomp.DeleteProfile(c) // Remove the devices path @@ -4332,7 +4333,7 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error { // If apparmor changed, re-validate the apparmor profile if shared.StringInSlice("raw.apparmor", changedConfig) || shared.StringInSlice("security.nesting", changedConfig) { - err = AAParseProfile(c) + err = apparmor.ParseProfile(c) if err != nil { return errors.Wrap(err, "Parse AppArmor profile") } @@ -4404,7 +4405,7 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error { if key == "raw.apparmor" || key == "security.nesting" { // Update the AppArmor profile - err = AALoadProfile(c) + err = apparmor.LoadProfile(c) if err != nil { return err }
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel