diff --git a/.travis.yml b/.travis.yml index 0f368fa..600819c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,8 +34,8 @@ deploy: file: - "./dist/tini" - "./dist/tini-static" - - "./dist/tini_0.8.0.deb" - - "./dist/tini_0.8.0.rpm" + - "./dist/tini_0.8.2.deb" + - "./dist/tini_0.8.2.rpm" on: repo: krallin/tini tags: true diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ad09d3..0eaf532 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project (tini C) # Config set (tini_VERSION_MAJOR 0) set (tini_VERSION_MINOR 8) -set (tini_VERSION_PATCH 0) +set (tini_VERSION_PATCH 2) # Extract git version and dirty-ness execute_process ( @@ -31,7 +31,7 @@ endif() # Flags add_definitions (-D_FORTIFY_SOURCE=2) -set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Wextra -Wall -pedantic -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat -Werror=format-security") +set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -Werror -Wextra -Wall -pedantic-errors -O2 -fstack-protector --param=ssp-buffer-size=4 -Wformat") set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-s") # Build diff --git a/Dockerfile b/Dockerfile index 7eb5bf2..0c7d8cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,4 +5,4 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* # Pre-install those here for faster local builds. -RUN CFLAGS="-DPR_SET_CHILD_SUBREAPER=36 -DPR_GET_CHILD_SUBREAPER=37" pip install psutil python-prctl +RUN CFLAGS="-DPR_SET_CHILD_SUBREAPER=36 -DPR_GET_CHILD_SUBREAPER=37" pip install psutil python-prctl bitmap diff --git a/README.md b/README.md index 25a5796..4b38183 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ In Docker, you will want to use an entrypoint so you don't have to remember to manually invoke Tini: # Add Tini - ENV TINI_VERSION v0.8.0 + ENV TINI_VERSION v0.8.2 ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini RUN chmod +x /tini ENTRYPOINT ["/tini", "--"] diff --git a/ci/run_build.sh b/ci/run_build.sh index 198fc25..e30eb20 100755 --- a/ci/run_build.sh +++ b/ci/run_build.sh @@ -3,11 +3,15 @@ set -o errexit set -o nounset +# Default compiler +: ${CC:="gcc"} + # Paths : ${SOURCE_DIR:="."} : ${DIST_DIR:="${SOURCE_DIR}/dist"} : ${BUILD_DIR:="/tmp/build"} + # Make those paths absolute, and export them for the Python tests to consume. export SOURCE_DIR="$(readlink -f "${SOURCE_DIR}")" export DIST_DIR="$(readlink -f "${DIST_DIR}")" @@ -56,6 +60,14 @@ for tini in "${BUILD_DIR}/tini" "${BUILD_DIR}/tini-static"; do exit 1 fi + # Test stdin / stdout are handed over to child + echo "Testing pipe" + echo "exit 0" | $tini -vvv sh + if [[ ! "$?" -eq "0" ]]; then + echo "Pipe test failed" + exit 1 + fi + # Move files to the dist dir for testing mkdir -p "${DIST_DIR}" cp "${BUILD_DIR}"/tini{,-static,*.rpm,*deb} "${DIST_DIR}" @@ -72,6 +84,9 @@ for tini in "${BUILD_DIR}/tini" "${BUILD_DIR}/tini-static"; do fi done +# Compile test code +"${CC}" -o "${BUILD_DIR}/sigconf-test" "${SOURCE_DIR}/test/sigconf/sigconf-test.c" + # Create virtual environment to run tests. # Accept system site packages for faster local builds. VENV="${BUILD_DIR}/venv" @@ -82,7 +97,7 @@ export PATH="${VENV}/bin:${PATH}" export CFLAGS # We need them to build our test suite, regardless of FORCE_SUBREAPER # Install test dependencies -pip install psutil python-prctl +pip install psutil python-prctl bitmap # Run tests python "${SOURCE_DIR}/test/run_inner_tests.py" diff --git a/ddist.sh b/ddist.sh index eed21f8..f2b6fb7 100755 --- a/ddist.sh +++ b/ddist.sh @@ -17,7 +17,7 @@ rm -f "${HERE}/dist"/* docker build -t "${IMG}" . # Run test without subreaper support, don't copy build files here -docker run --rm \ +docker run -it --rm \ --volume="${HERE}:${SRC}" \ -e BUILD_DIR=/tmp/tini-build \ -e SOURCE_DIR="${SRC}" \ diff --git a/src/tini.c b/src/tini.c index ab7f77b..40142ad 100644 --- a/src/tini.c +++ b/src/tini.c @@ -16,14 +16,20 @@ #include "tiniConfig.h" -#define PRINT_FATAL(...) fprintf(stderr, "[FATAL] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); -#define PRINT_WARNING(...) if (verbosity > 0) { fprintf(stderr, "[WARN ] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } -#define PRINT_INFO(...) if (verbosity > 1) { fprintf(stdout, "[INFO ] "); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } -#define PRINT_DEBUG(...) if (verbosity > 2) { fprintf(stdout, "[DEBUG] "); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } -#define PRINT_TRACE(...) if (verbosity > 3) { fprintf(stdout, "[TRACE] "); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } +#define PRINT_FATAL(...) fprintf(stderr, "[FATAL tini (%i)] ", getpid()); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); +#define PRINT_WARNING(...) if (verbosity > 0) { fprintf(stderr, "[WARN tini (%i)] ", getpid()); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } +#define PRINT_INFO(...) if (verbosity > 1) { fprintf(stdout, "[INFO tini (%i)] ", getpid()); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } +#define PRINT_DEBUG(...) if (verbosity > 2) { fprintf(stdout, "[DEBUG tini (%i)] ", getpid()); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } +#define PRINT_TRACE(...) if (verbosity > 3) { fprintf(stdout, "[TRACE tini (%i)] ", getpid()); fprintf(stdout, __VA_ARGS__); fprintf(stdout, "\n"); } #define ARRAY_LEN(x) (sizeof(x) / sizeof(x[0])) +typedef struct { + sigset_t* const sigmask_ptr; + struct sigaction* const sigttin_action_ptr; + struct sigaction* const sigttou_action_ptr; +} signal_configuration_t; + #ifdef PR_SET_CHILD_SUBREAPER #define HAS_SUBREAPER 1 @@ -55,24 +61,71 @@ static const char reaper_warning[] = "Tini is not running as PID 1 " #endif "run Tini as PID 1."; +int restore_signals(const signal_configuration_t* const sigconf_ptr) { + if (sigprocmask(SIG_SETMASK, sigconf_ptr->sigmask_ptr, NULL)) { + PRINT_FATAL("Restoring child signal mask failed: '%s'", strerror(errno)); + return 1; + } -int spawn(const sigset_t* const child_sigset_ptr, char* const argv[], int* const child_pid_ptr) { + if (sigaction(SIGTTIN, sigconf_ptr->sigttin_action_ptr, NULL)) { + PRINT_FATAL("Restoring SIGTTIN handler failed: '%s'", strerror((errno))); + return 1; + } + + if (sigaction(SIGTTOU, sigconf_ptr->sigttou_action_ptr, NULL)) { + PRINT_FATAL("Restoring SIGTTOU handler failed: '%s'", strerror((errno))); + return 1; + } + + return 0; +} + +int isolate_child() { + // Put the child into a new process group. + if (setpgid(0, 0) < 0) { + PRINT_FATAL("setpgid failed: '%s'", strerror(errno)); + return 1; + } + + // If there is a tty, allocate it to this new process group. We + // can do this in the child process because we're blocking + // SIGTTIN / SIGTTOU. + + // Doing it in the child process avoids a race condition scenario + // if Tini is calling Tini (in which case the grandparent may make the + // parent the foreground process group, and the actual child ends up... + // in the background!) + if (tcsetpgrp(STDIN_FILENO, getpgrp())) { + if (errno == ENOTTY) { + PRINT_DEBUG("tcsetpgrp failed: no tty (ok to proceed)") + } else { + PRINT_FATAL("tcsetpgrp failed: '%s'", strerror(errno)); + return 1; + } + } + + return 0; +} + + +int spawn(const signal_configuration_t* const sigconf_ptr, char* const argv[], int* const child_pid_ptr) { pid_t pid; + // TODO: check if tini was a foreground process to begin with (it's not OK to "steal" the foreground!") + pid = fork(); if (pid < 0) { PRINT_FATAL("Fork failed: '%s'", strerror(errno)); return 1; } else if (pid == 0) { - // Child - if (sigprocmask(SIG_SETMASK, child_sigset_ptr, NULL)) { - PRINT_FATAL("Setting child signal mask failed: '%s'", strerror(errno)); + + // Put the child in a process group and make it the foreground process if there is a tty. + if (isolate_child()) { return 1; } - // Put the child into a new process group - if (setpgid(0, 0) < 0) { - PRINT_FATAL("setpgid failed: '%s'", strerror(errno)); + // Restore all signal handlers to the way they were before we touched them. + if (restore_signals(sigconf_ptr)) { return 1; } @@ -207,27 +260,48 @@ void reaper_check () { } -int prepare_sigmask(sigset_t* const parent_sigset_ptr, sigset_t* const child_sigset_ptr) { - /* Prepare signals to block; make sure we don't block program error signals. */ +int configure_signals(sigset_t* const parent_sigset_ptr, const signal_configuration_t* const sigconf_ptr) { + /* Block all signals that are meant to be collected by the main loop */ if (sigfillset(parent_sigset_ptr)) { PRINT_FATAL("sigfillset failed: '%s'", strerror(errno)); return 1; } + // These ones shouldn't be collected by the main loop uint i; - int ignore_signals[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS} ; - for (i = 0; i < ARRAY_LEN(ignore_signals); i++) { - if (sigdelset(parent_sigset_ptr, ignore_signals[i])) { - PRINT_FATAL("sigdelset failed: '%i'", ignore_signals[i]); + int signals_for_tini[] = {SIGFPE, SIGILL, SIGSEGV, SIGBUS, SIGABRT, SIGTRAP, SIGSYS, SIGTTIN, SIGTTOU}; + for (i = 0; i < ARRAY_LEN(signals_for_tini); i++) { + if (sigdelset(parent_sigset_ptr, signals_for_tini[i])) { + PRINT_FATAL("sigdelset failed: '%i'", signals_for_tini[i]); return 1; } } - if (sigprocmask(SIG_SETMASK, parent_sigset_ptr, child_sigset_ptr)) { + if (sigprocmask(SIG_SETMASK, parent_sigset_ptr, sigconf_ptr->sigmask_ptr)) { PRINT_FATAL("sigprocmask failed: '%s'", strerror(errno)); return 1; } + // Handle SIGTTIN and SIGTTOU separately. Since Tini makes the child process group + // the foreground process group, there's a chance Tini can end up not controlling the tty. + // If TOSTOP is set on the tty, this could block Tini on writing debug messages. We don't + // want that. Ignore those signals. + struct sigaction ign_action; + memset(&ign_action, 0, sizeof ign_action); + + ign_action.sa_handler = SIG_IGN; + sigemptyset(&ign_action.sa_mask); + + if (sigaction(SIGTTIN, &ign_action, sigconf_ptr->sigttin_action_ptr)) { + PRINT_FATAL("Failed to ignore SIGTTIN"); + return 1; + } + + if (sigaction(SIGTTOU, &ign_action, sigconf_ptr->sigttou_action_ptr)) { + PRINT_FATAL("Failed to ignore SIGTTOU"); + return 1; + } + return 0; } @@ -345,10 +419,19 @@ int main(int argc, char *argv[]) { return 1; } - /* Prepare sigmask */ - sigset_t parent_sigset; - sigset_t child_sigset; - if (prepare_sigmask(&parent_sigset, &child_sigset)) { + /* Configure signals */ + sigset_t parent_sigset, child_sigset; + struct sigaction sigttin_action, sigttou_action; + memset(&sigttin_action, 0, sizeof sigttin_action); + memset(&sigttou_action, 0, sizeof sigttou_action); + + signal_configuration_t child_sigconf = { + .sigmask_ptr = &child_sigset, + .sigttin_action_ptr = &sigttin_action, + .sigttou_action_ptr = &sigttou_action, + }; + + if (configure_signals(&parent_sigset, &child_sigconf)) { return 1; } @@ -363,7 +446,7 @@ int main(int argc, char *argv[]) { reaper_check(); /* Go on */ - if (spawn(&child_sigset, *child_args_ptr, &child_pid)) { + if (spawn(&child_sigconf, *child_args_ptr, &child_pid)) { return 1; } free(child_args_ptr); diff --git a/test/run_inner_tests.py b/test/run_inner_tests.py index e332bf3..b1f9def 100755 --- a/test/run_inner_tests.py +++ b/test/run_inner_tests.py @@ -6,6 +6,11 @@ import signal import subprocess import time import psutil +import bitmap +import re + + +SIGNUM_TO_SIGNAME = dict((v, k) for k,v in signal.__dict__.items() if re.match("^SIG[A-Z]+$", k)) def busy_wait(condition_callable, timeout): @@ -55,32 +60,58 @@ def main(): # Run the signals test - for signame in "SIGINT", "SIGTERM": - print "running signal test for: {0} ({1} with env {2})".format(signame, " ".join(target), env) + for signum in [signal.SIGINT, signal.SIGTERM]: + print "running signal test for: {0} ({1} with env {2})".format(SIGNUM_TO_SIGNAME[signum], " ".join(target), env) p = subprocess.Popen(target + [os.path.join(src, "test", "signals", "test.py")], env=dict(os.environ, **env)) - sig = getattr(signal, signame) - p.send_signal(sig) + p.send_signal(signum) ret = p.wait() - assert ret == -sig, "Signals test failed!" + assert ret == -signum, "Signals test failed (ret was {0}, expected {1})".format(ret, -signum) + # Run the process group test # This test has Tini spawn a process that ignores SIGUSR1 and spawns a child that doesn't (and waits on the child) # We send SIGUSR1 to Tini, and expect the grand-child to terminate, then the child, and then Tini. print "Running process group test" - p = subprocess.Popen([tini, '-g', '--', os.path.join(src, "test", "pgroup", "stage_1.py")], env=dict(os.environ, **env)) + p = subprocess.Popen([tini, '-g', '--', os.path.join(src, "test", "pgroup", "stage_1.py")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) busy_wait(lambda: len(psutil.Process(p.pid).children(recursive=True)) == 2, 10) p.send_signal(signal.SIGUSR1) busy_wait(lambda: p.poll() is not None, 10) + # Run failing test - print "Running failing test" + print "Running zombie reaping failure test (Tini should warn)" p = subprocess.Popen([tini, "--", os.path.join(src, "test", "reaping", "stage_1.py")], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() assert "zombie reaping won't work" in err, "No warning message was output!" ret = p.wait() assert ret == 1, "Reaping test succeeded (it should have failed)!" + + # Test that the signals are properly in place here. + print "running signal configuration test" + + p = subprocess.Popen([os.path.join(build, "sigconf-test"), tini, '-g', '--', "cat", "/proc/self/status"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = p.communicate() + + # Extract the signal properties, and add a zero at the end. + props = [line.split(":") for line in out.splitlines()] + props = [(k.strip(), v.strip()) for (k, v) in props] + props = [(k, bitmap.BitMap.fromstring(bin(int(v, 16))[2:].zfill(32))) for (k, v) in props if k in ["SigBlk", "SigIgn", "SigCgt"]] + props = dict(props) + + # Print actual handling configuration + for k, bmp in props.items(): + print "{0}: {1}".format(k, ", ".join(["{0} ({1})".format(SIGNUM_TO_SIGNAME[n+1], n+1) for n in bmp.nonzero()])) + + for signal_set_name, signals_to_test_for in [ + ("SigIgn", [signal.SIGTTOU, signal.SIGSEGV, signal.SIGINT,]), + ("SigBlk", [signal.SIGTTIN, signal.SIGILL, signal.SIGTERM,]), + ]: + for signum in signals_to_test_for: + # Use signum - 1 because the bitmap is 0-indexed but represents signals strting at 1 + assert (signum - 1) in props[signal_set_name].nonzero(), "{0} ({1}) is missing in {2}!".format(SIGNUM_TO_SIGNAME[signum], signum, signal_set_name) + print "---------------------------" print "All done, tests as expected" print "---------------------------" diff --git a/test/run_outer_tests.py b/test/run_outer_tests.py index 924189e..176bca7 100755 --- a/test/run_outer_tests.py +++ b/test/run_outer_tests.py @@ -5,6 +5,13 @@ import time import pipes import subprocess import threading +import pexpect +import signal + + +class ReturnContainer(): + def __init__(self): + self.value = None class Command(object): @@ -27,6 +34,8 @@ class Command(object): self.stdout, self.stderr = self.proc.communicate() thread = threading.Thread(target=target) + thread.daemon = True + thread.start() if self.post_cmd is not None: @@ -55,7 +64,52 @@ class Command(object): print "OK" -if __name__ == "__main__": +def attach_and_type_exit_0(name): + p = pexpect.spawn("docker attach {0}".format(name)) + p.sendline('') + p.sendline('exit 0') + + +def attach_and_issue_ctrl_c(name): + p = pexpect.spawn("docker attach {0}".format(name)) + p.expect_exact('#') + p.sendintr() + + +def test_tty_handling(img, name, base_cmd, fail_cmd, container_command, exit_function, expect_exit_code): + print "Testing TTY handling (using container command '{0}' and exit function '{1}')".format(container_command, exit_function.__name__) + rc = ReturnContainer() + + shell_ready_event = threading.Event() + + def spawn(): + p = pexpect.spawn(" ".join(base_cmd + ["--tty", "--interactive", img, "/tini/dist/tini", "-vvv", "--", container_command])) + p.expect_exact("#") + shell_ready_event.set() + rc.value = p.wait() + + thread = threading.Thread(target=spawn) + thread.daemon = True + + thread.start() + + if not shell_ready_event.wait(2): + raise Exception("Timeout waiting for shell to spawn") + + exit_function(name) + + thread.join(timeout=2) + + if thread.is_alive(): + subprocess.check_call(fail_cmd) + raise Exception("Timeout waiting for container to exit!") + + if rc.value != expect_exit_code: + raise Exception("Return code is: {0} (expected {1})".format(rc.value, expect_exit_code)) + + + +def main(): img = sys.argv[1] name = "{0}-test".format(img) @@ -69,8 +123,7 @@ if __name__ == "__main__": "--name={0}".format(name), ] - fail_cmd = ["docker", "kill", name] - + fail_cmd = ["docker", "kill", "-s", "KILL", name] # Funtional tests for entrypoint in ["/tini/dist/tini", "/tini/dist/tini-static"]: @@ -109,3 +162,12 @@ if __name__ == "__main__": ["centos:7", "rpm", "rpm"], ]: Command(base_cmd + [image, "sh", "-c", "{0} -i /tini/dist/*.{1} && /usr/bin/tini true".format(pkg_manager, extension)], fail_cmd).run() + + + # Test tty handling + test_tty_handling(img, name, base_cmd, fail_cmd, "dash", attach_and_type_exit_0, 0) + test_tty_handling(img, name, base_cmd, fail_cmd, "dash -c 'while true; do echo \#; sleep 0.1; done'", attach_and_issue_ctrl_c, 128 + signal.SIGINT) + + +if __name__ == "__main__": + main() diff --git a/test/sigconf/sigconf-test.c b/test/sigconf/sigconf-test.c new file mode 100644 index 0000000..23c9a81 --- /dev/null +++ b/test/sigconf/sigconf-test.c @@ -0,0 +1,29 @@ +/* +Test program to: ++ Ignore a few signals ++ Block a few signals ++ Exec whatever the test runner asked for +*/ + +#include +#include +#include + + +int main(int argc, char *argv[]) { + // Signals to ignore + signal(SIGTTOU, SIG_IGN); // This one should still be in SigIgn (Tini touches it to ignore it, and should restore it) + signal(SIGSEGV, SIG_IGN); // This one should still be in SigIgn (Tini shouldn't touch it) + signal(SIGINT, SIG_IGN); // This one should still be in SigIgn (Tini should block it to forward it, and restore it) + + // Signals to block + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGTTIN); // This one should still be in SigIgn (Tini touches it to ignore it, and should restore it) + sigaddset(&set, SIGILL); // This one should still be in SigIgn (Tini shouldn't touch it) + sigaddset(&set, SIGTERM); // This one should still be in SigIgn (Tini should block it to forward it, and restore it) + sigprocmask(SIG_BLOCK, &set, NULL); + + // Run whatever we were asked to run + execvp(argv[1], argv+1); +}