From 5b8d73bbcfc103a8296e060fb23fbac38872b958 Mon Sep 17 00:00:00 2001 From: michalbiesek Date: Fri, 2 Dec 2022 12:50:47 +0100 Subject: [PATCH] (#1108) IPC mechanism - client part --- cli/run/attach.go | 34 ++++- cli/util/ipc.go | 168 +++++++++++++++++++++ cli/util/ipccmd.go | 20 +++ cli/util/mq.go | 238 +++++++++++++++++++++++++++++ cli/util/mq_test.go | 99 +++++++++++++ cli/util/namespace.go | 37 +++++ cli/util/namespace_test.go | 34 +++++ cli/util/proc.go | 239 ++++++++++++++++++++++++------ cli/util/proc_test.go | 6 +- cli/util/scopestate.go | 49 ++++++ test/integration/cli/Dockerfile | 1 + test/integration/cli/dummy_filter | 2 + test/integration/cli/scope-test | 2 + test/integration/cli/test_cli.sh | 29 ++++ 14 files changed, 902 insertions(+), 56 deletions(-) create mode 100644 cli/util/ipc.go create mode 100644 cli/util/ipccmd.go create mode 100644 cli/util/mq.go create mode 100644 cli/util/mq_test.go create mode 100644 cli/util/namespace_test.go create mode 100644 cli/util/scopestate.go create mode 100755 test/integration/cli/dummy_filter diff --git a/cli/run/attach.go b/cli/run/attach.go index 182cfcfbc..863b8a91d 100644 --- a/cli/run/attach.go +++ b/cli/run/attach.go @@ -17,6 +17,7 @@ var ( errGetLinuxCap = errors.New("unable to get linux capabilities for current process") errLoadLinuxCap = errors.New("unable to load linux capabilities for current process") errMissingPtrace = errors.New("missing PTRACE capabilities to attach to a process") + errMissingScopedProc = errors.New("no scoped process found matching that name") errMissingProc = errors.New("no process found matching that name") errPidInvalid = errors.New("invalid PID") errPidMissing = errors.New("PID does not exist") @@ -30,14 +31,19 @@ var ( // Attach scopes an existing PID func (rc *Config) Attach(args []string) error { - pid, err := handleInputArg(args[0]) + pid, err := handleInputArg(args[0], true) if err != nil { return err } args[0] = fmt.Sprint(pid) var reattach bool // Check PID is not already being scoped - if !util.PidScoped(pid) { + status, err := util.PidScopeStatus(pid) + if err != nil { + return err + } + + if status == util.Disable || status == util.Setup { // Validate user has root permissions if err := util.UserVerifyRootPerm(); err != nil { return err @@ -113,7 +119,7 @@ func (rc *Config) DetachAll(args []string, prompt bool) error { fmt.Println("INFO: Run as root (or via sudo) to see all matching processes") } - procs, err := util.ProcessesScoped() + procs, err := util.ProcessesToDetach() if err != nil { return err } @@ -158,7 +164,7 @@ func (rc *Config) DetachAll(args []string, prompt bool) error { // DetachSingle unscopes an existing PID func (rc *Config) DetachSingle(args []string) error { - pid, err := handleInputArg(args[0]) + pid, err := handleInputArg(args[0], false) if err != nil { return err } @@ -173,7 +179,10 @@ func (rc *Config) DetachSingle(args []string) error { func (rc *Config) detach(args []string, pid int) error { // Check PID is already being scoped - if !util.PidScoped(pid) { + status, err := util.PidScopeStatus(pid) + if err != nil { + return err + } else if status != util.Active { return errNotScoped } @@ -189,10 +198,11 @@ func (rc *Config) detach(args []string, pid int) error { } // handleInputArg handles the input argument (process id/name) -func handleInputArg(InputArg string) (int, error) { +func handleInputArg(InputArg string, toAttach bool) (int, error) { // Get PID by name if non-numeric, otherwise validate/use InputArg var pid int var err error + var procs util.Processes if util.IsNumeric(InputArg) { pid, err = strconv.Atoi(InputArg) if err != nil { @@ -208,7 +218,12 @@ func handleInputArg(InputArg string) (int, error) { } } - procs, err := util.ProcessesByName(InputArg) + if toAttach { + procs, err = util.ProcessesByNameToAttach(InputArg) + } else { + procs, err = util.ProcessesByNameToDetach(InputArg) + } + if err != nil { return -1, err } @@ -241,7 +256,10 @@ func handleInputArg(InputArg string) (int, error) { if !adminStatus { fmt.Println("INFO: Run as root (or via sudo) to see all matching processes") } - return -1, errMissingProc + if toAttach { + return -1, errMissingProc + } + return -1, errMissingScopedProc } } diff --git a/cli/util/ipc.go b/cli/util/ipc.go new file mode 100644 index 000000000..628e4b40a --- /dev/null +++ b/cli/util/ipc.go @@ -0,0 +1,168 @@ +package util + +import ( + "errors" + "fmt" + "os" + "syscall" + "time" +) + +// ipc structure representes Inter Process Communication object +type ipcObj struct { + sender *sendMessageQueue // Message queue used to send messages + receiver *receiveMessageQueue // Message queue used to receive messages + ipcSwitch bool // Indicator if IPC switch occured +} + +var ( + errMissingProcMsgQueue = errors.New("missing message queue from PID") + errMissingResponse = errors.New("missing response from PID") +) + +// ipcGetScopeStatus dispatches cmd to the process specified by the pid. +// Returns the byte answer from scoped process endpoint. +func ipcGetScopeStatus(pid int) ([]byte, error) { + return ipcDispatcher(cmdGetScopeStatus, pid) +} + +// ipcDispatcher dispatches cmd to the process specified by the pid. +// Returns the byte answer from scoped process endpoint. +func ipcDispatcher(cmd ipcCmd, pid int) ([]byte, error) { + var answer []byte + var responseReceived bool + + ipc, err := newIPC(pid) + if err != nil { + return answer, err + } + defer ipc.destroyIPC() + + if err := ipc.send(cmd.byte()); err != nil { + return answer, err + } + + // TODO: Ugly hack but we need to wait for answer from process + for i := 0; i < 5000; i++ { + if !ipc.empty() { + responseReceived = true + break + } + time.Sleep(time.Millisecond) + } + + // Missing response + // The message queue on the application side exists but we are unable to receive + // an answer from it + if !responseReceived { + return answer, fmt.Errorf("%v %v", errMissingResponse, pid) + } + + return ipc.receive() +} + +// nsnewNonBlockMsgQReader creates an IPC structure with switching effective uid and gid +func nsnewNonBlockMsgQReader(name string, nsUid int, nsGid int, restoreUid int, restoreGid int) (*receiveMessageQueue, error) { + + if err := syscall.Setegid(nsGid); err != nil { + return nil, err + } + if err := syscall.Seteuid(nsUid); err != nil { + return nil, err + } + + receiver, err := newNonBlockMsgQReader(name) + if err != nil { + return nil, err + } + + if err := syscall.Seteuid(restoreUid); err != nil { + return nil, err + } + + if err := syscall.Setegid(restoreGid); err != nil { + return nil, err + } + + return receiver, err +} + +// newIPC creates an IPC object designated for communication with specific PID +func newIPC(pid int) (*ipcObj, error) { + restoreGid := os.Getegid() + restoreUid := os.Geteuid() + + ipcSame, err := namespaceSameIpc(pid) + if err != nil { + return nil, err + } + + // Retrieve information about user nad group id + nsUid, err := pidNsTranslateUid(restoreUid, pid) + if err != nil { + return nil, err + } + nsGid, err := pidNsTranslateGid(restoreGid, pid) + if err != nil { + return nil, err + } + + // Retrieve information about process namespace PID + _, ipcPid, err := pidLastNsPid(pid) + if err != nil { + return nil, err + } + + // Switch IPC if neeeded + if !ipcSame { + if err := namespaceSwitchIPC(pid); err != nil { + return nil, err + } + } + + // Try to open proc message queue + sender, err := openMsgQWriter(fmt.Sprintf("ScopeIPCIn.%d", ipcPid)) + if err != nil { + namespaceRestoreIPC() + return nil, errMissingProcMsgQueue + } + + // Try to create own message queue + receiver, err := nsnewNonBlockMsgQReader(fmt.Sprintf("ScopeIPCOut.%d", ipcPid), nsUid, nsGid, restoreUid, restoreGid) + if err != nil { + sender.close() + namespaceRestoreIPC() + return nil, err + } + + return &ipcObj{sender: sender, receiver: receiver, ipcSwitch: ipcSame}, nil +} + +// destroyIPC destroys an IPC object +func (ipc *ipcObj) destroyIPC() { + ipc.sender.close() + ipc.receiver.close() + ipc.receiver.unlink() + if ipc.ipcSwitch { + namespaceRestoreIPC() + } +} + +// receive receive the message from the process endpoint +func (ipc *ipcObj) receive() ([]byte, error) { + return ipc.receiver.receive(0) +} + +// empty checks if receiver message queque is empty +func (ipc *ipcObj) empty() bool { + atr, err := ipc.receiver.getAttributes() + if err != nil { + return true + } + return atr.CurrentMessages == 0 +} + +// send sends the message to the process endpoint +func (ipc *ipcObj) send(msg []byte) error { + return ipc.sender.send(msg, 0) +} diff --git a/cli/util/ipccmd.go b/cli/util/ipccmd.go new file mode 100644 index 000000000..83c4657a6 --- /dev/null +++ b/cli/util/ipccmd.go @@ -0,0 +1,20 @@ +package util + +// ipcCmd represents the command structure +type ipcCmd int64 + +const ( + cmdGetScopeStatus ipcCmd = iota +) + +func (cmd ipcCmd) string() string { + switch cmd { + case cmdGetScopeStatus: + return "getScopeStatus" + } + return "unknown" +} + +func (cmd ipcCmd) byte() []byte { + return []byte(cmd.string()) +} diff --git a/cli/util/mq.go b/cli/util/mq.go new file mode 100644 index 000000000..6ca2bba10 --- /dev/null +++ b/cli/util/mq.go @@ -0,0 +1,238 @@ +package util + +import ( + "errors" + "fmt" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +var ( + errEmptyMsg = errors.New("empty message not supported") + errMsgQCreate = errors.New("message queue error during create") + errMsgQOpen = errors.New("message queue error during open") + errMsgQGetAttr = errors.New("message queue error during get attributes") + errMsgQUnlink = errors.New("message queue error during unlink") + errMsgQSendMsg = errors.New("message queue error during sending msg") + errMsgQRecvMsg = errors.New("message queue error during receiving msg") +) + +// Default values of +// - maximum number of messsages in a queue +// - maximum message size in a queue +// Details: https://man7.org/linux/man-pages/man7/mq_overview.7.html +const mqMaxMsgMax int = 10 +const mqMaxMsgSize int = 8192 + +// Message queque attribute structure +// Details: https://man7.org/linux/man-pages/man3/mq_getattr.3.html +type messageQueueAttributes struct { + Flags int // Message queue flags: 0 or O_NONBLCOK + MaxQueueSize int // Max # of messages in queue + MaxMessageSize int // Max message size in bytes + CurrentMessages int // # of messages currently in queue +} + +type messageQueue struct { + fd int // Message queue file descriptor + name string // Message queue name + cap int // Message queue capacity +} + +type sendMessageQueue struct { + messageQueue +} + +type receiveMessageQueue struct { + messageQueue +} + +// getMQAttributes retrieves Message queue attributes from file descriptor +func getMQAttributes(fd int) (*messageQueueAttributes, error) { + + attr := new(messageQueueAttributes) + + // int syscall(SYS_mq_getsetattr, mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr) + // Details: https://man7.org/linux/man-pages/man2/mq_getsetattr.2.html + + _, _, errno := unix.Syscall(unix.SYS_MQ_GETSETATTR, + uintptr(fd), // mqdes + uintptr(0), // newattr + uintptr(unsafe.Pointer(attr))) // oldattr + + if errno != 0 { + return nil, fmt.Errorf("%v %v", errMsgQGetAttr, errno) + } + + return attr, nil +} + +// openMsgQWriter open existing message queue with write only permissions +func openMsgQWriter(name string) (*sendMessageQueue, error) { + unixName, err := unix.BytePtrFromString(name) + if err != nil { + return nil, err + } + + // mqd_t mq_open(const char *name, int oflag); + // Details: https://man7.org/linux/man-pages/man3/mq_open.3.html + mqfd, _, errno := unix.Syscall( + unix.SYS_MQ_OPEN, + uintptr(unsafe.Pointer(unixName)), // name + uintptr(unix.O_WRONLY), // oflag + 0, // unused + ) + + if errno != 0 { + return nil, fmt.Errorf("%v %v", errMsgQOpen, errno) + } + + fd := int(mqfd) + + // Get Message Queue MaxMessageSize + attr, err := getMQAttributes(fd) + if err != nil { + return nil, errno + } + + return &sendMessageQueue{ + messageQueue: messageQueue{ + fd: fd, + name: name, + cap: attr.MaxMessageSize, + }, + }, nil + +} + +// newNonBlockMsgQReader creates non-blocking message queue with read only permissions +func newNonBlockMsgQReader(name string) (*receiveMessageQueue, error) { + unixName, err := unix.BytePtrFromString(name) + if err != nil { + return nil, err + } + + oldmask := syscall.Umask(0) + // mqd_t mq_open(const char *name, int oflag, mode_t mode,struct mq_attr *attr) + // Details: https://man7.org/linux/man-pages/man3/mq_open.3.html + mqfd, _, errno := unix.Syscall6( + unix.SYS_MQ_OPEN, + uintptr(unsafe.Pointer(unixName)), // name + uintptr(unix.O_CREAT|unix.O_NONBLOCK|unix.O_RDONLY), // oflag + uintptr(0666), // mode + uintptr(unsafe.Pointer(&messageQueueAttributes{ + MaxQueueSize: mqMaxMsgMax, + MaxMessageSize: mqMaxMsgSize, + })), // attr + 0, // unused + 0, // unused + ) + syscall.Umask(oldmask) + + if errno != 0 { + return nil, fmt.Errorf("%v %v", errMsgQCreate, errno) + } + + return &receiveMessageQueue{ + messageQueue: messageQueue{ + fd: int(mqfd), + name: name, + cap: mqMaxMsgSize, + }, + }, nil +} + +// getAttributes retrieves message queue attributes +func (mq *messageQueue) getAttributes() (*messageQueueAttributes, error) { + return getMQAttributes(mq.fd) +} + +// close close message queue +func (mq *messageQueue) close() error { + return unix.Close(int(mq.fd)) +} + +// unlink unlinks message queue +func (mq *receiveMessageQueue) unlink() error { + unixName, err := unix.BytePtrFromString(mq.name) + if err != nil { + return err + } + + _, _, errno := unix.Syscall( + unix.SYS_MQ_UNLINK, + uintptr(unsafe.Pointer(unixName)), + 0, // unused + 0, // unused + ) + + if errno != 0 { + return fmt.Errorf("%v %v", errMsgQUnlink, errno) + } + + return nil +} + +// getBlockAbsTime retrieve the time which the call will block +func getBlockAbsTime(timeout time.Duration) uintptr { + if timeout == 0 { + return 0 + } + + ts := unix.NsecToTimespec(time.Now().Add(timeout).UnixNano()) + return uintptr(unsafe.Pointer(&ts)) +} + +// send data to message queue +func (mq *sendMessageQueue) send(msg []byte, timeout time.Duration) error { + msgLen := len(msg) + if msgLen == 0 { + return errEmptyMsg + } + + // int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout) + // Details: https:///man7.org/linux/man-pages/man3/mq_send.3.html + + _, _, errno := unix.Syscall6( + unix.SYS_MQ_TIMEDSEND, + uintptr(mq.fd), // mqdes + uintptr(unsafe.Pointer(&msg[0])), // msg_ptr + uintptr(msgLen), // msg_len + uintptr(0), // msg_prio + getBlockAbsTime(timeout), // abs_timeout + 0, // unused + ) + + if errno != 0 { + return fmt.Errorf("%v %v", errMsgQSendMsg, errno) + } + return nil +} + +// receive data from message queque +func (mq *receiveMessageQueue) receive(timeout time.Duration) ([]byte, error) { + + recvBuf := make([]byte, mq.cap) + + // ssize_t mq_timedreceive(mqd_t mqdes, char *restrict msg_ptr, size_t msg_len, unsigned int *restrict msg_prio, const struct timespec *restrict abs_timeout) + // Details: https://man7.org/linux/man-pages/man3/mq_receive.3.html + + size, _, errno := unix.Syscall6( + unix.SYS_MQ_TIMEDRECEIVE, + uintptr(mq.fd), // mqdes + uintptr(unsafe.Pointer(&recvBuf[0])), // msg_ptr + uintptr(mq.cap), // msg_len + uintptr(0), // msg_prio + getBlockAbsTime(timeout), // abs_timeout + 0, // unused + ) + + if errno != 0 { + return nil, fmt.Errorf("%v %v", errMsgQRecvMsg, errno) + } + + return recvBuf[0:int(size)], nil +} diff --git a/cli/util/mq_test.go b/cli/util/mq_test.go new file mode 100644 index 000000000..244f18c07 --- /dev/null +++ b/cli/util/mq_test.go @@ -0,0 +1,99 @@ +package util + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewMq(t *testing.T) { + const mqName string = "goTestMq" + + msgQ, err := newNonBlockMsgQReader(mqName) + assert.NoError(t, err) + assert.NotNil(t, msgQ) + atr, err := msgQ.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + err = msgQ.close() + assert.NoError(t, err) + err = msgQ.unlink() + assert.NoError(t, err) +} + +func TestOpenMqNonExisting(t *testing.T) { + msgQ, err := openMsgQWriter("not_existing") + assert.Error(t, err) + assert.Nil(t, msgQ) +} + +func TestCommunicationMq(t *testing.T) { + const mqName string = "goTestMq" + byteTestMsg := []byte("Lorem Ipsum") + + msgQReader, err := newNonBlockMsgQReader(mqName) + assert.NoError(t, err) + assert.NotNil(t, msgQReader) + atr, err := msgQReader.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Zero(t, atr.CurrentMessages) + + msgQWriter, err := openMsgQWriter(mqName) + assert.NoError(t, err) + assert.NotNil(t, msgQWriter) + atr, err = msgQWriter.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Zero(t, atr.CurrentMessages) + + // Try to read from empty queue + data, err := msgQReader.receive(time.Microsecond) + assert.Nil(t, data) + assert.Error(t, err) + + // Empty message send + err = msgQWriter.send([]byte(""), time.Microsecond) + assert.Error(t, err) + atr, err = msgQReader.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Zero(t, atr.CurrentMessages) + atr, err = msgQWriter.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Zero(t, atr.CurrentMessages) + + // Normal message send + err = msgQWriter.send(byteTestMsg, time.Microsecond) + assert.NoError(t, err) + atr, err = msgQReader.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Equal(t, atr.CurrentMessages, 1) + atr, err = msgQWriter.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Equal(t, atr.CurrentMessages, 1) + + data, err = msgQReader.receive(time.Microsecond) + assert.Equal(t, data, byteTestMsg) + assert.NotNil(t, data) + assert.NoError(t, err) + atr, err = msgQReader.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Zero(t, atr.CurrentMessages) + atr, err = msgQWriter.getAttributes() + assert.NotNil(t, atr) + assert.NoError(t, err) + assert.Zero(t, atr.CurrentMessages) + + err = msgQWriter.close() + assert.NoError(t, err) + err = msgQReader.close() + assert.NoError(t, err) + err = msgQReader.unlink() + assert.NoError(t, err) +} diff --git a/cli/util/namespace.go b/cli/util/namespace.go index 3ba5641fc..cbedd2aa8 100644 --- a/cli/util/namespace.go +++ b/cli/util/namespace.go @@ -2,9 +2,12 @@ package util import ( "errors" + "fmt" "os" + "syscall" lxd "github.com/lxc/lxd/client" + "golang.org/x/sys/unix" ) var ( @@ -85,6 +88,40 @@ func getContainerRuntimePids(runtimeProc string) ([]int, error) { return pids, nil } +// namespaceSameIpc compare own IPC namespace with specified pid +func namespaceSameIpc(pid int) (bool, error) { + selfFi, err := os.Stat("/proc/self/ns/ipc") + if err != nil { + return false, err + } + + pidFi, err := os.Stat(fmt.Sprintf("/proc/%v/ns/ipc", pid)) + if err != nil { + return false, err + } + return os.SameFile(selfFi, pidFi), nil +} + +// namespaceSwitchIPC switch IPC namespace to the specified pid +func namespaceSwitchIPC(pid int) error { + fd, err := os.Open(fmt.Sprintf("/proc/%v/ns/ipc", pid)) + if err != nil { + return err + } + defer fd.Close() + return unix.Setns(int(fd.Fd()), syscall.CLONE_NEWIPC) +} + +// namespaceRestoreIPC restore IPC namespace to the one specified in PID +func namespaceRestoreIPC() error { + fd, err := os.Open("/proc/self/ns/ipc") + if err != nil { + return err + } + defer fd.Close() + return unix.Setns(int(fd.Fd()), syscall.CLONE_NEWIPC) +} + /* * Detect whether or not scope was executed inside a container * What is a reasonable algorithm for determining if the current process is in a container? diff --git a/cli/util/namespace_test.go b/cli/util/namespace_test.go new file mode 100644 index 000000000..c00d06a30 --- /dev/null +++ b/cli/util/namespace_test.go @@ -0,0 +1,34 @@ +package util + +import ( + "os" + "os/exec" + "syscall" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCompareSelfIpcNs(t *testing.T) { + selfPid := os.Getpid() + sameIPC, err := namespaceSameIpc(selfPid) + assert.NoError(t, err) + assert.True(t, sameIPC) +} + +func TestCompareDiffIpcNs(t *testing.T) { + cmd := exec.Command("sh") + cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWIPC | syscall.CLONE_NEWUSER, + } + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + assert.NoError(t, err) + sameIPC, err := namespaceSameIpc(cmd.Process.Pid) + assert.NoError(t, err) + assert.False(t, sameIPC) + err = cmd.Wait() + assert.NoError(t, err) +} diff --git a/cli/util/proc.go b/cli/util/proc.go index bea1fb070..98ac37364 100644 --- a/cli/util/proc.go +++ b/cli/util/proc.go @@ -35,6 +35,8 @@ var ( errGetProcTask = errors.New("error getting process task") errGetProcChildren = errors.New("error getting process children") errGetNsPid = errors.New("error getting namespace PID") + errGetProcGidMap = errors.New("error getting process gid map") + errGetProcUidMap = errors.New("error getting process uid map") errMissingUser = errors.New("unable to find user") ) @@ -64,15 +66,9 @@ type searchFunc func(int, string) bool func pidScopeMapSearch(inputArg string, sF searchFunc) (PidScopeMapState, error) { pidMap := make(PidScopeMapState) - procDir, err := os.Open("/proc") - if err != nil { - return pidMap, errOpenProc - } - defer procDir.Close() - - procs, err := procDir.Readdirnames(0) + procs, err := pidProcDirsNames() if err != nil { - return pidMap, errReadProc + return pidMap, err } for _, p := range procs { @@ -88,7 +84,11 @@ func pidScopeMapSearch(inputArg string, sF searchFunc) (PidScopeMapState, error) } if sF(pid, inputArg) { - pidMap[pid] = PidScoped(pid) + status, err := PidScopeStatus(pid) + if err != nil { + continue + } + pidMap[pid] = (status == Active) } } @@ -105,19 +105,34 @@ func PidScopeMapByCmdLine(cmdLine string) (PidScopeMapState, error) { return pidScopeMapSearch(cmdLine, searchPidByCmdLine) } -// ProcessesByName returns an array of processes that match a given name -func ProcessesByName(name string) (Processes, error) { - processes := make([]Process, 0) - +// pidProcDirsNames returns a list wiht process directory names +func pidProcDirsNames() ([]string, error) { procDir, err := os.Open("/proc") if err != nil { - return processes, errOpenProc + return nil, errOpenProc } defer procDir.Close() - procs, err := procDir.Readdirnames(0) + return procDir.Readdirnames(0) +} + +// ProcessesByNameToDetach returns an array of processes to detach that match a given name +func ProcessesByNameToDetach(name string) (Processes, error) { + return processesByName(name, true) +} + +// ProcessesByNameToAttach returns an array of processes to attach that match a given name +func ProcessesByNameToAttach(name string) (Processes, error) { + return processesByName(name, false) +} + +// processesByName returns an array of processes that match a given name +func processesByName(name string, activeOnly bool) (Processes, error) { + processes := make([]Process, 0) + + procs, err := pidProcDirsNames() if err != nil { - return processes, errReadProc + return processes, err } i := 1 @@ -156,13 +171,72 @@ func ProcessesByName(name string) (Processes, error) { continue } + status, err := PidScopeStatus(pid) + if err != nil { + continue + } + // Add process if there is a name match if strings.Contains(command, name) { + if !activeOnly || (activeOnly && status == Active) { + processes = append(processes, Process{ + ID: i, + Pid: pid, + User: userName, + Scoped: status == Active, + Command: cmdLine, + }) + i++ + } + } + } + return processes, nil +} + +// ProcessesScoped returns an array of processes that are currently being scoped +func ProcessesScoped() (Processes, error) { + processes := make([]Process, 0) + + procs, err := pidProcDirsNames() + if err != nil { + return processes, err + } + + i := 1 + for _, p := range procs { + // Skip non-integers as they are not PIDs + if !IsNumeric(p) { + continue + } + + // Convert directory name to integer + pid, err := strconv.Atoi(p) + if err != nil { + continue + } + + cmdLine, err := PidCmdline(pid) + if err != nil { + continue + } + + // TODO in container namespace we cannot depend on following info + userName, err := PidUser(pid) + if err != nil && !errors.Is(err, errMissingUser) { + continue + } + + // Add process if is is scoped + status, err := PidScopeStatus(pid) + if err != nil { + continue + } + if status == Active { processes = append(processes, Process{ ID: i, Pid: pid, User: userName, - Scoped: PidScoped(pid), + Scoped: true, Command: cmdLine, }) i++ @@ -171,19 +245,13 @@ func ProcessesByName(name string) (Processes, error) { return processes, nil } -// ProcessesScoped returns an array of processes that are currently being scoped -func ProcessesScoped() (Processes, error) { +// ProcessesToDetach returns an array of processes that can be detached +func ProcessesToDetach() (Processes, error) { processes := make([]Process, 0) - procDir, err := os.Open("/proc") + procs, err := pidProcDirsNames() if err != nil { - return processes, errOpenProc - } - defer procDir.Close() - - procs, err := procDir.Readdirnames(0) - if err != nil { - return processes, errReadProc + return processes, err } i := 1 @@ -210,14 +278,17 @@ func ProcessesScoped() (Processes, error) { continue } - // Add process if is is scoped - scoped := PidScoped(pid) - if scoped { + status, err := PidScopeStatus(pid) + if err != nil && !errors.Is(err, errMissingUser) { + continue + } + // Add process if is is actively scoped + if status == Active { processes = append(processes, Process{ ID: i, Pid: pid, User: userName, - Scoped: scoped, + Scoped: true, Command: cmdLine, }) i++ @@ -244,32 +315,35 @@ func PidUser(pid int) (string, error) { return user.Username, nil } -// PidScoped checks if a process specified by PID is being scoped -func PidScoped(pid int) bool { - - // Look for libscope in /proc maps +// // PidScopeStatus checks a Scope Status if a process specified by PID. +func PidScopeStatus(pid int) (ScopeStatus, error) { pidMapFile, err := os.ReadFile(fmt.Sprintf("/proc/%v/maps", pid)) if err != nil { - return false + // Process or do not exist or we do not have permissions to read a map file + return Disable, err } + pidMap := string(pidMapFile) if !strings.Contains(pidMap, "libscope") { - return false + // Process does not contain libscope library in maps + return Disable, nil } // Ignore ldscope process command, err := PidCommand(pid) if err != nil { - return false + return Disable, nil } + if command == "ldscopedyn" { - return false + return Disable, nil } // Check shmem does not exist (if scope_anon does not exist the proc is scoped) files, err := os.ReadDir(fmt.Sprintf("/proc/%v/fd", pid)) if err != nil { - return false + // Process or do not exist or we do not have permissions to read a fd file + return Disable, err } for _, file := range files { @@ -279,11 +353,12 @@ func PidScoped(pid int) bool { continue } if strings.Contains(resolvedFileName, "scope_anon") { - return false + return Setup, nil } } - return true + // Retrieve information from IPC + return getScopeStatus(pid), nil } // PidCommand gets the command used to start the process specified by PID @@ -374,8 +449,82 @@ func PidThreadsPids(pid int) ([]int, error) { // PidExists checks if a PID is valid func PidExists(pid int) bool { pidPath := fmt.Sprintf("/proc/%v", pid) - if CheckDirExists(pidPath) { - return true + return CheckDirExists(pidPath) +} + +// pidLastNsPid process the NsPid file for specified PID. +// Returns status if the specified PID residents in nested PID namespace, last PID in namespace and status of operation. +func pidLastNsPid(pid int) (bool, int, error) { + // TODO: goprocinfo does not support all the status parameters (NsPid) + // handle procfs by ourselves ? + file, err := os.Open(fmt.Sprintf("/proc/%v/status", pid)) + if err != nil { + return false, -1, errGetProcStatus + } + defer file.Close() + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "NSpid:") { + var nsNestedStatus bool + // Skip Nspid + strPids := strings.Fields(line)[1:] + + strPidsSize := len(strPids) + if strPidsSize > 1 { + nsNestedStatus = true + } + nsLastPid, _ := strconv.Atoi(strPids[strPidsSize-1]) + + return nsNestedStatus, nsLastPid, nil + } + } + return false, -1, errGetProcStatus +} + +// pidNsTranslateUid translate specified uid to the ID-outside-ns in specified pid. +// See https://man7.org/linux/man-pages/man7/user_namespaces.7.html for details +func pidNsTranslateUid(uid int, pid int) (int, error) { + file, err := os.Open(fmt.Sprintf("/proc/%v/uid_map", pid)) + if err != nil { + return -1, errGetProcUidMap + } + defer file.Close() + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + uidMapEntry := strings.Fields(scanner.Text()) + uidInsideNs, _ := strconv.Atoi(uidMapEntry[0]) + uidOutsideNs, _ := strconv.Atoi(uidMapEntry[1]) + length, _ := strconv.Atoi(uidMapEntry[2]) + if (uid >= uidInsideNs) && (uid < uidInsideNs+length) { + return uidOutsideNs + uid, nil + } + } + // unreachable + return -1, errGetProcUidMap +} + +// pidNsTranslateGid translate specified gid to the ID-outside-ns in specified pid. +// See https://man7.org/linux/man-pages/man7/user_namespaces.7.html for details +func pidNsTranslateGid(gid int, pid int) (int, error) { + file, err := os.Open(fmt.Sprintf("/proc/%v/gid_map", pid)) + if err != nil { + return -1, errGetProcGidMap + } + defer file.Close() + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + gidMapEntry := strings.Fields(scanner.Text()) + gidInsideNs, _ := strconv.Atoi(gidMapEntry[0]) + gidOutsideNs, _ := strconv.Atoi(gidMapEntry[1]) + length, _ := strconv.Atoi(gidMapEntry[2]) + if (gid >= gidInsideNs) && (gid < gidInsideNs+length) { + return gidOutsideNs + gid, nil + } } - return false + // unreachable + return -1, errGetProcGidMap } diff --git a/cli/util/proc_test.go b/cli/util/proc_test.go index 3d1e7811c..6755f2040 100644 --- a/cli/util/proc_test.go +++ b/cli/util/proc_test.go @@ -9,14 +9,14 @@ import ( "github.com/stretchr/testify/assert" ) -// TestProcessesByName +// TestProcessesByNameToAttach // Assertions: // - The expected process array is returned // - No error is returned -func TestProcessesByName(t *testing.T) { +func TestProcessesByNameToAttach(t *testing.T) { // Current process name := "util.test" - result, err := ProcessesByName(name) + result, err := ProcessesByNameToAttach(name) user, _ := user.Current() exp := Processes{ Process{ diff --git a/cli/util/scopestate.go b/cli/util/scopestate.go new file mode 100644 index 000000000..6c36573c0 --- /dev/null +++ b/cli/util/scopestate.go @@ -0,0 +1,49 @@ +package util + +import ( + "bytes" +) + +// ScopeStatus represents the process status in context of libscope.so +type ScopeStatus int + +// Scope Status description +// +// When process started it will be in one of the following states: +// Disable +// Active +// Setup +// +// Possible transfer between states: +// Disable -> Active (attach required sudo - ptrace) +// Setup -> Active (attach required sudo - ptrace) +// Latent -> Active +// Active -> Latent +// Setup -> Latent +// +// If the process is in Active status it can be changed only to Latent status + +const ( + Disable ScopeStatus = iota // libscope.so is not loaded + Setup // libscope.so is loaded, reporting thread is not present + Active // libscope.so is loaded, reporting thread is present we are sending data + Latent // libscope.so is loaded, reporting thread is present we are not emiting any data +) + +func (state ScopeStatus) String() string { + return []string{"Disable", "Setup", "Active", "Latent"}[state] +} + +// getScopeStatus retreive sinformation about Scope status from IPC +func getScopeStatus(pid int) ScopeStatus { + bresp, err := ipcGetScopeStatus(pid) + if err != nil { + return Setup + } + if bytes.Equal(bresp, []byte("true")) { + return Active + } else if bytes.Equal(bresp, []byte("false")) { + return Latent + } + return Disable +} diff --git a/test/integration/cli/Dockerfile b/test/integration/cli/Dockerfile index 444e84c0e..7e7008b5b 100644 --- a/test/integration/cli/Dockerfile +++ b/test/integration/cli/Dockerfile @@ -14,6 +14,7 @@ RUN apt update \ COPY ./cli/test_cli.sh /opt/test/bin/test_cli.sh COPY ./cli/test_cli_dest.sh /opt/test/bin/test_cli_dest.sh COPY ./cli/test_edge.sh /opt/test/bin/test_edge.sh +COPY ./cli/dummy_filter /opt/test/dummy_filter COPY ./cli/expected.yml / RUN mkdir /opt/test-runner && \ diff --git a/test/integration/cli/dummy_filter b/test/integration/cli/dummy_filter new file mode 100755 index 000000000..53a05507c --- /dev/null +++ b/test/integration/cli/dummy_filter @@ -0,0 +1,2 @@ +deny: +- arg: loremIpsum diff --git a/test/integration/cli/scope-test b/test/integration/cli/scope-test index 20cdc90b3..d3b417f74 100755 --- a/test/integration/cli/scope-test +++ b/test/integration/cli/scope-test @@ -3,6 +3,8 @@ ERR=0 /opt/test/bin/test_cli.sh ERR+=$? +/opt/test/bin/test_cli_ipc.sh +ERR+=$? /opt/test/bin/test_cli_dest.sh ERR+=$? /opt/test/bin/test_edge.sh diff --git a/test/integration/cli/test_cli.sh b/test/integration/cli/test_cli.sh index fcc5468eb..20327e1b6 100755 --- a/test/integration/cli/test_cli.sh +++ b/test/integration/cli/test_cli.sh @@ -1,5 +1,6 @@ #! /bin/bash export SCOPE_EVENT_DEST=file:///opt/test/logs/events.log +DUMMY_FILTER_FILE="/opt/test-runner/dummy_filter" DEBUG=0 # set this to 1 to capture the EVT_FILE for each test FAILED_TEST_LIST="" @@ -175,6 +176,34 @@ returns 0 endtest +# +# Scope ps +# +starttest "Scope ipc all states" + +# Prepare Same process in all states +python3 -m http.server 9090 & +python3_disabled=$! +SCOPE_FILTER="/opt/test-runner/dummy_filter" ldscope -- python3 -m http.server 9091 & +python3_setup=$! +python3 -m http.server 9092 & +python3_active=$! +scope attach $python3_active +python3 -m http.server 9093 & +python3_latent=$! +scope attach $python3_latent +scope detach $python3_latent + +# Scope ps +run scope attach python3 +outputs "ID PID USER SCOPED COMMAND +1 ${python3_disabled} root false python3 -m http.server 9090 +2 ${python3_disabled} root true python3 -m http.server 9091 +3 ${python3_disabled} root true python3 -m http.server 9092 +4 ${python3_disabled} root false python3 -m http.server 9093" + +endtest + # # Scope start no force