The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/lxd/pull/5090
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) === util_linux.go for all linux util functions, without cgo specific code util_linux_cgo.go for all linux util functions depending on cgo Reason: github.com/sl1pm4t/terraform-provider-lxd uses the shared lxd code for interactions with the lxd daemon. Compiling this for other architectures, results in a build where archive_linux.go isn't compilable because of missing functions defined in util_linux.go This would fix compile errors where no cgo specific code is used but the code needed is not compiled because it is in a cgo only source file with this, gox could build the terraform plugin for the following platforms ``` gox Number of parallel builds: 7 --> windows/386: github.com/sl1pm4t/terraform-provider-lxd --> linux/arm: github.com/sl1pm4t/terraform-provider-lxd --> freebsd/amd64: github.com/sl1pm4t/terraform-provider-lxd --> windows/amd64: github.com/sl1pm4t/terraform-provider-lxd --> darwin/386: github.com/sl1pm4t/terraform-provider-lxd --> openbsd/386: github.com/sl1pm4t/terraform-provider-lxd --> freebsd/386: github.com/sl1pm4t/terraform-provider-lxd --> linux/386: github.com/sl1pm4t/terraform-provider-lxd --> darwin/amd64: github.com/sl1pm4t/terraform-provider-lxd --> linux/amd64: github.com/sl1pm4t/terraform-provider-lxd --> netbsd/arm: github.com/sl1pm4t/terraform-provider-lxd --> freebsd/arm: github.com/sl1pm4t/terraform-provider-lxd --> netbsd/386: github.com/sl1pm4t/terraform-provider-lxd --> netbsd/amd64: github.com/sl1pm4t/terraform-provider-lxd --> openbsd/amd64: github.com/sl1pm4t/terraform-provider-lxd --> linux/s390x: github.com/sl1pm4t/terraform-provider-lxd ```
From bdcbe41d913e6af68e0d2dcd61bfd0ea74f8da55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robbert=20M=C3=BCller?= <[email protected]> Date: Wed, 26 Sep 2018 15:42:07 +0200 Subject: [PATCH] Split code in 2 seperate files util_linux.go for all linux util functions, without cgo specific code util_linux_cgo.go for all linux util functions depending on cgo Reason: github.com/sl1pm4t/terraform-provider-lxd uses the shared lxd code for interactions with the lxd daemon. Compiling this for other architectures, results in a build where archive_linux.go isn't compilable because of missing functions defined in util_linux.go This would fix this compile issue ``` gox Number of parallel builds: 7 --> windows/386: github.com/sl1pm4t/terraform-provider-lxd --> linux/arm: github.com/sl1pm4t/terraform-provider-lxd --> freebsd/amd64: github.com/sl1pm4t/terraform-provider-lxd --> windows/amd64: github.com/sl1pm4t/terraform-provider-lxd --> darwin/386: github.com/sl1pm4t/terraform-provider-lxd --> openbsd/386: github.com/sl1pm4t/terraform-provider-lxd --> freebsd/386: github.com/sl1pm4t/terraform-provider-lxd --> linux/386: github.com/sl1pm4t/terraform-provider-lxd --> darwin/amd64: github.com/sl1pm4t/terraform-provider-lxd --> linux/amd64: github.com/sl1pm4t/terraform-provider-lxd --> netbsd/arm: github.com/sl1pm4t/terraform-provider-lxd --> freebsd/arm: github.com/sl1pm4t/terraform-provider-lxd --> netbsd/386: github.com/sl1pm4t/terraform-provider-lxd --> netbsd/amd64: github.com/sl1pm4t/terraform-provider-lxd --> openbsd/amd64: github.com/sl1pm4t/terraform-provider-lxd --> linux/s390x: github.com/sl1pm4t/terraform-provider-lxd ``` --- shared/util_linux.go | 576 -------------------------------------- shared/util_linux_cgo.go | 588 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 588 insertions(+), 576 deletions(-) create mode 100644 shared/util_linux_cgo.go diff --git a/shared/util_linux.go b/shared/util_linux.go index 043a408faf..2a71b7de56 100644 --- a/shared/util_linux.go +++ b/shared/util_linux.go @@ -1,421 +1,19 @@ // +build linux -// +build cgo package shared import ( "bufio" - "errors" "fmt" - "io" "os" "path/filepath" "reflect" "strings" - "sync" - "sync/atomic" "syscall" "unsafe" - "github.com/lxc/lxd/shared/logger" ) -// #cgo LDFLAGS: -lutil -lpthread -/* -#define _GNU_SOURCE -#include <errno.h> -#include <fcntl.h> -#include <grp.h> -#include <limits.h> -#include <poll.h> -#include <pty.h> -#include <pwd.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <unistd.h> -#include <sys/stat.h> -#include <sys/socket.h> -#include <sys/types.h> -#include <sys/un.h> - -#ifndef AT_SYMLINK_FOLLOW -#define AT_SYMLINK_FOLLOW 0x400 -#endif - -#ifndef AT_EMPTY_PATH -#define AT_EMPTY_PATH 0x1000 -#endif - -#define ABSTRACT_UNIX_SOCK_LEN sizeof(((struct sockaddr_un *)0)->sun_path) - -// This is an adaption from https://codereview.appspot.com/4589049, to be -// included in the stdlib with the stdlib's license. - -static int mygetgrgid_r(int gid, struct group *grp, - char *buf, size_t buflen, struct group **result) { - return getgrgid_r(gid, grp, buf, buflen, result); -} - -void configure_pty(int fd) { - struct termios term_settings; - struct winsize win; - - if (tcgetattr(fd, &term_settings) < 0) { - fprintf(stderr, "Failed to get settings: %s\n", strerror(errno)); - return; - } - - term_settings.c_iflag |= IMAXBEL; - term_settings.c_iflag |= IUTF8; - term_settings.c_iflag |= BRKINT; - term_settings.c_iflag |= IXANY; - - term_settings.c_cflag |= HUPCL; - - if (tcsetattr(fd, TCSANOW, &term_settings) < 0) { - fprintf(stderr, "Failed to set settings: %s\n", strerror(errno)); - return; - } - - if (ioctl(fd, TIOCGWINSZ, &win) < 0) { - fprintf(stderr, "Failed to get the terminal size: %s\n", strerror(errno)); - return; - } - - win.ws_col = 80; - win.ws_row = 25; - - if (ioctl(fd, TIOCSWINSZ, &win) < 0) { - fprintf(stderr, "Failed to set the terminal size: %s\n", strerror(errno)); - return; - } - - if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { - fprintf(stderr, "Failed to set FD_CLOEXEC: %s\n", strerror(errno)); - return; - } - - return; -} - -void create_pty(int *master, int *slave, uid_t uid, gid_t gid) { - if (openpty(master, slave, NULL, NULL, NULL) < 0) { - fprintf(stderr, "Failed to openpty: %s\n", strerror(errno)); - return; - } - - configure_pty(*master); - configure_pty(*slave); - - if (fchown(*slave, uid, gid) < 0) { - fprintf(stderr, "Warning: error chowning pty to container root\n"); - fprintf(stderr, "Continuing...\n"); - } - if (fchown(*master, uid, gid) < 0) { - fprintf(stderr, "Warning: error chowning pty to container root\n"); - fprintf(stderr, "Continuing...\n"); - } -} - -void create_pipe(int *master, int *slave) { - int pipefd[2]; - - if (pipe2(pipefd, O_CLOEXEC) < 0) { - fprintf(stderr, "Failed to create a pipe: %s\n", strerror(errno)); - return; - } - - *master = pipefd[0]; - *slave = pipefd[1]; -} - -int get_poll_revents(int lfd, int timeout, int flags, int *revents, int *saved_errno) -{ - int ret; - struct pollfd pfd = {lfd, flags, 0}; - -again: - ret = poll(&pfd, 1, timeout); - if (ret < 0) { - if (errno == EINTR) - goto again; - - *saved_errno = errno; - fprintf(stderr, "Failed to poll() on file descriptor.\n"); - return -1; - } - - *revents = pfd.revents; - - return ret; -} - -int lxc_abstract_unix_send_fds(int fd, int *sendfds, int num_sendfds, - void *data, size_t size) -{ - int ret; - struct msghdr msg; - struct iovec iov; - struct cmsghdr *cmsg = NULL; - char buf[1] = {0}; - char *cmsgbuf; - size_t cmsgbufsize = CMSG_SPACE(num_sendfds * sizeof(int)); - - memset(&msg, 0, sizeof(msg)); - memset(&iov, 0, sizeof(iov)); - - cmsgbuf = malloc(cmsgbufsize); - if (!cmsgbuf) - return -1; - - msg.msg_control = cmsgbuf; - msg.msg_controllen = cmsgbufsize; - - cmsg = CMSG_FIRSTHDR(&msg); - cmsg->cmsg_level = SOL_SOCKET; - cmsg->cmsg_type = SCM_RIGHTS; - cmsg->cmsg_len = CMSG_LEN(num_sendfds * sizeof(int)); - - msg.msg_controllen = cmsg->cmsg_len; - - memcpy(CMSG_DATA(cmsg), sendfds, num_sendfds * sizeof(int)); - - iov.iov_base = data ? data : buf; - iov.iov_len = data ? size : sizeof(buf); - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - - ret = sendmsg(fd, &msg, MSG_NOSIGNAL); - if (ret < 0) - fprintf(stderr, "%s - Failed to send file descriptor\n", strerror(errno)); - free(cmsgbuf); - return ret; -} - -int lxc_abstract_unix_recv_fds(int fd, int *recvfds, int num_recvfds, - void *data, size_t size) -{ - int ret; - struct msghdr msg; - struct iovec iov; - struct cmsghdr *cmsg = NULL; - char buf[1] = {0}; - char *cmsgbuf; - size_t cmsgbufsize = CMSG_SPACE(num_recvfds * sizeof(int)); - - memset(&msg, 0, sizeof(msg)); - memset(&iov, 0, sizeof(iov)); - - cmsgbuf = malloc(cmsgbufsize); - if (!cmsgbuf) - return -1; - - msg.msg_control = cmsgbuf; - msg.msg_controllen = cmsgbufsize; - - iov.iov_base = data ? data : buf; - iov.iov_len = data ? size : sizeof(buf); - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - - ret = recvmsg(fd, &msg, 0); - if (ret <= 0) { - fprintf(stderr, "%s - Failed to receive file descriptor\n", strerror(errno)); - goto out; - } - - cmsg = CMSG_FIRSTHDR(&msg); - - memset(recvfds, -1, num_recvfds * sizeof(int)); - if (cmsg && cmsg->cmsg_len == CMSG_LEN(num_recvfds * sizeof(int)) && - cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { - memcpy(recvfds, CMSG_DATA(cmsg), num_recvfds * sizeof(int)); - } - -out: - free(cmsgbuf); - return ret; -} -*/ -// #cgo CFLAGS: -std=gnu11 -Wvla -import "C" - -const ABSTRACT_UNIX_SOCK_LEN int = C.ABSTRACT_UNIX_SOCK_LEN - -const POLLIN int = C.POLLIN -const POLLPRI int = C.POLLPRI -const POLLNVAL int = C.POLLNVAL -const POLLERR int = C.POLLERR -const POLLHUP int = C.POLLHUP -const POLLRDHUP int = C.POLLRDHUP - -func GetPollRevents(fd int, timeout int, flags int) (int, int, error) { - var err error - revents := C.int(0) - saved_errno := C.int(0) - - ret := C.get_poll_revents(C.int(fd), C.int(timeout), C.int(flags), &revents, &saved_errno) - if int(ret) < 0 { - err = syscall.Errno(saved_errno) - } - - return int(ret), int(revents), err -} - -func AbstractUnixSendFd(sockFD int, sendFD int) error { - fd := C.int(sendFD) - sk_fd := C.int(sockFD) - ret := C.lxc_abstract_unix_send_fds(sk_fd, &fd, C.int(1), nil, C.size_t(0)) - if ret < 0 { - return fmt.Errorf("Failed to send file descriptor via abstract unix socket") - } - - return nil -} - -func AbstractUnixReceiveFd(sockFD int) (*os.File, error) { - fd := C.int(-1) - sk_fd := C.int(sockFD) - ret := C.lxc_abstract_unix_recv_fds(sk_fd, &fd, C.int(1), nil, C.size_t(0)) - if ret < 0 { - return nil, fmt.Errorf("Failed to receive file descriptor via abstract unix socket") - } - - file := os.NewFile(uintptr(fd), "") - return file, nil -} - -func OpenPty(uid, gid int64) (master *os.File, slave *os.File, err error) { - fd_master := C.int(-1) - fd_slave := C.int(-1) - rootUid := C.uid_t(uid) - rootGid := C.gid_t(gid) - - C.create_pty(&fd_master, &fd_slave, rootUid, rootGid) - - if fd_master == -1 || fd_slave == -1 { - return nil, nil, errors.New("Failed to create a new pts pair") - } - - master = os.NewFile(uintptr(fd_master), "master") - slave = os.NewFile(uintptr(fd_slave), "slave") - - return master, slave, nil -} - -func Pipe() (master *os.File, slave *os.File, err error) { - fd_master := C.int(-1) - fd_slave := C.int(-1) - - C.create_pipe(&fd_master, &fd_slave) - - if fd_master == -1 || fd_slave == -1 { - return nil, nil, errors.New("Failed to create a new pipe") - } - - master = os.NewFile(uintptr(fd_master), "master") - slave = os.NewFile(uintptr(fd_slave), "slave") - - return master, slave, nil -} - -// UserId is an adaption from https://codereview.appspot.com/4589049. -func UserId(name string) (int, error) { - var pw C.struct_passwd - var result *C.struct_passwd - - bufSize := C.sysconf(C._SC_GETPW_R_SIZE_MAX) - if bufSize < 0 { - bufSize = 4096 - } - - buf := C.malloc(C.size_t(bufSize)) - if buf == nil { - return -1, fmt.Errorf("allocation failed") - } - defer C.free(buf) - - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - -again: - rv, errno := C.getpwnam_r(cname, - &pw, - (*C.char)(buf), - C.size_t(bufSize), - &result) - if rv < 0 { - // OOM killer will take care of us if we end up doing this too - // often. - if errno == syscall.ERANGE { - bufSize *= 2 - tmp := C.realloc(buf, C.size_t(bufSize)) - if tmp == nil { - return -1, fmt.Errorf("allocation failed") - } - buf = tmp - goto again - } - return -1, fmt.Errorf("failed user lookup: %s", syscall.Errno(rv)) - } - - if result == nil { - return -1, fmt.Errorf("unknown user %s", name) - } - - return int(C.int(result.pw_uid)), nil -} - -// GroupId is an adaption from https://codereview.appspot.com/4589049. -func GroupId(name string) (int, error) { - var grp C.struct_group - var result *C.struct_group - - bufSize := C.sysconf(C._SC_GETGR_R_SIZE_MAX) - if bufSize < 0 { - bufSize = 4096 - } - - buf := C.malloc(C.size_t(bufSize)) - if buf == nil { - return -1, fmt.Errorf("allocation failed") - } - - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - -again: - rv, errno := C.getgrnam_r(cname, - &grp, - (*C.char)(buf), - C.size_t(bufSize), - &result) - if rv != 0 { - // OOM killer will take care of us if we end up doing this too - // often. - if errno == syscall.ERANGE { - bufSize *= 2 - tmp := C.realloc(buf, C.size_t(bufSize)) - if tmp == nil { - return -1, fmt.Errorf("allocation failed") - } - buf = tmp - goto again - } - - C.free(buf) - return -1, fmt.Errorf("failed group lookup: %s", syscall.Errno(rv)) - } - C.free(buf) - - if result == nil { - return -1, fmt.Errorf("unknown group %s", name) - } - - return int(C.int(result.gr_gid)), nil -} - // --- pure Go functions --- func Major(dev uint64) int { @@ -622,180 +220,6 @@ func GetAllXattr(path string) (xattrs map[string]string, err error) { return xattrs, nil } -// Extensively commented directly in the code. Please leave the comments! -// Looking at this in a couple of months noone will know why and how this works -// anymore. -func ExecReaderToChannel(r io.Reader, bufferSize int, exited <-chan bool, fd int) <-chan []byte { - if bufferSize <= (128 * 1024) { - bufferSize = (128 * 1024) - } - - ch := make(chan ([]byte)) - - // Takes care that the closeChannel() function is exactly executed once. - // This allows us to avoid using a mutex. - var once sync.Once - closeChannel := func() { - close(ch) - } - - // [1]: This function has just one job: Dealing with the case where we - // are running an interactive shell session where we put a process in - // the background that does hold stdin/stdout open, but does not - // generate any output at all. This case cannot be dealt with in the - // following function call. Here's why: Assume the above case, now the - // attached child (the shell in this example) exits. This will not - // generate any poll() event: We won't get POLLHUP because the - // background process is holding stdin/stdout open and noone is writing - // to it. So we effectively block on GetPollRevents() in the function - // below. Hence, we use another go routine here who's only job is to - // handle that case: When we detect that the child has exited we check - // whether a POLLIN or POLLHUP event has been generated. If not, we know - // that there's nothing buffered on stdout and exit. - var attachedChildIsDead int32 = 0 - go func() { - <-exited - - atomic.StoreInt32(&attachedChildIsDead, 1) - - ret, revents, err := GetPollRevents(fd, 0, (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) - if ret < 0 { - logger.Errorf("Failed to poll(POLLIN | POLLPRI | POLLHUP | POLLRDHUP) on file descriptor: %s.", err) - } else if ret > 0 { - if (revents & POLLERR) > 0 { - logger.Warnf("Detected poll(POLLERR) event.") - } else if (revents & POLLNVAL) > 0 { - logger.Warnf("Detected poll(POLLNVAL) event.") - } - } else if ret == 0 { - logger.Debugf("No data in stdout: exiting.") - once.Do(closeChannel) - return - } - }() - - go func() { - readSize := (128 * 1024) - offset := 0 - buf := make([]byte, bufferSize) - avoidAtomicLoad := false - - defer once.Do(closeChannel) - for { - nr := 0 - var err error - - ret, revents, err := GetPollRevents(fd, -1, (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) - if ret < 0 { - // This condition is only reached in cases where we are massively f*cked since we even handle - // EINTR in the underlying C wrapper around poll(). So let's exit here. - logger.Errorf("Failed to poll(POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP) on file descriptor: %s. Exiting.", err) - return - } - - // [2]: If the process exits before all its data has been read by us and no other process holds stdin or - // stdout open, then we will observe a (POLLHUP | POLLRDHUP | POLLIN) event. This means, we need to - // keep on reading from the pty file descriptor until we get a simple POLLHUP back. - both := ((revents & (POLLIN | POLLPRI)) > 0) && ((revents & (POLLHUP | POLLRDHUP)) > 0) - if both { - logger.Debugf("Detected poll(POLLIN | POLLPRI | POLLHUP | POLLRDHUP) event.") - read := buf[offset : offset+readSize] - nr, err = r.Read(read) - } - - if (revents & POLLERR) > 0 { - logger.Warnf("Detected poll(POLLERR) event: exiting.") - return - } else if (revents & POLLNVAL) > 0 { - logger.Warnf("Detected poll(POLLNVAL) event: exiting.") - return - } - - if ((revents & (POLLIN | POLLPRI)) > 0) && !both { - // This might appear unintuitive at first but is actually a nice trick: Assume we are running - // a shell session in a container and put a process in the background that is writing to - // stdout. Now assume the attached process (aka the shell in this example) exits because we - // used Ctrl+D to send EOF or something. If no other process would be holding stdout open we - // would expect to observe either a (POLLHUP | POLLRDHUP | POLLIN | POLLPRI) event if there - // is still data buffered from the previous process or a simple (POLLHUP | POLLRDHUP) if - // no data is buffered. The fact that we only observe a (POLLIN | POLLPRI) event means that - // another process is holding stdout open and is writing to it. - // One counter argument that can be leveraged is (brauner looks at tycho :)) - // "Hey, you need to write at least one additional tty buffer to make sure that - // everything that the attached child has written is actually shown." - // The answer to that is: - // "This case can only happen if the process has exited and has left data in stdout which - // would generate a (POLLIN | POLLPRI | POLLHUP | POLLRDHUP) event and this case is already - // handled and triggers another codepath. (See [2].)" - if avoidAtomicLoad || atomic.LoadInt32(&attachedChildIsDead) == 1 { - avoidAtomicLoad = true - // Handle race between atomic.StorInt32() in the go routine - // explained in [1] and atomic.LoadInt32() in the go routine - // here: - // We need to check for (POLLHUP | POLLRDHUP) here again since we might - // still be handling a pure POLLIN event from a write prior to the childs - // exit. But the child might have exited right before and performed - // atomic.StoreInt32() to update attachedChildIsDead before we - // performed our atomic.LoadInt32(). This means we accidentally hit this - // codepath and are misinformed about the available poll() events. So we - // need to perform a non-blocking poll() again to exclude that case: - // - // - If we detect no (POLLHUP | POLLRDHUP) event we know the child - // has already exited but someone else is holding stdin/stdout open and - // writing to it. - // Note that his case should only ever be triggered in situations like - // running a shell and doing stuff like: - // > ./lxc exec xen1 -- bash - // root@xen1:~# yes & - // . - // . - // . - // now send Ctrl+D or type "exit". By the time the Ctrl+D/exit event is - // triggered, we will have read all of the childs data it has written to - // stdout and so we can assume that anything that comes now belongs to - // the process that is holding stdin/stdout open. - // - // - If we detect a (POLLHUP | POLLRDHUP) event we know that we've - // hit this codepath on accident caused by the race between - // atomic.StoreInt32() in the go routine explained in [1] and - // atomic.LoadInt32() in this go routine. So the next call to - // GetPollRevents() will either return - // (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP) - // or (POLLHUP | POLLRDHUP). Both will trigger another codepath (See [2].) - // that takes care that all data of the child that is buffered in - // stdout is written out. - ret, revents, err := GetPollRevents(fd, 0, (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) - if ret < 0 { - logger.Errorf("Failed to poll(POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP) on file descriptor: %s. Exiting.", err) - return - } else if (revents & (POLLHUP | POLLRDHUP | POLLERR | POLLNVAL)) == 0 { - logger.Debugf("Exiting but background processes are still running.") - return - } - } - read := buf[offset : offset+readSize] - nr, err = r.Read(read) - } - - // The attached process has exited and we have read all data that may have - // been buffered. - if ((revents & (POLLHUP | POLLRDHUP)) > 0) && !both { - logger.Debugf("Detected poll(POLLHUP) event: exiting.") - return - } - - offset += nr - if offset > 0 && (offset+readSize >= bufferSize || err != nil) { - ch <- buf[0:offset] - offset = 0 - buf = make([]byte, bufferSize) - } - } - }() - - return ch -} - var ObjectFound = fmt.Errorf("Found requested object") func LookupUUIDByBlockDevPath(diskDevice string) (string, error) { diff --git a/shared/util_linux_cgo.go b/shared/util_linux_cgo.go new file mode 100644 index 0000000000..aa80b9df0a --- /dev/null +++ b/shared/util_linux_cgo.go @@ -0,0 +1,588 @@ +// +build linux +// +build cgo + +package shared + +import ( + "errors" + "fmt" + "io" + "os" + "sync" + "sync/atomic" + "syscall" + "unsafe" + + "github.com/lxc/lxd/shared/logger" +) + +// #cgo LDFLAGS: -lutil -lpthread +/* +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <limits.h> +#include <poll.h> +#include <pty.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> + +#ifndef AT_SYMLINK_FOLLOW +#define AT_SYMLINK_FOLLOW 0x400 +#endif + +#ifndef AT_EMPTY_PATH +#define AT_EMPTY_PATH 0x1000 +#endif + +#define ABSTRACT_UNIX_SOCK_LEN sizeof(((struct sockaddr_un *)0)->sun_path) + +// This is an adaption from https://codereview.appspot.com/4589049, to be +// included in the stdlib with the stdlib's license. + +static int mygetgrgid_r(int gid, struct group *grp, + char *buf, size_t buflen, struct group **result) { + return getgrgid_r(gid, grp, buf, buflen, result); +} + +void configure_pty(int fd) { + struct termios term_settings; + struct winsize win; + + if (tcgetattr(fd, &term_settings) < 0) { + fprintf(stderr, "Failed to get settings: %s\n", strerror(errno)); + return; + } + + term_settings.c_iflag |= IMAXBEL; + term_settings.c_iflag |= IUTF8; + term_settings.c_iflag |= BRKINT; + term_settings.c_iflag |= IXANY; + + term_settings.c_cflag |= HUPCL; + + if (tcsetattr(fd, TCSANOW, &term_settings) < 0) { + fprintf(stderr, "Failed to set settings: %s\n", strerror(errno)); + return; + } + + if (ioctl(fd, TIOCGWINSZ, &win) < 0) { + fprintf(stderr, "Failed to get the terminal size: %s\n", strerror(errno)); + return; + } + + win.ws_col = 80; + win.ws_row = 25; + + if (ioctl(fd, TIOCSWINSZ, &win) < 0) { + fprintf(stderr, "Failed to set the terminal size: %s\n", strerror(errno)); + return; + } + + if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) { + fprintf(stderr, "Failed to set FD_CLOEXEC: %s\n", strerror(errno)); + return; + } + + return; +} + +void create_pty(int *master, int *slave, uid_t uid, gid_t gid) { + if (openpty(master, slave, NULL, NULL, NULL) < 0) { + fprintf(stderr, "Failed to openpty: %s\n", strerror(errno)); + return; + } + + configure_pty(*master); + configure_pty(*slave); + + if (fchown(*slave, uid, gid) < 0) { + fprintf(stderr, "Warning: error chowning pty to container root\n"); + fprintf(stderr, "Continuing...\n"); + } + if (fchown(*master, uid, gid) < 0) { + fprintf(stderr, "Warning: error chowning pty to container root\n"); + fprintf(stderr, "Continuing...\n"); + } +} + +void create_pipe(int *master, int *slave) { + int pipefd[2]; + + if (pipe2(pipefd, O_CLOEXEC) < 0) { + fprintf(stderr, "Failed to create a pipe: %s\n", strerror(errno)); + return; + } + + *master = pipefd[0]; + *slave = pipefd[1]; +} + +int get_poll_revents(int lfd, int timeout, int flags, int *revents, int *saved_errno) +{ + int ret; + struct pollfd pfd = {lfd, flags, 0}; + +again: + ret = poll(&pfd, 1, timeout); + if (ret < 0) { + if (errno == EINTR) + goto again; + + *saved_errno = errno; + fprintf(stderr, "Failed to poll() on file descriptor.\n"); + return -1; + } + + *revents = pfd.revents; + + return ret; +} + +int lxc_abstract_unix_send_fds(int fd, int *sendfds, int num_sendfds, + void *data, size_t size) +{ + int ret; + struct msghdr msg; + struct iovec iov; + struct cmsghdr *cmsg = NULL; + char buf[1] = {0}; + char *cmsgbuf; + size_t cmsgbufsize = CMSG_SPACE(num_sendfds * sizeof(int)); + + memset(&msg, 0, sizeof(msg)); + memset(&iov, 0, sizeof(iov)); + + cmsgbuf = malloc(cmsgbufsize); + if (!cmsgbuf) + return -1; + + msg.msg_control = cmsgbuf; + msg.msg_controllen = cmsgbufsize; + + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(num_sendfds * sizeof(int)); + + msg.msg_controllen = cmsg->cmsg_len; + + memcpy(CMSG_DATA(cmsg), sendfds, num_sendfds * sizeof(int)); + + iov.iov_base = data ? data : buf; + iov.iov_len = data ? size : sizeof(buf); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + ret = sendmsg(fd, &msg, MSG_NOSIGNAL); + if (ret < 0) + fprintf(stderr, "%s - Failed to send file descriptor\n", strerror(errno)); + free(cmsgbuf); + return ret; +} + +int lxc_abstract_unix_recv_fds(int fd, int *recvfds, int num_recvfds, + void *data, size_t size) +{ + int ret; + struct msghdr msg; + struct iovec iov; + struct cmsghdr *cmsg = NULL; + char buf[1] = {0}; + char *cmsgbuf; + size_t cmsgbufsize = CMSG_SPACE(num_recvfds * sizeof(int)); + + memset(&msg, 0, sizeof(msg)); + memset(&iov, 0, sizeof(iov)); + + cmsgbuf = malloc(cmsgbufsize); + if (!cmsgbuf) + return -1; + + msg.msg_control = cmsgbuf; + msg.msg_controllen = cmsgbufsize; + + iov.iov_base = data ? data : buf; + iov.iov_len = data ? size : sizeof(buf); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + ret = recvmsg(fd, &msg, 0); + if (ret <= 0) { + fprintf(stderr, "%s - Failed to receive file descriptor\n", strerror(errno)); + goto out; + } + + cmsg = CMSG_FIRSTHDR(&msg); + + memset(recvfds, -1, num_recvfds * sizeof(int)); + if (cmsg && cmsg->cmsg_len == CMSG_LEN(num_recvfds * sizeof(int)) && + cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { + memcpy(recvfds, CMSG_DATA(cmsg), num_recvfds * sizeof(int)); + } + +out: + free(cmsgbuf); + return ret; +} +*/ +// #cgo CFLAGS: -std=gnu11 -Wvla +import "C" + +const ABSTRACT_UNIX_SOCK_LEN int = C.ABSTRACT_UNIX_SOCK_LEN + +const POLLIN int = C.POLLIN +const POLLPRI int = C.POLLPRI +const POLLNVAL int = C.POLLNVAL +const POLLERR int = C.POLLERR +const POLLHUP int = C.POLLHUP +const POLLRDHUP int = C.POLLRDHUP + +func GetPollRevents(fd int, timeout int, flags int) (int, int, error) { + var err error + revents := C.int(0) + saved_errno := C.int(0) + + ret := C.get_poll_revents(C.int(fd), C.int(timeout), C.int(flags), &revents, &saved_errno) + if int(ret) < 0 { + err = syscall.Errno(saved_errno) + } + + return int(ret), int(revents), err +} + +func AbstractUnixSendFd(sockFD int, sendFD int) error { + fd := C.int(sendFD) + sk_fd := C.int(sockFD) + ret := C.lxc_abstract_unix_send_fds(sk_fd, &fd, C.int(1), nil, C.size_t(0)) + if ret < 0 { + return fmt.Errorf("Failed to send file descriptor via abstract unix socket") + } + + return nil +} + +func AbstractUnixReceiveFd(sockFD int) (*os.File, error) { + fd := C.int(-1) + sk_fd := C.int(sockFD) + ret := C.lxc_abstract_unix_recv_fds(sk_fd, &fd, C.int(1), nil, C.size_t(0)) + if ret < 0 { + return nil, fmt.Errorf("Failed to receive file descriptor via abstract unix socket") + } + + file := os.NewFile(uintptr(fd), "") + return file, nil +} + +func OpenPty(uid, gid int64) (master *os.File, slave *os.File, err error) { + fd_master := C.int(-1) + fd_slave := C.int(-1) + rootUid := C.uid_t(uid) + rootGid := C.gid_t(gid) + + C.create_pty(&fd_master, &fd_slave, rootUid, rootGid) + + if fd_master == -1 || fd_slave == -1 { + return nil, nil, errors.New("Failed to create a new pts pair") + } + + master = os.NewFile(uintptr(fd_master), "master") + slave = os.NewFile(uintptr(fd_slave), "slave") + + return master, slave, nil +} + +func Pipe() (master *os.File, slave *os.File, err error) { + fd_master := C.int(-1) + fd_slave := C.int(-1) + + C.create_pipe(&fd_master, &fd_slave) + + if fd_master == -1 || fd_slave == -1 { + return nil, nil, errors.New("Failed to create a new pipe") + } + + master = os.NewFile(uintptr(fd_master), "master") + slave = os.NewFile(uintptr(fd_slave), "slave") + + return master, slave, nil +} + +// UserId is an adaption from https://codereview.appspot.com/4589049. +func UserId(name string) (int, error) { + var pw C.struct_passwd + var result *C.struct_passwd + + bufSize := C.sysconf(C._SC_GETPW_R_SIZE_MAX) + if bufSize < 0 { + bufSize = 4096 + } + + buf := C.malloc(C.size_t(bufSize)) + if buf == nil { + return -1, fmt.Errorf("allocation failed") + } + defer C.free(buf) + + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + +again: + rv, errno := C.getpwnam_r(cname, + &pw, + (*C.char)(buf), + C.size_t(bufSize), + &result) + if rv < 0 { + // OOM killer will take care of us if we end up doing this too + // often. + if errno == syscall.ERANGE { + bufSize *= 2 + tmp := C.realloc(buf, C.size_t(bufSize)) + if tmp == nil { + return -1, fmt.Errorf("allocation failed") + } + buf = tmp + goto again + } + return -1, fmt.Errorf("failed user lookup: %s", syscall.Errno(rv)) + } + + if result == nil { + return -1, fmt.Errorf("unknown user %s", name) + } + + return int(C.int(result.pw_uid)), nil +} + +// GroupId is an adaption from https://codereview.appspot.com/4589049. +func GroupId(name string) (int, error) { + var grp C.struct_group + var result *C.struct_group + + bufSize := C.sysconf(C._SC_GETGR_R_SIZE_MAX) + if bufSize < 0 { + bufSize = 4096 + } + + buf := C.malloc(C.size_t(bufSize)) + if buf == nil { + return -1, fmt.Errorf("allocation failed") + } + + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + +again: + rv, errno := C.getgrnam_r(cname, + &grp, + (*C.char)(buf), + C.size_t(bufSize), + &result) + if rv != 0 { + // OOM killer will take care of us if we end up doing this too + // often. + if errno == syscall.ERANGE { + bufSize *= 2 + tmp := C.realloc(buf, C.size_t(bufSize)) + if tmp == nil { + return -1, fmt.Errorf("allocation failed") + } + buf = tmp + goto again + } + + C.free(buf) + return -1, fmt.Errorf("failed group lookup: %s", syscall.Errno(rv)) + } + C.free(buf) + + if result == nil { + return -1, fmt.Errorf("unknown group %s", name) + } + + return int(C.int(result.gr_gid)), nil +} + +// Extensively commented directly in the code. Please leave the comments! +// Looking at this in a couple of months noone will know why and how this works +// anymore. +func ExecReaderToChannel(r io.Reader, bufferSize int, exited <-chan bool, fd int) <-chan []byte { + if bufferSize <= (128 * 1024) { + bufferSize = (128 * 1024) + } + + ch := make(chan ([]byte)) + + // Takes care that the closeChannel() function is exactly executed once. + // This allows us to avoid using a mutex. + var once sync.Once + closeChannel := func() { + close(ch) + } + + // [1]: This function has just one job: Dealing with the case where we + // are running an interactive shell session where we put a process in + // the background that does hold stdin/stdout open, but does not + // generate any output at all. This case cannot be dealt with in the + // following function call. Here's why: Assume the above case, now the + // attached child (the shell in this example) exits. This will not + // generate any poll() event: We won't get POLLHUP because the + // background process is holding stdin/stdout open and noone is writing + // to it. So we effectively block on GetPollRevents() in the function + // below. Hence, we use another go routine here who's only job is to + // handle that case: When we detect that the child has exited we check + // whether a POLLIN or POLLHUP event has been generated. If not, we know + // that there's nothing buffered on stdout and exit. + var attachedChildIsDead int32 = 0 + go func() { + <-exited + + atomic.StoreInt32(&attachedChildIsDead, 1) + + ret, revents, err := GetPollRevents(fd, 0, (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) + if ret < 0 { + logger.Errorf("Failed to poll(POLLIN | POLLPRI | POLLHUP | POLLRDHUP) on file descriptor: %s.", err) + } else if ret > 0 { + if (revents & POLLERR) > 0 { + logger.Warnf("Detected poll(POLLERR) event.") + } else if (revents & POLLNVAL) > 0 { + logger.Warnf("Detected poll(POLLNVAL) event.") + } + } else if ret == 0 { + logger.Debugf("No data in stdout: exiting.") + once.Do(closeChannel) + return + } + }() + + go func() { + readSize := (128 * 1024) + offset := 0 + buf := make([]byte, bufferSize) + avoidAtomicLoad := false + + defer once.Do(closeChannel) + for { + nr := 0 + var err error + + ret, revents, err := GetPollRevents(fd, -1, (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) + if ret < 0 { + // This condition is only reached in cases where we are massively f*cked since we even handle + // EINTR in the underlying C wrapper around poll(). So let's exit here. + logger.Errorf("Failed to poll(POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP) on file descriptor: %s. Exiting.", err) + return + } + + // [2]: If the process exits before all its data has been read by us and no other process holds stdin or + // stdout open, then we will observe a (POLLHUP | POLLRDHUP | POLLIN) event. This means, we need to + // keep on reading from the pty file descriptor until we get a simple POLLHUP back. + both := ((revents & (POLLIN | POLLPRI)) > 0) && ((revents & (POLLHUP | POLLRDHUP)) > 0) + if both { + logger.Debugf("Detected poll(POLLIN | POLLPRI | POLLHUP | POLLRDHUP) event.") + read := buf[offset : offset+readSize] + nr, err = r.Read(read) + } + + if (revents & POLLERR) > 0 { + logger.Warnf("Detected poll(POLLERR) event: exiting.") + return + } else if (revents & POLLNVAL) > 0 { + logger.Warnf("Detected poll(POLLNVAL) event: exiting.") + return + } + + if ((revents & (POLLIN | POLLPRI)) > 0) && !both { + // This might appear unintuitive at first but is actually a nice trick: Assume we are running + // a shell session in a container and put a process in the background that is writing to + // stdout. Now assume the attached process (aka the shell in this example) exits because we + // used Ctrl+D to send EOF or something. If no other process would be holding stdout open we + // would expect to observe either a (POLLHUP | POLLRDHUP | POLLIN | POLLPRI) event if there + // is still data buffered from the previous process or a simple (POLLHUP | POLLRDHUP) if + // no data is buffered. The fact that we only observe a (POLLIN | POLLPRI) event means that + // another process is holding stdout open and is writing to it. + // One counter argument that can be leveraged is (brauner looks at tycho :)) + // "Hey, you need to write at least one additional tty buffer to make sure that + // everything that the attached child has written is actually shown." + // The answer to that is: + // "This case can only happen if the process has exited and has left data in stdout which + // would generate a (POLLIN | POLLPRI | POLLHUP | POLLRDHUP) event and this case is already + // handled and triggers another codepath. (See [2].)" + if avoidAtomicLoad || atomic.LoadInt32(&attachedChildIsDead) == 1 { + avoidAtomicLoad = true + // Handle race between atomic.StorInt32() in the go routine + // explained in [1] and atomic.LoadInt32() in the go routine + // here: + // We need to check for (POLLHUP | POLLRDHUP) here again since we might + // still be handling a pure POLLIN event from a write prior to the childs + // exit. But the child might have exited right before and performed + // atomic.StoreInt32() to update attachedChildIsDead before we + // performed our atomic.LoadInt32(). This means we accidentally hit this + // codepath and are misinformed about the available poll() events. So we + // need to perform a non-blocking poll() again to exclude that case: + // + // - If we detect no (POLLHUP | POLLRDHUP) event we know the child + // has already exited but someone else is holding stdin/stdout open and + // writing to it. + // Note that his case should only ever be triggered in situations like + // running a shell and doing stuff like: + // > ./lxc exec xen1 -- bash + // root@xen1:~# yes & + // . + // . + // . + // now send Ctrl+D or type "exit". By the time the Ctrl+D/exit event is + // triggered, we will have read all of the childs data it has written to + // stdout and so we can assume that anything that comes now belongs to + // the process that is holding stdin/stdout open. + // + // - If we detect a (POLLHUP | POLLRDHUP) event we know that we've + // hit this codepath on accident caused by the race between + // atomic.StoreInt32() in the go routine explained in [1] and + // atomic.LoadInt32() in this go routine. So the next call to + // GetPollRevents() will either return + // (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP) + // or (POLLHUP | POLLRDHUP). Both will trigger another codepath (See [2].) + // that takes care that all data of the child that is buffered in + // stdout is written out. + ret, revents, err := GetPollRevents(fd, 0, (POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) + if ret < 0 { + logger.Errorf("Failed to poll(POLLIN | POLLPRI | POLLERR | POLLHUP | POLLRDHUP) on file descriptor: %s. Exiting.", err) + return + } else if (revents & (POLLHUP | POLLRDHUP | POLLERR | POLLNVAL)) == 0 { + logger.Debugf("Exiting but background processes are still running.") + return + } + } + read := buf[offset : offset+readSize] + nr, err = r.Read(read) + } + + // The attached process has exited and we have read all data that may have + // been buffered. + if ((revents & (POLLHUP | POLLRDHUP)) > 0) && !both { + logger.Debugf("Detected poll(POLLHUP) event: exiting.") + return + } + + offset += nr + if offset > 0 && (offset+readSize >= bufferSize || err != nil) { + ch <- buf[0:offset] + offset = 0 + buf = make([]byte, bufferSize) + } + } + }() + + return ch +} +
_______________________________________________ lxc-devel mailing list [email protected] http://lists.linuxcontainers.org/listinfo/lxc-devel
