diff --git a/.travis.yml b/.travis.yml index 84a3ad6..048a2da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,15 @@ script: make test +sudo: false +language: python +python: + - "2.7" +install: + - env -u CFLAGS -u LDFLAGS pip install -r test/requirements.txt +addons: + apt: + packages: + - gcc-multilib + - libcap-dev env: - CFLAGS=-m32 LDFLAGS=-m32 DISABLE_TESTS=1 - CFLAGS=-m64 LDFLAGS=-m64 -before_install: - - sudo apt-get update - - sudo apt-get -y install gcc-multilib python-pexpect diff --git a/Makefile b/Makefile index 0fb5b0d..7127912 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -override CFLAGS+=-Wall -Werror -D_GNU_SOURCE -g +override CFLAGS := -Wall -Werror -D_GNU_SOURCE -g $(CFLAGS) OBJS=reptyr.o reallocarray.o attach.o UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Linux) @@ -8,11 +8,15 @@ ifeq ($(UNAME_S),FreeBSD) OBJS += platform/freebsd/freebsd_ptrace.o platform/freebsd/freebsd.o LDFLAGS += -lprocstat endif -# Note that because of how Make works, this can be overriden from the +# Note that because of how Make works, this can be overridden from the # command-line. # # e.g. install to /usr with `make PREFIX=/usr` PREFIX=/usr/local +BINDIR=$(PREFIX)/bin +MANDIR=$(PREFIX)/share/man + +PKG_CONFIG ?= pkg-config all: reptyr @@ -39,12 +43,18 @@ ptrace.o: ptrace.h platform/platform.h $(wildcard platform/*/arch/*.h) clean: rm -f reptyr $(OBJS) test/victim.o test/victim +BASHCOMPDIR ?= $(shell $(PKG_CONFIG) --variable=completionsdir bash-completion 2>/dev/null) + install: reptyr - install -d -m 755 $(DESTDIR)$(PREFIX)/bin/ - install -m 755 reptyr $(DESTDIR)$(PREFIX)/bin/reptyr - install -d -m 755 $(DESTDIR)$(PREFIX)/share/man/man1 - install -m 644 reptyr.1 $(DESTDIR)$(PREFIX)/share/man/man1/reptyr.1 - install -d -m 755 $(DESTDIR)$(PREFIX)/share/man/fr/man1 - install -m 644 reptyr.fr.1 $(DESTDIR)$(PREFIX)/share/man/fr/man1/reptyr.1 + install -d -m 755 $(DESTDIR)$(BINDIR) + install -m 755 reptyr $(DESTDIR)$(BINDIR)/reptyr + install -d -m 755 $(DESTDIR)$(MANDIR)/man1 + install -m 644 reptyr.1 $(DESTDIR)$(MANDIR)/man1/reptyr.1 + install -d -m 755 $(DESTDIR)$(MANDIR)/fr/man1 + install -m 644 reptyr.fr.1 $(DESTDIR)$(MANDIR)/fr/man1/reptyr.1 + bashcompdir=$(BASHCOMPDIR) ; \ + test -z "$$bashcompdir" && bashcompdir=/etc/bash_completion.d ; \ + install -d -m 755 $(DESTDIR)$$bashcompdir ; \ + install -m 644 reptyr.bash $(DESTDIR)$$bashcompdir/reptyr .PHONY: PHONY diff --git a/README.md b/README.md index cba124c..6c3c6c5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,21 @@ background it, you will still have to run "bg" or "fg" in the old terminal. This is likely impossible to fix in a reasonable way without patching your shell.) +Typical usage pattern +--------------------- + +* Start a long running process, e.g. `top` +* Background the process with CTRL-Z +* Resume the process in the background: `bg` +* Display your running background jobs with `jobs -l`, this should look like this: + * `[1]+ 4711 Stopped (signal) top` + * (The `-l` in `jobs -l` makes sure you'll get the PID) +* Disown the jobs from the current parent with `disown top`. After that, `jobs` will not show the job any more, but `ps -a` will. +* Start your terminal multiplexer of choice, e.g. `tmux` +* Reattach to the backgrounded process: `reptyr 4711` +* Detach your terminal multiplexer (e.g. CTRL-A D) and close ssh +* Reconnect ssh, attach to your multiplexer (e.g. `tmux attach`), rejoice! + "But wait, isn't this just screenify?" -------------------------------------- @@ -41,11 +56,16 @@ accomplishes this. PORTABILITY ----------- -reptyr is Linux-only. It uses ptrace to attach to the target and control it at -the syscall level, so it is highly dependent on Linux's particular syscall API, -syscalls, and terminal ioctl()s. A port to Solaris or BSD may be technically -feasible, but would probably require significant re-architecting to abstract out -the platform-specific bits. +reptyr supports Linux and FreeBSD. Not all functionality is currently +available on FreeBSD. (Notably, FreeBSD doesn't support `reptyr -T` at +this time. + +`reptyr` uses ptrace to attach to the target and control it at the +syscall level, so it is highly dependent on details of the syscall +API, available syscalls, and terminal ioctl()s. A port to other +operating systems may be technically feasible, but requires +significant low-level knowledge of the relevant platform, and may +entail significant refactors. reptyr works on i386, x86_64, and ARM. Ports to other architectures should be straightforward, and should in most cases be as simple as adding an arch/ARCH.h diff --git a/Vagrantfile b/Vagrantfile index d8696e2..f0319f9 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -23,6 +23,14 @@ EOS machine.vm.box = 'chef/fedora-20' end + config.vm.define 'centos-6-x86_64' do |machine| + machine.vm.box = 'bento/centos-6.7' + end + + config.vm.define 'centos-5-x86_64' do |machine| + machine.vm.box = 'bento/centos-5.11' + end + config.vm.synced_folder ".", "/vagrant", type: 'nfs', id: 'vagrant-root' end diff --git a/attach.c b/attach.c index 4e65d25..8d9cbf8 100644 --- a/attach.c +++ b/attach.c @@ -237,6 +237,16 @@ int grab_pid(pid_t pid, struct ptrace_child *child, child_addr_t *scratch) { return err; } +int preflight_check(pid_t pid) { + struct ptrace_child child; + debug("Making sure we have permission to attach..."); + if (ptrace_attach_child(&child, pid)) { + return child.error; + } + ptrace_detach_child(&child); + return 0; +} + int attach_child(pid_t pid, const char *pty, int force_stdio) { struct ptrace_child child; child_addr_t scratch_page = -1; @@ -252,6 +262,10 @@ int attach_child(pid_t pid, const char *pty, int force_stdio) { return err; } + if ((err = preflight_check(pid))) { + return err; + } + debug("Using tty: %s", pty); if ((err = copy_tty_state(pid, pty))) { @@ -375,8 +389,11 @@ int setup_steal_socket(struct steal_pty_state *steal) { return errno; steal->addr_un.sun_family = AF_UNIX; - snprintf(steal->addr_un.sun_path, sizeof(steal->addr_un.sun_path), - "%s/reptyr.sock", steal->tmpdir); + if (snprintf(steal->addr_un.sun_path, sizeof(steal->addr_un.sun_path), + "%s/reptyr.sock", steal->tmpdir) >= sizeof(steal->addr_un.sun_path)) { + error("tmpdir path too long!"); + return ENAMETOOLONG; + } if ((steal->sockfd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) return errno; @@ -384,9 +401,9 @@ int setup_steal_socket(struct steal_pty_state *steal) { if (bind(steal->sockfd, &steal->addr, sizeof(steal->addr_un)) < 0) return errno; - if (chown(steal->addr_un.sun_path, steal->target_stat.uid, steal->target_stat.gid) < 0) + if (chown(steal->addr_un.sun_path, steal->emulator_uid, -1) < 0) debug("chown %s: %s", steal->addr_un.sun_path, strerror(errno)); - if (chown(steal->tmpdir, steal->target_stat.uid, steal->target_stat.gid) < 0) + if (chown(steal->tmpdir, steal->emulator_uid, -1) < 0) debug("chown %s: %s", steal->tmpdir, strerror(errno)); return 0; @@ -484,7 +501,7 @@ int steal_child_pty(struct steal_pty_state *steal) { // it doesn't die. int steal_block_hup(struct steal_pty_state *steal) { struct ptrace_child leader; - child_addr_t scratch; + child_addr_t scratch = 0; int err = 0; if ((err = grab_pid(steal->target_stat.sid, &leader, &scratch))) @@ -532,6 +549,9 @@ int steal_pty(pid_t pid, int *pty) { struct steal_pty_state steal = {}; long page_size = sysconf(_SC_PAGE_SIZE); + if ((err = preflight_check(pid))) + goto out; + if ((err = get_terminal_state(&steal, pid))) goto out; diff --git a/platform/freebsd/freebsd.h b/platform/freebsd/freebsd.h index 6b7d41d..e4dce47 100644 --- a/platform/freebsd/freebsd.h +++ b/platform/freebsd/freebsd.h @@ -39,9 +39,9 @@ #include #include -#define do_socketcall(child, name, a0, a1, a2, a3, a4) \ +#define do_socketcall(child, scratch, name, a0, a1, a2, a3, a4) \ ({ \ - int __ret=-1; \ + int __ret=-1; \ if (ptrace_syscall_numbers((child))->nr_##name) { \ __ret = do_syscall((child), name, a0, a1, a2, a3, a4, 0); \ } \ diff --git a/platform/freebsd/freebsd_ptrace.c b/platform/freebsd/freebsd_ptrace.c index de1c708..9632778 100644 --- a/platform/freebsd/freebsd_ptrace.c +++ b/platform/freebsd/freebsd_ptrace.c @@ -47,7 +47,7 @@ static int __ptrace_command(struct ptrace_child *child, int req, void *, int); -#define ptrace_command(cld, req, ...) _ptrace_command(cld, req, ## __VA_ARGS__, NULL, NULL) +#define ptrace_command(cld, req, ...) _ptrace_command(cld, req, ## __VA_ARGS__, 0, 0) #define _ptrace_command(cld, req, addr, data, ...) __ptrace_command((cld), (req), (void*)(addr), (int)(data)) diff --git a/platform/linux/arch/amd64.h b/platform/linux/arch/amd64.h index b161035..63c25e7 100644 --- a/platform/linux/arch/amd64.h +++ b/platform/linux/arch/amd64.h @@ -65,7 +65,7 @@ struct syscall_numbers arch_syscall_numbers[2] = { * include unistd_32.h, but those definitions would conflict with the * standard ones. So, let's just hardcode the values for now. Probably * we should generate this from unistd_32.h during the build process or - * soemthing. + * something. */ .nr_mmap = 90, .nr_mmap2 = 192, diff --git a/platform/linux/linux.c b/platform/linux/linux.c index 45a6ff1..448c6a2 100644 --- a/platform/linux/linux.c +++ b/platform/linux/linux.c @@ -46,13 +46,6 @@ int parse_proc_stat(int statfd, struct proc_stat *out) { } out->ctty = dev; - struct stat st; - if (fstat(statfd, &st) != 0) - return assert_nonzero(errno); - - out->uid = st.st_uid; - out->gid = st.st_gid; - return 0; } @@ -67,8 +60,51 @@ int read_proc_stat(pid_t pid, struct proc_stat *out) { error("Unable to open %s: %s", stat_path, strerror(errno)); return -statfd; } - err = parse_proc_stat(statfd, out); + + + close(statfd); + return err; +} + +int read_uid(pid_t pid, uid_t *out) { + char stat_path[PATH_MAX]; + char buf[1024]; + int statfd; + int err = 0; + int n; + char *p = buf; + + snprintf(stat_path, sizeof stat_path, "/proc/%d/status", pid); + statfd = open(stat_path, O_RDONLY); + if (statfd < 0) { + error("Unable to open %s: %s", stat_path, strerror(errno)); + return -statfd; + } + + if ((n = read(statfd, buf, sizeof(buf))) < 0) { + err = assert_nonzero(errno); + goto out; + } + while (p < buf + n) { + if (strncmp(p, "Uid:\t", strlen("Uid:\t")) == 0) + break; + p = memchr(p, '\n', buf+n-p); + if (p == NULL) + break; + p++; + continue; + } + if (p == NULL || p >= buf + n) { + debug("Unable to parse emulator uid: no Uid line found"); + *out = -1; + goto out; + } + if(sscanf(p, "Uid:\t%d", out) < 0) { + debug("Unable to parse emulator uid: unparseable Uid line"); + } + + out: close(statfd); return err; } @@ -148,7 +184,7 @@ int check_pgroup(pid_t target) { memcpy(pid_stat.comm, "???", 4); } error("Process %d (%.*s) shares %d's process group. Unable to attach.\n" - "(This most commonly means that %d has suprocesses).", + "(This most commonly means that %d has sub-processes).", (int)pid, TASK_COMM_LENGTH, pid_stat.comm, (int)target, (int)target); err = EINVAL; goto out; @@ -192,9 +228,10 @@ int *get_child_tty_fds(struct ptrace_child *child, int statfd, int *count) { } if (stat("/dev/console", &console_st) < 0) { - child->error = errno; error("Unable to stat /dev/console"); - return NULL; + console_st = (struct stat){ + .st_rdev = -1, + }; } snprintf(buf, sizeof buf, "/proc/%d/fd/", child->pid); @@ -237,6 +274,9 @@ int get_terminal_state(struct steal_pty_state *steal, pid_t target) { if ((err = find_terminal_emulator(steal))) return err; + if ((err = read_uid(steal->emulator_pid, &steal->emulator_uid))) + return err; + return 0; } diff --git a/platform/linux/linux.h b/platform/linux/linux.h index 3ad600f..3ec5a99 100644 --- a/platform/linux/linux.h +++ b/platform/linux/linux.h @@ -24,6 +24,14 @@ #define LINUX_H #ifdef __linux__ + +// Important that we include before , for +// compatibility with certain older libcs, in which +// #define's some symbols that defines via +// `enum. c.f. https://bugs.launchpad.net/ubuntu/+source/eglibc/+bug/1261872/ +// and https://github.com/nelhage/reptyr/issues/67 +#include + #include #include #include @@ -32,8 +40,8 @@ #include #include #include +#include #include -#include #include #include #include diff --git a/platform/linux/linux_ptrace.c b/platform/linux/linux_ptrace.c index 5549571..893d4af 100644 --- a/platform/linux/linux_ptrace.c +++ b/platform/linux/linux_ptrace.c @@ -47,8 +47,13 @@ typeof(y) _min2 = (y); \ _min1 < _min2 ? _min1 : _min2; }) +#ifdef PTRACE_TRACEME +static long __ptrace_command(struct ptrace_child *child, int req, + void *, void*); +#else static long __ptrace_command(struct ptrace_child *child, enum __ptrace_request req, void *, void*); +#endif #define ptrace_command(cld, req, ...) _ptrace_command(cld, req, ## __VA_ARGS__, NULL, NULL) #define _ptrace_command(cld, req, addr, data, ...) __ptrace_command((cld), (req), (void*)(addr), (void*)(data)) @@ -293,8 +298,13 @@ int ptrace_memcpy_from_child(struct ptrace_child *child, void *dst, child_addr_t return 0; } +#ifdef PTRACE_TRACEME +static long __ptrace_command(struct ptrace_child *child, int req, + void *addr, void *data) { +#else static long __ptrace_command(struct ptrace_child *child, enum __ptrace_request req, void *addr, void *data) { +#endif long rv; errno = 0; rv = ptrace(req, child->pid, addr, data); diff --git a/platform/platform.h b/platform/platform.h index 5f10066..ca0fd88 100644 --- a/platform/platform.h +++ b/platform/platform.h @@ -23,6 +23,10 @@ #ifndef PLATFORM_H #define PLATFORM_H +#ifdef __APPLE__ +#error "reptyr does not currently support macOS" +#endif + #include "linux/linux.h" #include "freebsd/freebsd.h" #include "../ptrace.h" @@ -45,14 +49,14 @@ struct proc_stat { char state; pid_t ppid, sid, pgid; dev_t ctty; - uid_t uid; - gid_t gid; }; struct steal_pty_state { struct proc_stat target_stat; pid_t emulator_pid; + uid_t emulator_uid; + struct fd_array master_fds; char tmpdir[PATH_MAX]; diff --git a/reptyr.bash b/reptyr.bash new file mode 100644 index 0000000..2eb8d9b --- /dev/null +++ b/reptyr.bash @@ -0,0 +1,19 @@ +# bash completion for reptyr(1) + +_reptyr() +{ + case $3 in + -l|-L|-h|-v) return ;; + esac + + if [[ $2 == -* ]]; then + COMPREPLY=( $(compgen -W '-l -L -s -T -h -v -V' -- "$2") ) + return + fi + + case $3 in + [1-9]*) ;; + *) COMPREPLY=( $(compgen -W '$(command ps axo pid=)' -- "$2") ) ;; + esac +} && +complete -F _reptyr reptyr diff --git a/reptyr.c b/reptyr.c index 4563af6..1b8f587 100644 --- a/reptyr.c +++ b/reptyr.c @@ -124,23 +124,34 @@ void do_proxy(int pty) { char buf[4096]; ssize_t count; fd_set set; - struct timeval timeout; + sigset_t mask; + sigset_t select_mask; + struct sigaction sa; + + // Block WINCH while we're outside the select, but unblock it + // while we're inside: + sigemptyset(&mask); + sigaddset(&mask, SIGWINCH); + if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) { + fprintf(stderr, "sigprocmask: %m"); + return; + } + sa.sa_handler = do_winch; + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sigaction(SIGWINCH, &sa, NULL); + resize_pty(pty); + while (1) { if (winch_happened) { winch_happened = 0; - /* - * FIXME: If a signal comes in after this point but before - * select(), the resize will be delayed until we get more - * input. signalfd() is probably the cleanest solution. - */ resize_pty(pty); } FD_ZERO(&set); FD_SET(0, &set); FD_SET(pty, &set); - timeout.tv_sec = 0; - timeout.tv_usec = 1000; - if (select(pty + 1, &set, NULL, NULL, &timeout) < 0) { + sigemptyset(&select_mask); + if (pselect(pty + 1, &set, NULL, NULL, NULL, &select_mask) < 0) { if (errno == EINTR) continue; fprintf(stderr, "select: %m"); @@ -179,7 +190,6 @@ void usage(char *me) { int main(int argc, char **argv) { struct termios saved_termios; - struct sigaction act; int pty; int opt; int err; @@ -287,11 +297,6 @@ int main(int argc, char **argv) { } setup_raw(&saved_termios); - memset(&act, 0, sizeof act); - act.sa_handler = do_winch; - act.sa_flags = 0; - sigaction(SIGWINCH, &act, NULL); - resize_pty(pty); do_proxy(pty); do { errno = 0; diff --git a/reptyr.fr.1 b/reptyr.fr.1 index 3bf82d5..2007f49 100644 --- a/reptyr.fr.1 +++ b/reptyr.fr.1 @@ -51,7 +51,7 @@ même si vous fermez le shell sans le faire. .B \-l .IP Plutôt que d'attacher un nouveau processus, crée un couveau couple de -pty, redirige l'extrémité maîtresse vers le terminal en corus, puis +pty, redirige l'extrémité maîtresse vers le terminal en cours, puis affiche le nom du pty esclave. Il pourra être passé en argument par exemple à l'option .I set inferior-tty diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 0000000..f5cca46 --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1,2 @@ +pexpect +python-prctl>1.6 diff --git a/test/tty-steal.py b/test/tty-steal.py index 3e1c510..c3e1f8b 100644 --- a/test/tty-steal.py +++ b/test/tty-steal.py @@ -3,9 +3,17 @@ import sys if os.getenv("NO_TEST_STEAL") is not None: - print "Skipping tty-stealing tests because $NO_TEST_STEAL is set." + print("Skipping tty-stealing tests because $NO_TEST_STEAL is set.") sys.exit(0) +try: + import prctl + PR_SET_PTRACER_ANY = 0xffffffff + if hasattr(prctl, 'set_ptracer'): + prctl.set_ptracer(PR_SET_PTRACER_ANY) +except ImportError: + print("Unable to import `prctl`, skipping `PR_SET_PTRACER`.") + child = pexpect.spawn("test/victim") child.setecho(False) child.sendline("hello") diff --git a/test/victim.c b/test/victim.c index cb1bf93..60b5ce8 100644 --- a/test/victim.c +++ b/test/victim.c @@ -1,9 +1,14 @@ #include +#include int main(int argc, char **argv) { char *line = NULL; size_t cap = 0; +#ifdef PR_SET_PTRACER + prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY); +#endif + while(getline(&line, &cap, stdin) != -1) { printf("ECHO: %s", line); }