├── .gitignore ├── LICENSE.txt ├── README.md ├── build.sh ├── include ├── compiler.h ├── libc.h └── sigcheck.h ├── scripts ├── async_safe_syms ├── candidates ├── examples ├── gendefs.py ├── runtests.sh └── sigcheck ├── src ├── interceptors.c ├── libc.c └── sigcheck.c └── tests ├── sigaction-1.c ├── sigaction-1.c.log ├── signal-1.c ├── signal-1.c.log ├── signal-2.c └── signal-2.c.log /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | *.swp 3 | *.o 4 | *.i 5 | *.s 6 | core 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2020 Yury Gribov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | Signal Checker is a small proof-of-concept tool for detecting 4 | unsafe signal handlers. C standard requires signal handlers 5 | to only call reentrant subset of libc (see CERT's [SIG30-C](https://wiki.sei.cmu.edu/confluence/display/c/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers) for more details and real-world vulnerabilities). 6 | 7 | The tool works by preloading a DSO (libsigcheck.so) to a process. 8 | The DSO intercepts all (!) libc functions. The interceptors check 9 | whether they run in unsafe context. 10 | 11 | The tool is rather hacky but it was able to detect potential errors 12 | in popular programs (zip, aspell). 13 | 14 | All credits for the idea (but not for it's ugly implementation) 15 | should go to Michal Zalewski (aka [lcamtuf](http://lcamtuf.coredump.cx)). 16 | 17 | The project is MIT licensed. It does not have any fancy dependencies, 18 | just Glibc, GCC and Python. 19 | 20 | # Why should I care about signal safety? 21 | 22 | Check lcamtuf's [Delivering Signals for Fun and Profit](http://lcamtuf.coredump.cx/signals.txt) or CWE's 23 | [Signal Handler with Functionality that is not Asynchronous-Safe](https://cwe.mitre.org/data/definitions/828.html). 24 | 25 | Basically unsafe signal handlers may be used to exploit enclosing process. 26 | This is particularly dangerous on Linux, where ordinary user can send signal 27 | to setuid process. 28 | 29 | # What are current results? 30 | 31 | Quite interesting: I saw unsafe behavior in archivers (tar, bzip2, zip, etc.), 32 | Texinfo, aspell, make, calendar, gpg and gdb (see scripts/examples for details). 33 | 34 | # Usage 35 | 36 | Run your app under sigcheck tool and send it a signal: 37 | 38 | ``` 39 | $ sigcheck myapp ... > /dev/null & 40 | $ kill -HUP $! 41 | ``` 42 | 43 | Instead of manually sending the signals, you can ask the tool to automate it 44 | by setting SIGCHECK\_FORK\_TESTS environment variable to "atexit" 45 | (to send signals prior to exiting program) or to "onset" 46 | (to send signal immediately after it got set). Both may find different sets of 47 | bugs for different applications. 48 | 49 | Other influential environment variables: 50 | * SIGCHECK\_VERBOSE - print debug info 51 | * SIGCHECK\_MAX\_ERRORS - limit number of reported errors 52 | * SIGCHECK\_OUTPUT\_FILENO - output file descriptor (TODO: make this a filename?) 53 | 54 | For some examples, see scripts/examples. 55 | 56 | # Build 57 | 58 | To build the tool, simply run ./build.sh from project top directory. 59 | This has only been tested in Ubuntu 14.04. 60 | 61 | # Test 62 | 63 | To test the tool, run scripts/runtests.sh from project top directory. 64 | Real-world examples are available in scripts/examples. 65 | 66 | # Future plans 67 | 68 | The main high-level items are 69 | * run a complete distro under this (e.g. by putting libsigcheck.so to /etc/ld.so.preload) 70 | * design (basically whether all this should be rewritten to use uprobes) 71 | * interception of libc is ugly ugly (although efficient) 72 | * make code thread-safe 73 | 74 | Also various TODOs are scattered all over the codebase. 75 | 76 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2015-2016 Yury Gribov 4 | # 5 | # Use of this source code is governed by MIT license that can be 6 | # found in the LICENSE.txt file. 7 | 8 | # TODO: 9 | # * support versioned symbols 10 | # * compile with -ffreestanding to avoid autogenerated memset and crap 11 | 12 | set -e 13 | 14 | cd $(dirname $0) 15 | 16 | OBJ=bin 17 | 18 | mkdir -p $OBJ 19 | rm -f $OBJ/* 20 | 21 | scripts/gendefs.py $OBJ/ 22 | 23 | CPPFLAGS='-Iinclude -Ibin -D_GNU_SOURCE' 24 | 25 | CFLAGS='-g -O2' 26 | CFLAGS="$CFLAGS -Wall -Wextra -Werror" 27 | CFLAGS="$CFLAGS -fvisibility=hidden -fPIC -shared" 28 | #CFLAGS="$CFLAGS -g -O0 -save-temps" 29 | 30 | LDFLAGS='-Wl,--no-as-needed -ldl -lm -lpthread' 31 | 32 | echo 'Build libsigcheck.so' 33 | gcc -o $OBJ/libsigcheck.so src/*.c $CPPFLAGS $CFLAGS $LDFLAGS 34 | 35 | cp scripts/sigcheck $OBJ 36 | 37 | echo 'Quick functionality check' 38 | SIGCHECK_VERBOSE=1 $OBJ/sigcheck bash -c 'whoami; whoami' 39 | 40 | -------------------------------------------------------------------------------- /include/compiler.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #ifndef COMPILER_H 9 | #define COMPILER_H 10 | 11 | #ifdef __GNUC__ 12 | 13 | #define ALIGN(n) __attribute__((aligned(n))) 14 | #define EXPORT __attribute__((visibility("default"))) 15 | #define UNUSED __attribute__((unused)) 16 | #define USED __attribute__((used)) 17 | #define DEFINE_ALIAS(decl, target) decl __attribute__((alias(#target))); 18 | 19 | #define atomic_inc(p) __sync_fetch_and_add(p, 1) 20 | #define atomic_dec(p) __sync_fetch_and_add(p, -1) 21 | 22 | #else 23 | #error Unknown compiler 24 | #endif 25 | 26 | #endif 27 | 28 | -------------------------------------------------------------------------------- /include/libc.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #ifndef LIBC_H 9 | #define LIBC_H 10 | 11 | #include // size_t 12 | #include 13 | 14 | static __attribute__((noreturn)) void internal__exit(int status) { 15 | asm( 16 | "mov %0, %%rdi\n" 17 | "mov %1, %%eax\n" 18 | "syscall\n" 19 | :: "i"(__NR_exit), "r"(status) 20 | ); 21 | while(1); 22 | } 23 | 24 | static inline size_t internal_strlen(const char *s) { 25 | size_t n; 26 | for(n = 0; s && *s; ++n, ++s); 27 | return n; 28 | } 29 | 30 | static inline void *internal_memchr(void *p, char c, size_t n) { 31 | char *pc = (char *)p; 32 | size_t i; 33 | for(i = 0; i < n; ++i) { 34 | if(pc[i] == c) 35 | return pc + i; 36 | } 37 | return 0; 38 | } 39 | 40 | static inline int internal_memcmp(const void *a, const void *b, size_t n) { 41 | const char *ac = (const char *)a, 42 | *bc = (const char *)b; 43 | size_t i; 44 | for(i = 0; i < n; ++i) { 45 | if(ac[i] < bc[i]) 46 | return -1; 47 | else if(ac[i] > bc[i]) 48 | return 1; 49 | } 50 | return 0; 51 | } 52 | 53 | char *internal_strstr(const char *haystack, const char *needle); 54 | 55 | const char *int2str(int value, char *str, size_t size); 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /include/sigcheck.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #ifndef SIGCHECK_H 9 | #define SIGCHECK_H 10 | 11 | #include 12 | 13 | extern uintptr_t libc_base, libm_base, libpthread_base; 14 | 15 | extern int sigcheck_initialized, sigcheck_initializing; 16 | 17 | void sigcheck_init(); 18 | 19 | void check_context(const char *name, const char *lib); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /scripts/async_safe_syms: -------------------------------------------------------------------------------- 1 | # Copyright 2015-2016 Yury Gribov 2 | # 3 | # Use of this source code is governed by MIT license that can be 4 | # found in the LICENSE.txt file. 5 | 6 | # POSIX.1-2004 (according to `man 7 signal') 7 | _Exit 8 | _exit 9 | abort 10 | accept 11 | access 12 | aio_error 13 | aio_return 14 | aio_suspend 15 | alarm 16 | bind 17 | cfgetispeed 18 | cfgetospeed 19 | cfsetispeed 20 | cfsetospeed 21 | chdir 22 | chmod 23 | chown 24 | clock_gettime 25 | close 26 | connect 27 | creat 28 | dup 29 | dup2 30 | execle 31 | execve 32 | fchmod 33 | fchown 34 | fcntl 35 | fdatasync 36 | fork 37 | fpathconf 38 | fstat 39 | fsync 40 | ftruncate 41 | getegid 42 | geteuid 43 | getgid 44 | getgroups 45 | getpeername 46 | getpgrp 47 | getpid 48 | getppid 49 | getsockname 50 | getsockopt 51 | getuid 52 | kill 53 | link 54 | listen 55 | lseek 56 | lstat 57 | mkdir 58 | mkfifo 59 | open 60 | pathconf 61 | pause 62 | pipe 63 | poll 64 | posix_trace_event 65 | pselect 66 | raise 67 | read 68 | readlink 69 | recv 70 | recvfrom 71 | recvmsg 72 | rename 73 | rmdir 74 | select 75 | sem_post 76 | send 77 | sendmsg 78 | sendto 79 | setgid 80 | setpgid 81 | setsid 82 | setsockopt 83 | setuid 84 | shutdown 85 | sigaction 86 | sigaddset 87 | sigdelset 88 | sigemptyset 89 | sigfillset 90 | sigismember 91 | signal 92 | sigpause 93 | sigpending 94 | sigprocmask 95 | sigqueue 96 | sigset 97 | sigsuspend 98 | sleep 99 | sockatmark 100 | socket 101 | socketpair 102 | stat 103 | symlink 104 | sysconf 105 | tcdrain 106 | tcflow 107 | tcflush 108 | tcgetattr 109 | tcgetpgrp 110 | tcsendbreak 111 | tcsetattr 112 | tcsetpgrp 113 | time 114 | timer_getoverrun 115 | timer_gettime 116 | timer_settime 117 | times 118 | umask 119 | uname 120 | unlink 121 | utime 122 | wait 123 | waitpid 124 | write 125 | 126 | # POSIX.1-2008 (according to `man 7 signal') 127 | execl 128 | execv 129 | faccessat 130 | fchmodat 131 | fchownat 132 | fexecve 133 | fstatat 134 | futimens 135 | linkat 136 | mkdirat 137 | mkfifoat 138 | mknod 139 | mknodat 140 | openat 141 | readlinkat 142 | renameat 143 | symlinkat 144 | unlinkat 145 | utimensat 146 | utimes 147 | 148 | # From http://austingroupbugs.net/view.php?id=692 149 | ffs 150 | htonl 151 | htons 152 | memccpy 153 | memchr 154 | memcmp 155 | memcpy 156 | memmove 157 | memset 158 | ntohl 159 | ntohs 160 | stpcpy 161 | stpncpy 162 | strcat 163 | strchr 164 | strcmp 165 | strcpy 166 | strcspn 167 | strlen 168 | strncat 169 | strncmp 170 | strncpy 171 | strnlen 172 | strpbrk 173 | strrchr 174 | strspn 175 | strstr 176 | strtok_r 177 | wcpcpy 178 | wcpncpy 179 | wcscat 180 | wcschr 181 | wcscmp 182 | wcscpy 183 | wcscspn 184 | wcslen 185 | wcsncat 186 | wcsncmp 187 | wcsncpy 188 | wcsnlen 189 | wcspbrk 190 | wcsrchr 191 | wcsspn 192 | wcsstr 193 | wcstok 194 | wmemchr 195 | wmemcmp 196 | wmemcpy 197 | wmemmove 198 | wmemset 199 | 200 | -------------------------------------------------------------------------------- /scripts/candidates: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2015-2016 Yury Gribov 4 | # 5 | # Use of this source code is governed by MIT license that can be 6 | # found in the LICENSE.txt file. 7 | 8 | # Script to find candidate executables. 9 | 10 | set -e 11 | 12 | is_executable() { 13 | file "$1" | grep -qF ELF 14 | } 15 | 16 | does_use_signals() { 17 | readelf --dyn-syms -W "$1" | grep -q 'UND.*\<\(signal\|sigaction\)\>' 18 | } 19 | 20 | count_signals() { 21 | objdump -d "$1" | grep -c '\<\(signal\|sigaction\)\>' 22 | } 23 | 24 | for f in $(find /bin /usr/bin -type f -a -executable); do 25 | if is_executable $f && does_use_signals $f; then 26 | num_calls=$(count_signals $f) 27 | echo "$f: $num_calls" 28 | fi 29 | done | sort -t ' ' -rnk2 30 | 31 | -------------------------------------------------------------------------------- /scripts/examples: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2015-2016 Yury Gribov 4 | # 5 | # Use of this source code is governed by MIT license that can be 6 | # found in the LICENSE.txt file. 7 | 8 | # Sigcheck examples. 9 | 10 | cd $(dirname $0)/.. 11 | 12 | SIGCHECK=bin/sigcheck 13 | 14 | runtest() { 15 | if ! which $1; then 16 | echo "$1 not installed, can't test \"$@\"" 17 | return 1 18 | fi 19 | 20 | echo "Checking \"$@\" (atexit)" 21 | SIGCHECK_FORK_TESTS=atexit SIGCHECK_OUTPUT_FILENO=3 \ 22 | timeout 5 $SIGCHECK "$@" 3>&1 >/dev/null 2>&1 23 | 24 | echo "Checking \"$@\" (onset)" 25 | SIGCHECK_FORK_TESTS=onset SIGCHECK_OUTPUT_FILENO=3 \ 26 | timeout 5 $SIGCHECK "$@" 3>&1 >/dev/null 2>&1 27 | } 28 | 29 | #runtest hahaha 30 | 31 | runtest ls 32 | 33 | runtest bash -c 'whoami; whoami' 34 | 35 | runtest dash -c 'whoami; whoami' 36 | 37 | touch 1 38 | runtest tar czf 1.tgz 1 39 | runtest tar cjf 1.tar.bz2 1 40 | runtest tar cJf 1.tar.xz 1 41 | rm -f 1.tgz 1.tar.bz2 1.tar.xz 1 42 | 43 | runtest /usr/bin/man bash 44 | 45 | runtest info bash 46 | 47 | runtest strace ls 48 | 49 | runtest git branch 50 | 51 | runtest ssh-keygen -t rsa -f tmp.dat -N '' 52 | rm -f tmp.dat* 53 | 54 | touch 1 55 | runtest zip 1.zip 1 56 | rm -f 1 1.zip 57 | 58 | runtest wget 'http://google.ru' 59 | rm -f index.html* 60 | 61 | echo 'Hello world' > 1 62 | runtest aspell -c 1 63 | rm -f 1 64 | 65 | echo 'Hello world' > 1 66 | echo 'Hello worldd' > 2 67 | runtest diff 1 2 68 | rm -f 1 2 69 | 70 | echo -e '1\n2' > 1 71 | runtest sort 1 72 | rm -f 1 73 | 74 | runtest make -d 75 | rm -f 1 76 | 77 | runtest calendar -A 10 78 | rm -f 1 79 | 80 | runtest gpg --list-public-keys 81 | rm -f 1 82 | 83 | runtest gdb -ex run -ex quit ls 84 | 85 | -------------------------------------------------------------------------------- /scripts/gendefs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2015-2020 Yury Gribov 4 | # 5 | # Use of this source code is governed by MIT license that can be 6 | # found in the LICENSE.txt file. 7 | 8 | # TODO: 9 | # * support versioned symbols? 10 | 11 | import os 12 | import os.path 13 | import subprocess 14 | import sys 15 | import re 16 | 17 | script_dir = os.path.dirname(sys.argv[0]) 18 | 19 | def error(msg): 20 | sys.stderr.write('error: %s\n' % msg) 21 | sys.exit(1) 22 | 23 | def safe_run(cmd): 24 | p = subprocess.Popen(cmd, stdout = subprocess.PIPE) 25 | res = p.communicate() 26 | if p.returncode != 0: 27 | _, stderr = res 28 | error("%s failed: %s" % (cmd[0], stderr)) 29 | return res 30 | 31 | def find_glibc(): 32 | stdout, stderr = safe_run(['ldd', '/bin/sh']) 33 | libc_name = 'libc.so.6' 34 | for line in stdout.split('\n'): 35 | idx = line.find(libc_name + ' => ') 36 | if idx == -1: 37 | continue 38 | # libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fab5a92f000) 39 | lib = line[idx:].split(' ')[2] 40 | root = os.path.dirname(lib) 41 | link = os.readlink(lib) 42 | if not link: 43 | error('failed to readlink %s' % link) 44 | ver = re.findall(r'[0-9]+\.[0-9]+', link)[0] 45 | return (root, ver) 46 | error('failed to locate ' + libc_name) 47 | 48 | def get_public_funs(lib, filename): 49 | stdout, stderr = safe_run(['readelf', '--dyn-syms', '-W', filename]) 50 | 51 | res = [] 52 | for line in stdout.split('\n'): 53 | # Don't consider undefined symbols 54 | if re.search(r'\bUND\b', line): 55 | continue 56 | 57 | # Skip internal functions 58 | # TODO: __libc_memalign is _not_ private! 59 | if re.search(r'GLIBC_PRIVATE|^__libc_|__errno_location', line): 60 | continue 61 | 62 | # Only consider default version 63 | # TODO: take care of legacy versions 64 | if not re.search(r'@@GLIBC_', line): 65 | continue 66 | 67 | # Only consider functions 68 | if not re.search(r'\bFUNC\b', line): 69 | continue 70 | 71 | #print line 72 | words = re.split(r'\s+', line) 73 | name = re.sub(r'@.*', '', words[8]) 74 | addr = int("0x" + words[2], 16) 75 | 76 | # We intercept signal API separately (and it's async-safe anyway) 77 | if re.match(r'^(signal|sysv_signal|bsd_signal|sigaction)$', name): 78 | continue 79 | 80 | # We need these during initialization and error reporting. 81 | # Hopefully they don't call other libc functions... 82 | if re.match(r'^(open|close|read|write)$', name): 83 | continue 84 | 85 | res.append((name, addr, lib)) 86 | 87 | return res 88 | 89 | if len(sys.argv) != 2: 90 | error('invalid syntax') 91 | out = sys.argv[1] 92 | 93 | libroot, libver = find_glibc() 94 | 95 | # TODO: other parts of glibc: crypt, resolv, dl, rt, nss*, nsl, etc.)? 96 | # See http://www.faqs.org/docs/linux_scratch/appendixa/glibc.html 97 | libs = map( 98 | lambda lib: (lib, '%s-%s.so' % (lib, libver)), 99 | ['libc', 'libm', 'libpthread'] 100 | ) 101 | 102 | syms = [] 103 | for lib, filename in libs: 104 | syms += get_public_funs(lib, os.path.join(libroot, filename)) 105 | 106 | async_safe_syms = open(os.path.join(script_dir, 'async_safe_syms')).read().split('\n') 107 | async_safe_syms = filter(lambda s: s, async_safe_syms) 108 | async_safe_syms = filter(lambda s: s[0] != '#', async_safe_syms) 109 | async_safe_syms = set(async_safe_syms) 110 | 111 | syms = filter(lambda s: s[0] not in async_safe_syms, syms) 112 | syms.sort(key = lambda s: s[0]) 113 | 114 | syms_no_dups = [] 115 | for i in range(0, len(syms)): 116 | if not syms_no_dups or syms_no_dups[-1][0] != syms[i][0]: 117 | syms_no_dups.append(syms[i]) 118 | 119 | f = open(os.path.join(out, 'all_libc_syms.def'), 'w') 120 | f.write(''' 121 | #ifndef SYMBOL 122 | #error You must define SYMBOL(name, addr, lib) 123 | #endif 124 | ''') 125 | for name, addr, lib in syms_no_dups: 126 | f.write("SYMBOL(%s, 0x%x, %s)\n" % (name, addr, lib)) 127 | f.write("#undef SYMBOL\n") 128 | f.close() 129 | 130 | f = open(os.path.join(out, 'all_libc_libs.def'), 'w') 131 | f.write(''' 132 | #ifndef LIBRARY 133 | #error You must define LIBRARY(name, filename) 134 | #endif 135 | ''') 136 | for name, filename in libs: 137 | f.write('LIBRARY(%s, "%s")\n' % (name, filename)) 138 | f.write('#undef LIBRARY\n') 139 | f.close() 140 | 141 | -------------------------------------------------------------------------------- /scripts/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2015-2016 Yury Gribov 4 | # 5 | # Use of this source code is governed by MIT license that can be 6 | # found in the LICENSE.txt file. 7 | 8 | cd $(dirname $0)/.. 9 | 10 | SIGCHECK=bin/sigcheck 11 | CFLAGS='-g -O2 -Wall -Wextra -Werror' 12 | 13 | status=0 14 | for f in tests/*.c; do 15 | gcc $f -o bin/test.out $CFLAGS 16 | $SIGCHECK bin/test.out > bin/test.log 2>&1 17 | sed -i -e 's!(pid [0-9]\+)!(pid PID)!g' bin/test.log 18 | if ! diff $f.log bin/test.log; then 19 | echo "$f: FAIL" 20 | status=1 21 | else 22 | echo "$f: SUCCESS" 23 | fi 24 | done 25 | 26 | return $status 27 | 28 | -------------------------------------------------------------------------------- /scripts/sigcheck: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2015-2016 Yury Gribov 4 | # 5 | # Use of this source code is governed by MIT license that can be 6 | # found in the LICENSE.txt file. 7 | LD_PRELOAD=$(dirname $0)/libsigcheck.so${LD_PRELOAD:+:$LD_PRELOAD} "$@" 8 | -------------------------------------------------------------------------------- /src/interceptors.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #include "sigcheck.h" 9 | 10 | #ifdef __x86_64__ 11 | // Notes: 12 | // * it's ok to trash %rax, it's a temp register 13 | #define SYMBOL(name, addr, lib) \ 14 | asm( \ 15 | "\n" \ 16 | " .global " #name "\n" \ 17 | " .type " #name ", @function\n" \ 18 | " .text\n" \ 19 | #name ":\n" \ 20 | " .cfi_startproc\n" \ 21 | " pushq %rdi\n" \ 22 | " .cfi_def_cfa_offset 16\n" \ 23 | " .cfi_offset 7, -16\n" \ 24 | " pushq %rsi\n" \ 25 | " .cfi_def_cfa_offset 24\n" \ 26 | " .cfi_offset 6, -24\n" \ 27 | " pushq %rdx\n" \ 28 | " .cfi_def_cfa_offset 32\n" \ 29 | " .cfi_offset 2, -32\n" \ 30 | " pushq %rcx\n" \ 31 | " .cfi_def_cfa_offset 40\n" \ 32 | " .cfi_offset 1, -40\n" \ 33 | " pushq %r8\n" \ 34 | " .cfi_def_cfa_offset 48\n" \ 35 | " .cfi_offset 8, -48\n" \ 36 | " pushq %r9\n" \ 37 | " .cfi_def_cfa_offset 56\n" \ 38 | " .cfi_offset 9, -56\n" \ 39 | " movl sigcheck_initialized(%rip), %eax\n" \ 40 | " testl %eax, %eax\n" \ 41 | " jne 1f\n" \ 42 | " call sigcheck_init_1\n" \ 43 | "1:\n" \ 44 | " leaq .LC_name_" #name "(%rip), %rdi\n" \ 45 | " movq " #lib "_name(%rip), %rsi\n" \ 46 | " call check_context\n" \ 47 | " popq %r9\n" \ 48 | " .cfi_def_cfa_offset 48\n" \ 49 | " popq %r8\n" \ 50 | " .cfi_def_cfa_offset 40\n" \ 51 | " popq %rcx\n" \ 52 | " .cfi_def_cfa_offset 32\n" \ 53 | " popq %rdx\n" \ 54 | " .cfi_def_cfa_offset 24\n" \ 55 | " popq %rsi\n" \ 56 | " .cfi_def_cfa_offset 16\n" \ 57 | " popq %rdi\n" \ 58 | " .cfi_def_cfa_offset 8\n" \ 59 | " movq " #lib "_base(%rip), %rax\n" \ 60 | " addq $" #addr ", %rax\n" \ 61 | " jmp *%rax\n" \ 62 | ".LC_name_" #name ":\n" \ 63 | " .string \"" #name "\"\n" \ 64 | ".LC_lib_" #name ":\n" \ 65 | " .string \"" #lib "\"\n" \ 66 | " .cfi_endproc\n" \ 67 | " .size " #name ", .-" #name "\n" \ 68 | ); 69 | #else 70 | #error Unsupported platform 71 | #endif 72 | #include "all_libc_syms.def" 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/libc.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #include 9 | 10 | // Copied from libsanitizer 11 | char *internal_strstr(const char *haystack, const char *needle) { 12 | // This is O(N^2), but we are not using it in hot places. 13 | size_t len1 = internal_strlen(haystack); 14 | size_t len2 = internal_strlen(needle); 15 | if (len1 < len2) return 0; 16 | size_t pos; 17 | for (pos = 0; pos <= len1 - len2; pos++) { 18 | if (internal_memcmp(haystack + pos, needle, len2) == 0) 19 | return (char *)haystack + pos; 20 | } 21 | return 0; 22 | } 23 | 24 | const char *int2str(int value, char *str, size_t size) { 25 | int neg = value < 0; 26 | value = value >= 0 ? value : -value; 27 | 28 | if(!size) 29 | return 0; 30 | 31 | size_t cur = size - 1; 32 | str[cur] = 0; 33 | 34 | while(value > 0) { 35 | if(!cur) 36 | return 0; 37 | int digit = value % 10; 38 | value /= 10; 39 | str[--cur] = '0' + digit; 40 | } 41 | 42 | if(neg) { 43 | if(!cur) 44 | return 0; 45 | str[--cur] = '-'; 46 | } 47 | 48 | return &str[cur]; 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/sigcheck.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include "sigcheck.h" 21 | #include "libc.h" 22 | #include "compiler.h" 23 | 24 | static int sigcheck_fd = -1; 25 | 26 | int sigcheck_initialized = 0, 27 | sigcheck_initializing = 0; 28 | 29 | // TODO: more verbose prints. 30 | // TODO: add missing error checks 31 | // TODO: mind thread safety in all functions 32 | 33 | static void sigcheck_print(const char *msg, ...) { 34 | va_list ap; 35 | 36 | int fd = sigcheck_fd >= 0 ? sigcheck_fd : STDERR_FILENO; 37 | 38 | va_start(ap, msg); 39 | const char *s; 40 | for(s = msg; s; s = va_arg(ap, const char *)) { 41 | size_t len = internal_strlen(s); 42 | while(len > 0) { 43 | ssize_t n = write(fd, s, len); 44 | len -= n; 45 | s += n; 46 | } 47 | } 48 | va_end(ap); 49 | } 50 | 51 | #define SAY_START(...) do { \ 52 | char buf[128]; \ 53 | const char *pid = sigcheck_initialized ? int2str((int)getpid(), buf, sizeof(buf)) : "???"; \ 54 | sigcheck_print("sigcheck (pid ", pid, "): ",##__VA_ARGS__, NULL); \ 55 | } while(0) 56 | #define SAY(...) sigcheck_print(__VA_ARGS__, NULL) 57 | 58 | #define DIE(...) do { \ 59 | SAY_START("internal error: ",##__VA_ARGS__, "\n"); \ 60 | internal__exit(1); \ 61 | } while(0) 62 | 63 | static volatile int signal_depth = 0; 64 | static volatile int num_errors = 0; 65 | 66 | static int verbose = 0; 67 | static int max_errors = -1; 68 | 69 | enum fork_tests_mode { 70 | FORK_TESTS_NONE, 71 | FORK_TESTS_ATEXIT, 72 | FORK_TESTS_ONSET 73 | }; 74 | 75 | static enum fork_tests_mode fork_tests = 76 | FORK_TESTS_NONE; 77 | 78 | static inline void push_signal_context(void) { 79 | assert(signal_depth >= 0 && "invalid nesting"); 80 | atomic_inc(&signal_depth); 81 | } 82 | 83 | static inline void pop_signal_context(void) { 84 | atomic_dec(&signal_depth); 85 | assert(signal_depth >= 0 && "invalid nesting"); 86 | } 87 | 88 | #define LIBRARY(name, filename) \ 89 | uintptr_t name ## _base; \ 90 | const char * name ## _name = filename; 91 | #include "all_libc_libs.def" 92 | 93 | // This initializes data for interceptors so that libc can work 94 | void __attribute__((constructor)) sigcheck_init_1(void) { 95 | assert(!sigcheck_initializing && "recursive init"); 96 | sigcheck_initializing = 1; 97 | 98 | // Find base addresses of intercepted libs 99 | 100 | // TODO: sadly we can't check errno for EINTR (it's not yet initialized); 101 | // one option is replacing all libc wrapper with internal_syscall. 102 | // Actually this may not be a problem, as signals are not delivered during 103 | // syscall in modern kernels (?). 104 | int fd = open("/proc/self/maps", O_RDONLY); 105 | if(fd < 0) { 106 | DIE("failed to open /proc/self/maps"); 107 | } 108 | 109 | char buf[256]; 110 | size_t end = 0, nread; 111 | while((nread = read(fd, &buf[end], sizeof(buf) - end)) > 0) { 112 | char *nl = internal_memchr(buf, '\n', sizeof(buf)); 113 | 114 | *nl = 0; 115 | 116 | uintptr_t *base = 0; 117 | #define LIBRARY(name, filename) \ 118 | if(internal_strstr(buf, filename) && !name ## _base) \ 119 | base = &name ## _base; \ 120 | else 121 | #include "all_libc_libs.def" 122 | {} 123 | 124 | if(base) { 125 | // 7f438376c000-7f4383928000 r-xp 00000000 08:01 659498 /lib/x86_64-linux-gnu/libc-2.19.so 126 | uintptr_t v = 0; 127 | size_t i; 128 | for(i = 0; buf[i]; ++i) { 129 | char c = buf[i]; 130 | if(c >= '0' && c <= '9') 131 | v = v * 16 + (c - '0'); 132 | else if(c >= 'a' && c <= 'f') 133 | v = v * 16 + 10 + (c - 'a'); 134 | else if(c >= 'A' && c <= 'F') 135 | v = v * 16 + 10 + (c - 'A'); 136 | else // '-' 137 | break; 138 | } 139 | *base = v; 140 | } 141 | 142 | // Copy start of next line 143 | size_t i, end_new; 144 | for(i = nl - buf + 1, end_new = 0; i < end + nread; ++i, ++end_new) 145 | buf[end_new] = buf[i]; 146 | end = end_new; 147 | } 148 | 149 | close(fd); 150 | 151 | #define LIBRARY(name, filename) \ 152 | if(!name ## _base) DIE(#name " not found"); 153 | #include "all_libc_libs.def" 154 | 155 | sigcheck_initialized = 1; 156 | sigcheck_initializing = 0; 157 | } 158 | 159 | static void sigcheck_finalize(void); 160 | 161 | // This performs the rest of initialization 162 | void __attribute__((constructor)) sigcheck_init_2(void) { 163 | if(!sigcheck_initialized) 164 | sigcheck_init_1(); 165 | 166 | char *sigcheck_fd_ = getenv("SIGCHECK_OUTPUT_FILENO"); 167 | if(sigcheck_fd_) 168 | sigcheck_fd = atoi(sigcheck_fd_); 169 | 170 | char *verbose_ = getenv("SIGCHECK_VERBOSE"); 171 | if(verbose_) 172 | verbose = atoi(verbose_); 173 | 174 | char *max_errors_ = getenv("SIGCHECK_MAX_ERRORS"); 175 | if(max_errors_) 176 | max_errors = atoi(max_errors_); 177 | 178 | char *fork_tests_ = getenv("SIGCHECK_FORK_TESTS"); 179 | if(!fork_tests_ || 0 == strcmp(fork_tests_, "none")) 180 | fork_tests = FORK_TESTS_NONE; 181 | else if(0 == strcmp(fork_tests_, "atexit")) 182 | fork_tests = FORK_TESTS_ATEXIT; 183 | else if(0 == strcmp(fork_tests_, "onset")) 184 | fork_tests = FORK_TESTS_ONSET; 185 | else 186 | DIE("unknown value for SIGCHECK_FORK_TESTS: ", fork_tests_); 187 | 188 | // TODO: can we force this to be run _before_ all other handlers? 189 | atexit(sigcheck_finalize); 190 | } 191 | 192 | struct sigcheck_info { 193 | union { 194 | sighandler_t h; 195 | void (*sa)(int, siginfo_t *, void *); 196 | } user_handler; 197 | int is_handled; 198 | int active; 199 | int siginfo; 200 | }; 201 | 202 | static inline void sigcheck_info_clear(volatile struct sigcheck_info *si) { 203 | si->user_handler.h = 0; 204 | si->active = 0; 205 | } 206 | 207 | static volatile struct sigcheck_info sigtab[_NSIG]; 208 | 209 | static int is_deadly_signal(int signum) { 210 | switch(signum) { 211 | case SIGSEGV: 212 | case SIGBUS: 213 | case SIGABRT: 214 | case SIGILL: 215 | case SIGTERM: 216 | return 1; 217 | default: 218 | return 0; 219 | } 220 | } 221 | 222 | static int is_interesting_signal(int signum) { 223 | switch(signum) { 224 | // TODO: I mainly disabled these to get bash working; 225 | // perhaps enable these at some point. 226 | case SIGCHLD: 227 | case SIGCONT: 228 | case SIGSTOP: 229 | case SIGTSTP: 230 | case SIGTTIN: 231 | case SIGTTOU: 232 | return 0; 233 | default: 234 | return 1; 235 | } 236 | } 237 | 238 | static void about_signal(int signum) { 239 | char buf[128]; 240 | const char *signum_str = int2str(signum, buf, sizeof(buf)); 241 | if(!signum_str) 242 | DIE("increase buffer size in about_signal"); 243 | const char *sig_str = sys_siglist[signum]; 244 | SAY_START("signal ", signum_str, " (", sig_str, "): "); 245 | } 246 | 247 | static volatile int suppress_sigchld; 248 | 249 | void maybe_fork_signal_test(int signum) { 250 | if(!is_interesting_signal(signum)) 251 | return; 252 | sigset_t mask; 253 | sigemptyset(&mask); 254 | // TODO: pthread_mask? But it's not async-signal-safe... 255 | if(0 != sigprocmask(0, 0, &mask) || sigismember(&mask, signum)) 256 | return; 257 | suppress_sigchld = 1; 258 | pid_t pid = fork(); 259 | if(pid < 0) { 260 | DIE("failed to fork test process"); 261 | } else if(pid == 0) { 262 | // TODO: ensure that child always terminates 263 | if(verbose) { 264 | about_signal(signum); 265 | SAY("sending in forked process\n"); 266 | } 267 | if(0 != raise(signum)) { 268 | SAY("raise() failed"); 269 | } 270 | _exit(0); 271 | } else { 272 | if(waitpid(pid, 0, 0) < 0) 273 | DIE("failed to wait for forked test process"); 274 | } 275 | suppress_sigchld = 0; 276 | } 277 | 278 | static void sigcheck_finalize(void) { 279 | int i; 280 | for(i = 0; i < _NSIG; ++i) { 281 | if(!sigtab[i].is_handled) 282 | continue; 283 | if(verbose) { 284 | about_signal(i); 285 | SAY("is handled\n"); 286 | } 287 | if(fork_tests == FORK_TESTS_ATEXIT) { 288 | maybe_fork_signal_test(i); 289 | } 290 | } 291 | } 292 | 293 | static int do_report_error() { 294 | if(max_errors < 0) 295 | return 1; 296 | if(num_errors >= max_errors) 297 | return 0; 298 | return atomic_inc(&num_errors) >= max_errors; 299 | } 300 | 301 | void check_context(const char *name, const char *lib) { 302 | if(verbose >= 2) 303 | SAY_START("check_context: ", name, " from ", lib, "\n"); 304 | if(!signal_depth) 305 | return; 306 | // For all active signals 307 | int i; 308 | for(i = 0; i < _NSIG; ++i) { 309 | if(!sigtab[i].active) 310 | continue; 311 | if(do_report_error()) { 312 | about_signal(i); 313 | SAY("unsafe call to ", name, " from ", lib, " in user handler\n"); 314 | } 315 | } 316 | } 317 | 318 | #define BAD_ERRNO 0x12345 319 | 320 | static void sigcheck(int signum, siginfo_t *info, void *ctx) { 321 | if(signum == SIGCHLD && suppress_sigchld) 322 | return; 323 | 324 | volatile struct sigcheck_info *si = &sigtab[signum]; 325 | if(!si->user_handler.h) 326 | DIE("received signal but no handler"); 327 | 328 | push_signal_context(); 329 | si->active = 1; 330 | int old_errno = errno; 331 | // Check that errno is preserved 332 | if(!is_deadly_signal(signum)) 333 | errno = BAD_ERRNO; 334 | if(si->siginfo) { 335 | si->user_handler.sa(signum, info, ctx); 336 | } else { 337 | si->user_handler.h(signum); 338 | } 339 | if(errno != BAD_ERRNO && do_report_error()) { 340 | about_signal(signum); 341 | SAY("errno not preserved in user handler\n"); 342 | } 343 | errno = old_errno; 344 | si->active = 0; 345 | pop_signal_context(); 346 | } 347 | 348 | // TODO: factor out common code from signal() and sigaction() 349 | EXPORT sighandler_t signal(int signum, sighandler_t handler) { 350 | static sighandler_t (*signal_real)(int signum, sighandler_t handler) = 0; 351 | if(!signal_real) { 352 | // Non-atomic but who cares? 353 | signal_real = dlsym(RTLD_NEXT, "signal"); 354 | } 355 | 356 | volatile struct sigcheck_info *si = &sigtab[signum]; 357 | 358 | struct sigcheck_info si_old = *si; 359 | 360 | if(handler == SIG_ERR || signum < 1 || signum >= _NSIG || handler == (void *)sigcheck) 361 | return signal_real(signum, handler); 362 | 363 | if (handler != SIG_IGN && handler != SIG_DFL) { 364 | if(verbose) { 365 | about_signal(signum); 366 | SAY("setting up a handler\n"); 367 | } 368 | si->user_handler.h = handler; 369 | si->is_handled = 1; 370 | si->siginfo = 0; 371 | handler = (sighandler_t)sigcheck; 372 | } else { 373 | if(verbose) { 374 | about_signal(signum); 375 | SAY("clearing a handler\n"); 376 | } 377 | si->is_handled = 0; 378 | } 379 | 380 | sighandler_t res = signal_real(signum, handler); 381 | if(res == SIG_ERR) 382 | *si = si_old; 383 | 384 | return res; 385 | } 386 | 387 | EXPORT int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact) { 388 | static int (*sigaction_real)(int signum, const struct sigaction *act, struct sigaction *oldact) = 0; 389 | if(!sigaction_real) { 390 | // Non-atomic but who cares? 391 | sigaction_real = dlsym(RTLD_NEXT, "sigaction"); 392 | } 393 | 394 | if(!act) 395 | return sigaction_real(signum, act, oldact); 396 | 397 | volatile struct sigcheck_info *si = &sigtab[signum]; 398 | struct sigcheck_info si_old = *si; 399 | 400 | struct sigaction myact; 401 | 402 | int siginfo = (act->sa_flags & SA_SIGINFO) != 0; 403 | void *handler = siginfo ? (void *)act->sa_sigaction : (void *)act->sa_handler; 404 | 405 | if(handler == SIG_ERR || signum < 1 || signum >= _NSIG || handler == sigcheck) 406 | return sigaction_real(signum, act, oldact); 407 | 408 | if (handler != SIG_IGN && handler != SIG_DFL) { 409 | if(verbose) { 410 | about_signal(signum); 411 | SAY("setting up a handler\n"); 412 | } 413 | si->user_handler.h = handler; 414 | si->is_handled = 1; 415 | si->siginfo = siginfo; 416 | if(siginfo) 417 | si->user_handler.sa = act->sa_sigaction; 418 | else 419 | si->user_handler.h = act->sa_handler; 420 | myact.sa_sigaction = sigcheck; 421 | myact.sa_flags = act->sa_flags | SA_SIGINFO; 422 | myact.sa_mask = act->sa_mask; 423 | act = &myact; 424 | } else { 425 | if(verbose) { 426 | about_signal(signum); 427 | SAY("clearing a handler\n"); 428 | } 429 | si->is_handled = 0; 430 | } 431 | 432 | int res = sigaction_real(signum, act, oldact); 433 | if(res != 0) 434 | *si = si_old; 435 | else if(fork_tests == FORK_TESTS_ONSET && si->is_handled) { 436 | maybe_fork_signal_test(signum); 437 | } 438 | 439 | return res; 440 | } 441 | 442 | DEFINE_ALIAS(EXPORT sighandler_t sysv_signal(int signum, sighandler_t handler), signal); 443 | DEFINE_ALIAS(EXPORT sighandler_t bsd_signal(int signum, sighandler_t handler), signal); 444 | 445 | // TODO: intercept sigprocmask and pthreadmask and fork tests 446 | // if signal is enabled 447 | 448 | -------------------------------------------------------------------------------- /tests/sigaction-1.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | void myhandler(int signum) { 13 | printf("Hello from myhandler: %d\n", signum); 14 | } 15 | 16 | int main() { 17 | struct sigaction sa; 18 | sa.sa_handler = myhandler; 19 | sa.sa_flags = 0; 20 | sigemptyset (&sa.sa_mask); 21 | if(0 != sigaction(SIGHUP, &sa, 0)) { 22 | fprintf(stderr, "sigaction() failed\n"); 23 | exit(1); 24 | } 25 | raise(SIGHUP); 26 | return 0; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /tests/sigaction-1.c.log: -------------------------------------------------------------------------------- 1 | sigcheck (pid PID): signal 1 (Hangup): unsafe call to __printf_chk from libc-2.19.so in user handler 2 | Hello from myhandler: 1 3 | -------------------------------------------------------------------------------- /tests/signal-1.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | void myhandler(int signum) { 13 | printf("Hello from myhandler: %d\n", signum); 14 | } 15 | 16 | int main() { 17 | if(SIG_ERR == signal(SIGHUP, myhandler)) { 18 | fprintf(stderr, "signal() failed\n"); 19 | exit(1); 20 | } 21 | raise(SIGHUP); 22 | return 0; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /tests/signal-1.c.log: -------------------------------------------------------------------------------- 1 | sigcheck (pid PID): signal 1 (Hangup): unsafe call to __printf_chk from libc-2.19.so in user handler 2 | Hello from myhandler: 1 3 | -------------------------------------------------------------------------------- /tests/signal-2.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2016 Yury Gribov 3 | * 4 | * Use of this source code is governed by MIT license that can be 5 | * found in the LICENSE.txt file. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | void myhandler(int signum) { 14 | signum = signum; 15 | errno = 0; 16 | } 17 | 18 | int main() { 19 | if(SIG_ERR == signal(SIGHUP, myhandler)) { 20 | fprintf(stderr, "signal() failed\n"); 21 | exit(1); 22 | } 23 | raise(SIGHUP); 24 | printf("Hello from main!\n"); 25 | return 0; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /tests/signal-2.c.log: -------------------------------------------------------------------------------- 1 | sigcheck (pid PID): signal 1 (Hangup): errno not preserved in user handler 2 | Hello from main! 3 | --------------------------------------------------------------------------------