├── .github └── workflows │ ├── ci.yml │ └── coverity.yml ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── scripts ├── gcov-tool-many ├── install-deps.sh ├── pregrind └── travis.sh ├── src ├── async_safe.c ├── async_safe.h ├── common.h └── pregrind.c └── tests ├── exec ├── child.c ├── parent.c └── run.sh ├── spawn ├── child.c ├── parent.c └── run.sh └── system ├── child.c ├── parent.c └── run.sh /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # TODO: 2 | # - sanitizers 3 | 4 | name: CI 5 | on: 6 | push: 7 | paths-ignore: 8 | - 'LICENSE.txt' 9 | - 'README.md' 10 | pull_request: 11 | paths-ignore: 12 | - 'LICENSE.txt' 13 | - 'README.md' 14 | jobs: 15 | Tests: 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-18.04, ubuntu-20.04, ubuntu-22.04, ubuntu-latest] 20 | cc: [gcc, clang] 21 | runs-on: ${{ matrix.os }} 22 | env: 23 | CC: ${{ matrix.cc }} 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Install deps 27 | run: scripts/install-deps.sh 28 | - name: Run tests 29 | run: scripts/travis.sh 30 | CSA: 31 | runs-on: ubuntu-latest 32 | env: 33 | CC: clang 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Install deps 37 | run: | 38 | scripts/install-deps.sh 39 | sudo apt-get install clang-tools 40 | - name: Run tests 41 | run: scan-build --keep-going --status-bugs make clean all 42 | Coverage: 43 | needs: Tests 44 | runs-on: ubuntu-latest 45 | environment: secrets 46 | env: 47 | COVERAGE: 1 48 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 49 | steps: 50 | - uses: actions/checkout@v2 51 | - name: Install deps 52 | run: scripts/install-deps.sh 53 | - name: Run tests 54 | run: scripts/travis.sh 55 | -------------------------------------------------------------------------------- /.github/workflows/coverity.yml: -------------------------------------------------------------------------------- 1 | name: Coverity 2 | on: 3 | schedule: 4 | # Run on Mondays 5 | - cron: '0 5 * * MON' 6 | jobs: 7 | Coverity: 8 | runs-on: ubuntu-latest 9 | environment: secrets 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install deps 13 | run: scripts/install-deps.sh 14 | - uses: vapier/coverity-scan-action@v0 15 | with: 16 | project: valgrind-preload 17 | token: ${{ secrets.COVERITY_SCAN_TOKEN }} 18 | email: ${{ secrets.EMAIL }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | bin 3 | 4 | # GDB 5 | .gdb_history 6 | 7 | # Vim files 8 | .*swp 9 | 10 | # Test files 11 | parent 12 | child 13 | test.log 14 | *.gcda 15 | *.gcno 16 | *.gcov 17 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2022 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2017-2022 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 | CC ?= gcc 7 | DESTDIR ?= /usr/local 8 | 9 | CPPFLAGS = -D_GNU_SOURCE 10 | CFLAGS = -g -fPIC -fvisibility=hidden -Wall -Wextra -Werror 11 | LDFLAGS = -shared -fPIC -Wl,--warn-common 12 | LIBS = -ldl 13 | 14 | ifneq (,$(COVERAGE)) 15 | DEBUG = 1 16 | CFLAGS += --coverage -DNDEBUG 17 | CFLAGS += -fprofile-dir=coverage.%p 18 | LDFLAGS += --coverage 19 | endif 20 | ifeq (,$(DEBUG)) 21 | CFLAGS += -O2 22 | LDFLAGS += -Wl,-O2 23 | else 24 | CFLAGS += -O0 25 | endif 26 | 27 | $(shell mkdir -p bin) 28 | 29 | all: bin/libpregrind.so bin/pregrind 30 | 31 | bin/%: scripts/% Makefile 32 | cp $< $@ 33 | 34 | bin/libpregrind.so: bin/pregrind.o bin/async_safe.o Makefile bin/FLAGS 35 | $(CC) $(LDFLAGS) -o $@ $(filter %.o, $^) $(LIBS) 36 | 37 | bin/%.o: src/async_safe.h src/common.h 38 | 39 | bin/%.o: src/%.c Makefile bin/FLAGS 40 | $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< 41 | 42 | bin/FLAGS: FORCE 43 | if test x"$(CFLAGS) $(CXXFLAGS) $(LDFLAGS)" != x"$$(cat $@)"; then \ 44 | echo "$(CFLAGS) $(CXXFLAGS) $(LDFLAGS)" > $@; \ 45 | fi 46 | 47 | clean: 48 | rm -f bin/* 49 | find -name \*.gcov -o -name \*.gcno -o -name \*.gcda -o -name coverage.\* | xargs rm -rf 50 | 51 | install: 52 | mkdir -p $(DESTDIR) 53 | install bin/libpregrind.so $(DESTDIR)/lib 54 | install scripts/pregrind $(DESTDIR)/bin 55 | 56 | check: 57 | tests/exec/run.sh 58 | tests/system/run.sh 59 | tests/spawn/run.sh 60 | @echo SUCCESS 61 | 62 | .PHONY: clean all check install FORCE 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](http://img.shields.io/:license-MIT-blue.svg)](https://github.com/yugr/Localizer/blob/master/LICENSE.txt) 2 | [![Build Status](https://github.com/yugr/valgrind-preload/actions/workflows/ci.yml/badge.svg)](https://github.com/yugr/valgrind-preload/actions) 3 | [![codecov](https://codecov.io/gh/yugr/valgrind-preload/branch/master/graph/badge.svg)](https://codecov.io/gh/yugr/valgrind-preload) 4 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/yugr/valgrind-preload.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/yugr/valgrind-preload/alerts/) 5 | [![Coverity Scan](https://scan.coverity.com/projects/yugr-valgrind-preload/badge.svg)](https://scan.coverity.com/projects/yugr-valgrind-preload) 6 | 7 | 8 | # What's this? 9 | 10 | Valgrind-preload (AKA Pregrind) is a simple `LD_PRELOAD`-able library 11 | which will cause all spawned processes to be started under Valgrind. 12 | 13 | It's functionality is similar to Valgrind's standard `--trace-children=yes` 14 | but fixes few disadvantages: 15 | * avoids intrumenting setuid processes (Valgrind 16 | [doesn't handle them](http://stackoverflow.com/questions/1701752/how-do-i-run-valgrind-to-a-process-which-has-super-user-bit-on) 17 | so you have to laboriously blacklist all of them via `--trace-children-skip` 18 | which is unreliable and disables instrumentation of grandchildren) 19 | * can easily be enabled for the whole distro or chroot via `ld.so.preload` 20 | (no need to search for `init` and replace it with a wrapper) 21 | 22 | The tool seems to be pretty stable now, e.g. I was able to 23 | [instrument complete Debian package builds](https://github.com/yugr/debian_pkg_test/tree/master/examples/valgrind-preload) 24 | and even found [several new errors](https://github.com/yugr/valgrind-preload#trophies). 25 | 26 | # Usage 27 | 28 | To use, just preload `libpregrind.so` to your app: 29 | 30 | $ LD_PRELOAD=path/to/libpregrind.so app arg1 ... 31 | 32 | You can also use a simple wrapper script: 33 | 34 | $ path/to/pregrind app arg1 ... 35 | 36 | Finally, if you want to instrument _whole system_, preload `libpregrind.so` 37 | globally: 38 | 39 | $ echo path/to/libpregrind.so >> /etc/ld.so.preload 40 | 41 | Note that in this mode `libpregrind.so` will be preloaded to 42 | _all newly started processes_ so any malfunction may permanently break your 43 | system. It's thus highly recommended to only do this in a chroot or VM. 44 | 45 | Library can be customized through environment variables: 46 | * PREGRIND\_LOG\_PATH - log to files inside this directory, rather than to stderr 47 | * PREGRIND\_FLAGS - additional flags for Valgrind (e.g. `--track-origins=yes`) 48 | * PREGRIND\_VERBOSE - print diagnostic info 49 | * PREGRIND\_DISABLE - disable instrumentation 50 | * PREGRIND\_BLACKLIST - name of file with wildcard patterns of files 51 | which should not be instrumented 52 | 53 | # Build 54 | 55 | To build the tool, simply run make from top directory. 56 | 57 | # Trophies 58 | 59 | * [acl: Uninitialized value in lt-setfacl](http://savannah.nongnu.org/bugs/index.php?50566) (fixed) 60 | * [libsndfile: Uninitialized format in error\_test](https://github.com/erikd/libsndfile/issues/209) (fixed) 61 | * [libsndfile: Buffer overflow in string\_test](https://github.com/erikd/libsndfile/issues/208) (was already fixed in upstream) 62 | * [docbook-to-man: Memcpy parameter overlap in docbook-to-man](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=858389) (fixed) 63 | 64 | # Known issues 65 | 66 | Various TODO and FIXME are scattered all over the codebase 67 | but tool should be quite robust. Ping me if you need more features. 68 | -------------------------------------------------------------------------------- /scripts/gcov-tool-many: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 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 | set -eu 9 | 10 | error() { 11 | prefix="error:" 12 | if test -t 2; then 13 | prefix="${RED}${prefix}${END_COLOR}" 14 | fi 15 | printf "$(basename $0): $prefix $@\\n" >&2 16 | exit 1 17 | } 18 | 19 | warn() { 20 | prefix="warning:" 21 | if test -t 2; then 22 | prefix="${RED}${prefix}${END_COLOR}" 23 | fi 24 | printf "$(basename $0): $prefix $@\\n" >&2 25 | } 26 | 27 | mkcleandir() { 28 | mkdir -p "$1" 29 | rm -rf "$1"/* 30 | } 31 | 32 | usage() { 33 | cat <&2 <&2 "$me: action '$action' not implemented" 142 | exit 1 143 | ;; 144 | esac 145 | -------------------------------------------------------------------------------- /scripts/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sudo apt-get -y install valgrind 3 | -------------------------------------------------------------------------------- /scripts/pregrind: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eu 2 | 3 | # Copyright 2017 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 | D=$(dirname $0) 9 | D=$(readlink -f $D) 10 | LD_PRELOAD=$D/libpregrind.so${LD_PRELOAD:+:$LD_PRELOAD} $@ 11 | -------------------------------------------------------------------------------- /scripts/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 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 | set -eu 9 | set -x 10 | 11 | if test -n "${TRAVIS:-}" -o -n "${GITHUB_ACTIONS:-}"; then 12 | set -x 13 | fi 14 | 15 | cd $(dirname $0)/.. 16 | 17 | make "$@" clean all 18 | make "$@" check 19 | 20 | # Upload coverage 21 | if test -n "${COVERAGE:-}"; then 22 | # Merge DLL coverage from different tests 23 | scripts/gcov-tool-many merge tests/*/merged_profile 24 | # Get rid of complex names e.g. #home#yugr#src#my#valgrind-preload#bin#async_safe.gcda 25 | for f in `find -name '#*.gc[dn][ao]'`; do mv $f $(basename $f | tr \# /); done 26 | mv bin/*.gc[dn][ao] . 27 | # Generate DLL report 28 | gcov *.gcno 29 | # Generate test reports 30 | for t in tests/*; do 31 | ! test -d $t || (cd $t && gcov *.gcno) 32 | done 33 | # Upload 34 | curl --retry 5 -s https://codecov.io/bash > codecov.bash 35 | bash codecov.bash -Z -X gcov 36 | fi 37 | -------------------------------------------------------------------------------- /src/async_safe.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 "async_safe.h" 9 | #include "common.h" 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | void safe_fputs(int fd, const char *s) { 21 | ssize_t written, rem = strlen(s); 22 | while(rem) { 23 | written = write(fd, s, rem); 24 | assert(written >= 0 && "write() failed"); 25 | rem -= written; 26 | s += written; 27 | } 28 | } 29 | 30 | // mmap(2) isn't officially async-safe but most probably is 31 | void *safe_malloc(size_t n, int error_fd) { 32 | n = (n + sizeof(MemControlBlock) + PAGE_SIZE - 1) & ~(uintptr_t)(PAGE_SIZE - 1); 33 | void *ret = mmap(0, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 34 | if (!ret || ret == MAP_FAILED) { 35 | safe_fprintf(error_fd, PREFIX "safe_malloc() failed to allocate %zd bytes: %s\n", n, sys_errlist[errno]); 36 | abort(); 37 | } 38 | AS_BLOCK(ret)->size = n; 39 | return RAW2USER(ret); 40 | } 41 | 42 | void safe_free(void *p, int error_fd) { 43 | void *buf = USER2RAW(p); 44 | size_t size = AS_BLOCK(buf)->size; 45 | if (0 != munmap(buf, size)) { 46 | safe_fprintf(error_fd, PREFIX "safe_free() failed to unmap memory at 0x%p (size %zd)\n", p, size); 47 | abort(); 48 | } 49 | } 50 | 51 | char *safe_strdup(const char *s, int error_fd) { 52 | char *ss = safe_malloc(strlen(s) + 1, error_fd); 53 | strcpy(ss, s); 54 | return ss; 55 | } 56 | 57 | char *safe_basename(char *f) { 58 | char *sep = strrchr(f, '/'); 59 | return sep ? sep + 1 : f; 60 | } 61 | 62 | // Not very efficient but enough for our simple usecase 63 | int safe_fnmatch(const char *p, const char *s) { 64 | if(*p == 0 || *s == 0) 65 | return *p == *s; 66 | 67 | switch(*p) { 68 | case '?': 69 | return *s && safe_fnmatch(p + 1, s + 1); 70 | case '*': 71 | if(p[1] == 0) 72 | return 1; 73 | else if(p[1] == '*') 74 | return safe_fnmatch(p + 1, s); 75 | else { 76 | size_t i; 77 | for(i = 0; s[i]; ++i) 78 | if(p[1] == s[i] && safe_fnmatch(p + 2, s + i + 1)) 79 | return 1; 80 | return 0; 81 | } 82 | default: 83 | return *p == *s && safe_fnmatch(p + 1, s + 1); 84 | } 85 | 86 | abort(); 87 | } 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/async_safe.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 ASYNC_SAFE_H 9 | #define ASYNC_SAFE_H 10 | 11 | #include 12 | #include 13 | 14 | // Async-safe analogs of common functions 15 | 16 | void safe_fputs(int fd, const char *s); 17 | 18 | // snprintf isn't officially async-safe but should be if not using floats 19 | #define safe_fprintf(fd, fmt, ...) do { \ 20 | char _buf[512]; \ 21 | snprintf(_buf, sizeof(_buf), fmt, ##__VA_ARGS__); \ 22 | safe_fputs(fd, _buf); \ 23 | } while(0) 24 | 25 | typedef struct { 26 | size_t size; 27 | } MemControlBlock; 28 | 29 | #define AS_BLOCK(ptr) ((MemControlBlock *)(ptr)) 30 | #define RAW2USER(ptr) ((void *)(AS_BLOCK(ptr) + 1)) 31 | #define USER2RAW(ptr) ((void *)(AS_BLOCK(ptr) - 1)) 32 | 33 | void *safe_malloc(size_t n, int error_fd); 34 | void safe_free(void *p, int error_fd); 35 | char *safe_strdup(const char *s, int error_fd); 36 | 37 | char *safe_basename(char *f); 38 | 39 | int safe_fnmatch(const char *p, const char *s); 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /src/common.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 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 COMMON_H 9 | #define COMMON_H 10 | 11 | // Common defs 12 | 13 | #define PREFIX "libpregrind.so: " 14 | 15 | #define EXPORT __attribute__((visibility("default"))) 16 | 17 | #define PAGE_SIZE (4 * 1024u) 18 | 19 | // sys_errlist is not declared in newer Glibc's 20 | extern const char *const sys_errlist[]; 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /src/pregrind.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2022 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 "async_safe.h" 9 | #include "common.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | int (*real_execl)(const char *path, const char *arg, ...); 27 | int (*real_execlp)(const char *file, const char *arg, ...); 28 | int (*real_execle)(const char *path, const char *arg, ...); 29 | int (*real_execv)(const char *path, char *const argv[]); 30 | int (*real_execvp)(const char *file, char *const argv[]); 31 | int (*real_execve)(const char *path, char *const argv[], char *const envp[]); 32 | int (*real_execvpe)(const char *file, char *const argv[], char *const envp[]); 33 | int (*real_posix_spawn)(pid_t *pid, const char *path, 34 | const posix_spawn_file_actions_t *file_actions, 35 | const posix_spawnattr_t *attrp, 36 | char *const argv[], char *const envp[]); 37 | int (*real_posix_spawnp)(pid_t *pid, const char *file, 38 | const posix_spawn_file_actions_t *file_actions, 39 | const posix_spawnattr_t *attrp, 40 | char *const argv[], char *const envp[]); 41 | 42 | const char *vg_flags[128]; 43 | const char *vg_log_path_templ; 44 | const char *log_file; 45 | int v; 46 | int disable; 47 | int i_am_root; 48 | char *blacklist[64]; 49 | volatile int is_initialized; 50 | 51 | static int get_log_fd() { 52 | static int log_fd = -1; 53 | 54 | if(!log_file) 55 | return STDERR_FILENO; 56 | 57 | if(log_fd > 0) 58 | return log_fd; 59 | 60 | // We delay opening the file until we have something to write. 61 | // This is racy but this isn't a big deal (worst case the output will be corrupted). 62 | log_fd = open(log_file, O_CREAT | O_WRONLY | O_APPEND, S_IRWXU | S_IRWXG | S_IRWXO); 63 | if(-1 == log_fd) { 64 | fprintf(stderr, PREFIX "open() of %s failed: %s\n", log_file, sys_errlist[errno]); 65 | abort(); 66 | } 67 | 68 | return log_fd; 69 | } 70 | 71 | #define safe_printf(fmt, ...) safe_fprintf(get_log_fd(), fmt, ##__VA_ARGS__) 72 | #define safe_puts(s) safe_fputs(get_log_fd(), s) 73 | 74 | // Reserve space for size and at least one trailing nullptr 75 | #define GET_EFFECTIVE_SIZE(size) ((size) - sizeof(MemControlBlock) - sizeof (char *)) 76 | 77 | static char **va_list_to_argv(va_list ap, const char *arg0) { 78 | void *buf = safe_malloc(PAGE_SIZE >> 1, get_log_fd()); 79 | const char **args = (const char **)buf; 80 | size_t max_args = GET_EFFECTIVE_SIZE(PAGE_SIZE >> 1) / sizeof(const char *); 81 | 82 | args[0] = arg0; 83 | ++args; 84 | --max_args; 85 | 86 | while(arg0) { 87 | assert(max_args > 0 && "Too many arguments"); 88 | arg0 = va_arg(ap, const char *); 89 | args[0] = arg0; 90 | ++args; 91 | --max_args; 92 | } while(arg0); 93 | 94 | return (char **)buf; 95 | } 96 | 97 | static char *get_prog_name() { 98 | FILE *p = fopen("/proc/self/cmdline", "rb"); 99 | assert(p && "Failed to read /proc"); 100 | 101 | static char s[128]; 102 | memset(s, 0, sizeof(s)); 103 | 104 | int nread = fread(s, 1, sizeof(s), p); 105 | if(nread < 0 || !memchr(s, 0, sizeof(s))) { 106 | dprintf(get_log_fd(), PREFIX "fread() from /proc/self/exe failed: %s\n", sys_errlist[errno]); 107 | abort(); 108 | } 109 | 110 | return safe_basename(s); 111 | } 112 | 113 | static char *trim_whites(char *s) { 114 | for(; isspace(*s); ++s); 115 | 116 | char *end = NULL, *p; 117 | for(p = s; *p; ++p) { 118 | int space = isspace(*p); 119 | if(end && !space) 120 | end = NULL; 121 | else if(!end && space) 122 | end = p; 123 | } 124 | 125 | if(end) 126 | *end = 0; 127 | 128 | return s; 129 | } 130 | 131 | static void maybe_init() { 132 | assert(!is_initialized && "Init called twice"); 133 | 134 | const char *verbose = getenv("PREGRIND_VERBOSE"); 135 | if(verbose) { 136 | v = atoi(verbose); 137 | } 138 | 139 | const char *flags = getenv("PREGRIND_FLAGS"); 140 | if(flags) { 141 | flags = strdup(flags); 142 | 143 | size_t i = 0; 144 | 145 | while(flags) { 146 | for(; *flags == ' '; ++flags) 147 | 148 | if(!*flags) 149 | break; 150 | 151 | char *next = strchr(flags, ' '); 152 | if(next) { 153 | *next = 0; 154 | ++next; 155 | } 156 | 157 | assert(i < sizeof(vg_flags) / sizeof(vg_flags[0]) - 1 && "Too many flags"); 158 | vg_flags[i++] = flags; 159 | 160 | flags = next; 161 | } 162 | 163 | vg_flags[i] = NULL; 164 | } 165 | 166 | char *log_dir_rel = getenv("PREGRIND_LOG_PATH"); 167 | if(log_dir_rel) { 168 | char *log_dir = log_dir_rel; 169 | if(log_dir[0] != '/') { 170 | log_dir = realpath(log_dir, 0); 171 | if(!log_dir) { 172 | fprintf(stderr, PREFIX "realpath() of %s failed: %s\n", log_dir_rel, sys_errlist[errno]); 173 | abort(); 174 | } 175 | 176 | // Absolutize to protect against chdirs 177 | if(0 != setenv("PREGRIND_LOG_PATH", log_dir, 1)) { 178 | fprintf(stderr, PREFIX "setenv() failed: %s\n", sys_errlist[errno]); 179 | abort(); 180 | } 181 | } 182 | 183 | const char *name = get_prog_name(); 184 | size_t name_len = strlen(name); 185 | 186 | size_t log_dir_len = strlen(log_dir); 187 | vg_log_path_templ = malloc(log_dir_len + 20); 188 | sprintf((char *)vg_log_path_templ, "%s/vg.%d.", log_dir, (int)getuid()); // FIXME: snprintf 189 | 190 | log_file = malloc(log_dir_len + name_len + 30); 191 | sprintf((char *)log_file, "%s/%s.%d.%d", log_dir, name, (int)getuid(), (int)getpid()); // FIXME: snprintf 192 | 193 | if(log_dir != log_dir_rel) 194 | free(log_dir); 195 | } 196 | 197 | const char *disable_ = getenv("PREGRIND_DISABLE"); 198 | if(disable_) { 199 | disable = atoi(disable_); 200 | } 201 | 202 | const char *blacklist_name = getenv("PREGRIND_BLACKLIST"); 203 | if(blacklist_name) { 204 | FILE *p = fopen(blacklist_name, "rb"); 205 | if(!p) { 206 | dprintf(get_log_fd(), PREFIX "failed to open blacklist file %s\n", blacklist_name); 207 | abort(); 208 | } 209 | 210 | char buf[128]; 211 | size_t i = 0; 212 | while(fgets(buf, sizeof(buf), p)) { 213 | char *s = buf; 214 | 215 | char *nl = strchr(s, '\n'); 216 | if(nl) 217 | *nl = 0; 218 | 219 | if(i >= sizeof(blacklist) / sizeof(blacklist[0])) { 220 | dprintf(get_log_fd(), PREFIX "failed to read patterns from %s: too many\n", blacklist_name); 221 | abort(); 222 | } 223 | 224 | // Strip comments 225 | char *comment = strchr(s, '#'); 226 | if(comment) 227 | *comment = 0; 228 | 229 | s = trim_whites(s); 230 | 231 | if(*s) 232 | blacklist[i++] = strdup(s); 233 | } 234 | 235 | fclose(p); 236 | } 237 | 238 | #define INIT_REAL(f) do { \ 239 | real_ ## f = (typeof(real_ ## f))dlsym(RTLD_NEXT, #f); \ 240 | assert(real_ ## f && "Failed to locate true exec"); \ 241 | } while(0) 242 | 243 | INIT_REAL(execl); 244 | INIT_REAL(execlp); 245 | INIT_REAL(execle); 246 | INIT_REAL(execv); 247 | INIT_REAL(execvp); 248 | INIT_REAL(execve); 249 | INIT_REAL(execvpe); 250 | INIT_REAL(posix_spawn); 251 | INIT_REAL(posix_spawnp); 252 | 253 | #undef INIT_REAL 254 | 255 | i_am_root = getuid() == 0; 256 | 257 | if(v) 258 | dprintf(get_log_fd(), PREFIX "initialized: v=%d, vg_log_path_templ=%s, log_file=%s, i_am_root=%d\n", v, vg_log_path_templ ? vg_log_path_templ : "(stderr)", log_file, i_am_root); 259 | 260 | // TODO: membar 261 | asm(""); 262 | is_initialized = 1; 263 | } 264 | 265 | // Avoid issues with async-safety of dlsym by reading symbols at startup 266 | __attribute__((constructor)) 267 | static void dummy() { 268 | maybe_init(); 269 | } 270 | 271 | static char **init_valgrind_argv(char * const *argv) { 272 | void *buf = safe_malloc(PAGE_SIZE >> 1, get_log_fd()); 273 | const char **new_args = buf; 274 | size_t max_args = GET_EFFECTIVE_SIZE(PAGE_SIZE >> 1) / sizeof(char *); 275 | 276 | new_args[0] = safe_strdup("/usr/bin/valgrind", get_log_fd()); 277 | ++new_args; 278 | --max_args; 279 | 280 | if(vg_log_path_templ) { 281 | char *name = safe_basename(argv[0]); 282 | size_t name_len = strlen(name); 283 | 284 | size_t templ_len = strlen(vg_log_path_templ); 285 | char *out = safe_malloc(templ_len + name_len + 20, get_log_fd()); 286 | sprintf(out, "--log-file=%s%s.%%p", vg_log_path_templ, name); // Valgrind understands %%p // FIXME: snprintf 287 | 288 | new_args[0] = out; 289 | ++new_args; 290 | --max_args; 291 | 292 | } 293 | 294 | const char **vg_flag; 295 | for(vg_flag = vg_flags; vg_flag[0]; ++vg_flag, ++new_args, --max_args) { 296 | assert(max_args && "Too many flags"); 297 | new_args[0] = safe_strdup(vg_flag[0], get_log_fd()); 298 | } 299 | 300 | for(; argv[0]; ++new_args, --max_args, ++argv) { 301 | assert(max_args && "Too many arguments"); 302 | new_args[0] = safe_strdup(argv[0], get_log_fd()); 303 | } 304 | 305 | if(v) { 306 | safe_puts(PREFIX "executing: "); 307 | for(const char **p = new_args; *p; ++p) { 308 | safe_puts(*p); 309 | safe_puts(" "); 310 | } 311 | safe_puts("\n"); 312 | } 313 | 314 | return (char **)buf; 315 | } 316 | 317 | static void free_valgrind_argv(char **argv) { 318 | for (size_t i = 0; argv[i]; ++i) 319 | safe_free(argv[i], get_log_fd()); 320 | safe_free(argv, get_log_fd()); 321 | } 322 | 323 | static const char *find_file_in_path(const char *file, char *buf, size_t buf_sz) { 324 | const char *path = getenv("PATH"); 325 | if(!path) 326 | return NULL; 327 | 328 | struct stat perm; 329 | do { 330 | char *next = strchr(path, ':'); 331 | 332 | int is_empty = (next && next == path + 1) || (!next && !path[0]); 333 | int needed; 334 | if(is_empty) { 335 | needed = snprintf(buf, buf_sz, "%s", file); 336 | } else { 337 | int len = next ? next - path : (int)strlen(path); 338 | needed = snprintf(buf, buf_sz, "%.*s/%s", len, path, file); 339 | } 340 | 341 | if(needed < 0 || (size_t)needed >= buf_sz) { 342 | safe_printf(PREFIX "failed to find file %s in path: string too long\n", file); 343 | return NULL; 344 | } 345 | 346 | if(0 == stat(buf, &perm)) 347 | return buf; 348 | 349 | path = next ? next + 1 : 0; 350 | } while(path); 351 | 352 | return NULL; 353 | } 354 | 355 | static int can_instrument(const char *arg0, char *const *argv) { 356 | if(!is_initialized) // If initializer hasn't been called, we can't do much (we have to be async-safe) 357 | return 0; 358 | 359 | if(disable) 360 | return 0; 361 | 362 | // Do not try to instrument Valgrind itself 363 | if(strstr(arg0, "valgrind") || strstr(argv[0], "valgrind")) { 364 | if(v) 365 | safe_printf(PREFIX "not instrumenting %s: it's Valgrind!\n", arg0); 366 | return 0; 367 | } 368 | 369 | char buf[256]; 370 | if(!strchr(arg0, '/')) { 371 | const char *path = find_file_in_path(arg0, buf, sizeof(buf)); 372 | if(!path) { 373 | safe_printf(PREFIX "not instrumenting %s: failed to find file in path\n", arg0); 374 | return 0; 375 | } 376 | arg0 = path; 377 | } 378 | 379 | size_t i; 380 | for(i = 0; i < sizeof(blacklist) / sizeof(blacklist[0]) && blacklist[i]; ++i) { 381 | if(safe_fnmatch(blacklist[i], arg0)) { 382 | if(v) 383 | safe_printf(PREFIX "not instrumenting %s: blacklisted\n", arg0); 384 | return 0; 385 | } 386 | } 387 | 388 | struct stat perm; 389 | if(0 != stat(arg0, &perm)) { 390 | if(v) 391 | safe_printf(PREFIX "stat() failed on %s: %s\n", arg0, sys_errlist[errno]); 392 | // Do not abort() as some packages seem to check for presense of files by trying to run them 393 | return 0; 394 | } 395 | 396 | // Avoid calling setuids as VG can't instrument them 397 | if(!i_am_root && (perm.st_mode & (S_ISUID | S_ISGID | S_ISVTX))) { 398 | if(v) 399 | safe_printf(PREFIX "not instrumenting %s: setuid\n", arg0); 400 | return 0; 401 | } 402 | 403 | return 1; 404 | } 405 | 406 | static int exec_worker(const char *arg0, char *const *argv, int file_or_path, int has_envp, char *const *envp) { 407 | if(!can_instrument(arg0, argv)) 408 | return has_envp && file_or_path ? real_execvpe(arg0, argv, envp) 409 | : has_envp && !file_or_path ? real_execve(arg0, argv, envp) 410 | : !has_envp && file_or_path ? real_execvp(arg0, argv) 411 | : /* !has_envp && !file_or_path */ real_execv(arg0, argv); 412 | 413 | char **new_argv = init_valgrind_argv(argv); 414 | 415 | int retcode = has_envp 416 | ? real_execve(new_argv[0], new_argv, envp) 417 | : real_execv(new_argv[0], new_argv); 418 | 419 | if (0 != retcode) { 420 | free_valgrind_argv(new_argv); 421 | } 422 | 423 | return retcode; 424 | } 425 | 426 | EXPORT int execl(const char *path, const char *arg, ...) { 427 | if(v) 428 | safe_printf(PREFIX "intercepted execl: %s\n", path); 429 | 430 | va_list ap; 431 | va_start(ap, arg); 432 | char **args = va_list_to_argv(ap, arg); 433 | 434 | return exec_worker(path, args, /*file_or_path*/0, /*has_envp*/ 0, 0); 435 | } 436 | 437 | EXPORT int execlp(const char *file, const char *arg, ...) { 438 | if(v) 439 | safe_printf(PREFIX "intercepted execlp: %s\n", file); 440 | 441 | va_list ap; 442 | va_start(ap, arg); 443 | char **args = va_list_to_argv(ap, arg); 444 | 445 | return exec_worker(file, args, /*file_or_path*/ 1, /*has_envp*/ 0, 0); 446 | } 447 | 448 | EXPORT int execle(const char *path, const char *arg, ...) { 449 | if(v) 450 | safe_printf(PREFIX "intercepted execle: %s\n", path); 451 | 452 | va_list ap; 453 | va_start(ap, arg); 454 | char **args = va_list_to_argv(ap, arg); 455 | 456 | char * const *e = va_arg(ap, char * const *); 457 | 458 | return exec_worker(path, args, /*file_or_path*/ 0, /*has_envp*/ 1, e); 459 | } 460 | 461 | EXPORT int execv(const char *path, char *const argv[]) { 462 | if(v) 463 | safe_printf(PREFIX "intercepted execv: %s\n", path); 464 | return exec_worker(path, argv, /*file_or_path*/ 0, /*has_envp*/ 0, 0); 465 | } 466 | 467 | EXPORT int execve(const char *path, char *const argv[], char *const envp[]) { 468 | if(v) 469 | safe_printf(PREFIX "intercepted execve: %s\n", path); 470 | return exec_worker(path, argv, /*file_or_path*/ 0, /*has_envp*/ 1, envp); 471 | } 472 | 473 | EXPORT int execvp(const char *file, char *const argv[]) { 474 | if(v) 475 | safe_printf(PREFIX "intercepted execvp: %s\n", file); 476 | return exec_worker(file, argv, /*file_or_path*/ 1, /*has_envp*/ 0, 0); 477 | } 478 | 479 | EXPORT int execvpe(const char *file, char *const argv[], char *const envp[]) { 480 | if(v) 481 | safe_printf(PREFIX "intercepted execvpe: %s\n", file); 482 | return exec_worker(file, argv, /*file_or_path*/ 1, /*has_envp*/ 1, envp); 483 | } 484 | 485 | static int spawn_worker(pid_t *pid, const char *path, 486 | const posix_spawn_file_actions_t *file_actions, 487 | const posix_spawnattr_t *attrp, 488 | char *const *argv, char *const *envp, 489 | int path_or_file) { 490 | if(!can_instrument(path, argv)) 491 | return (path_or_file ? real_posix_spawn : real_posix_spawnp)(pid, path, file_actions, attrp, argv, envp); 492 | 493 | char **new_argv = init_valgrind_argv(argv); 494 | 495 | int status = real_posix_spawnp(pid, "valgrind", file_actions, attrp, new_argv, envp); 496 | 497 | free_valgrind_argv(new_argv); 498 | 499 | return status; 500 | } 501 | 502 | EXPORT int posix_spawn(pid_t *pid, const char *path, 503 | const posix_spawn_file_actions_t *file_actions, 504 | const posix_spawnattr_t *attrp, 505 | char *const argv[], char *const envp[]) { 506 | return spawn_worker(pid, path, file_actions, attrp, argv, envp, 1); 507 | } 508 | 509 | EXPORT int posix_spawnp(pid_t *pid, const char *path, 510 | const posix_spawn_file_actions_t *file_actions, 511 | const posix_spawnattr_t *attrp, 512 | char *const argv[], char *const envp[]) { 513 | return spawn_worker(pid, path, file_actions, attrp, argv, envp, 0); 514 | } 515 | 516 | // TODO: execlpe 517 | -------------------------------------------------------------------------------- /tests/exec/child.c: -------------------------------------------------------------------------------- 1 | /* * Copyright 2022 Yury Gribov 2 | * 3 | * The MIT License (MIT) 4 | *▫ 5 | * Use of this source code is governed by MIT license that can be 6 | * found in the LICENSE.txt file. 7 | */ 8 | 9 | #include 10 | 11 | #ifdef __clang__ 12 | # define noipa optnone 13 | #elif __GNUC__ < 8 14 | # define noipa noinline,noclone 15 | #endif 16 | 17 | int *buf; 18 | 19 | __attribute__((noipa)) 20 | int error() { 21 | return buf[1]; 22 | } 23 | 24 | int main() { 25 | buf = (int *)malloc(1); 26 | return error(); 27 | } 28 | -------------------------------------------------------------------------------- /tests/exec/parent.c: -------------------------------------------------------------------------------- 1 | /* * Copyright 2022 Yury Gribov 2 | * 3 | * The MIT License (MIT) 4 | *▫ 5 | * Use of this source code is governed by MIT license that can be 6 | * found in the LICENSE.txt file. 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | int main() { 16 | int pid = fork(); 17 | if (0 == pid) { 18 | // Child 19 | execl("./child", "./child", NULL); 20 | perror("parent: failed to execute child"); 21 | exit(1); 22 | } 23 | // Parent 24 | if (pid < 0) { 25 | perror("parent: failed to fork"); 26 | exit(1); 27 | } 28 | int wstatus; 29 | if (waitpid(pid, &wstatus, 0) < 0) { 30 | perror("parent: failed to wait"); 31 | exit(1); 32 | } 33 | if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != RC) { 34 | fprintf(stderr, "parent: child exited for different reason\n"); 35 | exit(1); 36 | } 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /tests/exec/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 Yury Gribov 4 | # 5 | # The MIT License (MIT) 6 | # 7 | # Use of this source code is governed by MIT license that can be 8 | # found in the LICENSE.txt file. 9 | 10 | # This is a simple test for valgrind-preload functionality. 11 | 12 | set -eu 13 | 14 | cd $(dirname $0) 15 | 16 | if test -n "${GITHUB_ACTIONS:-}"; then 17 | set -x 18 | fi 19 | 20 | RC=23 21 | CFLAGS="-g -O2 -Wall -Wextra -Werror -DRC=$RC" 22 | 23 | if test -n "${COVERAGE:-}"; then 24 | CFLAGS="$CFLAGS --coverage -DNDEBUG" 25 | CFLAGS="$CFLAGS -fprofile-dir=coverage.%p" 26 | fi 27 | 28 | ROOT=$PWD/../.. 29 | 30 | ${CC:-gcc} $CFLAGS parent.c -o parent 31 | ${CC:-gcc} $CFLAGS child.c -o child 32 | 33 | export PREGRIND_FLAGS="-q --error-exitcode=$RC" 34 | 35 | if ! LD_PRELOAD=$ROOT/bin/libpregrind.so ./parent > test.log 2>&1; then 36 | echo "exec: test failed" >&2 37 | cat test.log >&2 38 | fi 39 | 40 | if test -n "${COVERAGE:-}"; then 41 | # Merge DLL coverage from both processes 42 | gcov-tool merge coverage.* 43 | rm -rf coverage.* 44 | fi 45 | -------------------------------------------------------------------------------- /tests/spawn/child.c: -------------------------------------------------------------------------------- 1 | /* * Copyright 2022 Yury Gribov 2 | * 3 | * The MIT License (MIT) 4 | *▫ 5 | * Use of this source code is governed by MIT license that can be 6 | * found in the LICENSE.txt file. 7 | */ 8 | 9 | #include 10 | 11 | #ifdef __clang__ 12 | # define noipa optnone 13 | #elif __GNUC__ < 8 14 | # define noipa noinline,noclone 15 | #endif 16 | 17 | int *buf; 18 | 19 | __attribute__((noipa)) 20 | int error() { 21 | return buf[1]; 22 | } 23 | 24 | int main() { 25 | buf = (int *)malloc(1); 26 | return error(); 27 | } 28 | -------------------------------------------------------------------------------- /tests/spawn/parent.c: -------------------------------------------------------------------------------- 1 | /* * Copyright 2022 Yury Gribov 2 | * 3 | * The MIT License (MIT) 4 | *▫ 5 | * Use of this source code is governed by MIT license that can be 6 | * found in the LICENSE.txt file. 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | extern char **environ; 17 | 18 | int main() { 19 | char *argv[] = {"./child", 0}; 20 | int pid; 21 | if (0 != posix_spawn(&pid, "./child", NULL, NULL, argv, environ)) { 22 | perror("parent: failed to spawn child"); 23 | exit(1); 24 | } 25 | int wstatus; 26 | if (waitpid(pid, &wstatus, 0) < 0) { 27 | perror("parent: failed to wait for child"); 28 | exit(1); 29 | } 30 | if (!WIFEXITED(wstatus) && WEXITSTATUS(wstatus) != RC) { 31 | fprintf(stderr, "parent: child exited for different reason\n"); 32 | exit(1); 33 | } 34 | return 0; 35 | } 36 | -------------------------------------------------------------------------------- /tests/spawn/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 Yury Gribov 4 | # 5 | # The MIT License (MIT) 6 | # 7 | # Use of this source code is governed by MIT license that can be 8 | # found in the LICENSE.txt file. 9 | 10 | # This is a simple test for valgrind-preload functionality. 11 | 12 | set -eu 13 | 14 | cd $(dirname $0) 15 | 16 | if test -n "${GITHUB_ACTIONS:-}"; then 17 | set -x 18 | fi 19 | 20 | RC=23 21 | CFLAGS="-g -O0 -Wall -Wextra -Werror -DRC=$RC" 22 | 23 | if test -n "${COVERAGE:-}"; then 24 | CFLAGS="$CFLAGS --coverage -DNDEBUG" 25 | CFLAGS="$CFLAGS -fprofile-dir=coverage.%p" 26 | fi 27 | 28 | ROOT=$PWD/../.. 29 | 30 | ${CC:-gcc} $CFLAGS parent.c -o parent 31 | ${CC:-gcc} $CFLAGS child.c -o child 32 | 33 | export PREGRIND_FLAGS="-q --error-exitcode=$RC" 34 | 35 | if ! LD_PRELOAD=$ROOT/bin/libpregrind.so ./parent > test.log 2>&1 \ 36 | || ! grep -q 'Invalid read of size 4' test.log; then 37 | echo "spawn: test failed" >&2 38 | cat test.log >&2 39 | fi 40 | 41 | if test -n "${COVERAGE:-}"; then 42 | # Merge DLL coverage from both processes 43 | gcov-tool merge coverage.* 44 | rm -rf coverage.* 45 | fi 46 | -------------------------------------------------------------------------------- /tests/system/child.c: -------------------------------------------------------------------------------- 1 | /* * Copyright 2022 Yury Gribov 2 | * 3 | * The MIT License (MIT) 4 | *▫ 5 | * Use of this source code is governed by MIT license that can be 6 | * found in the LICENSE.txt file. 7 | */ 8 | 9 | #include 10 | 11 | #ifdef __clang__ 12 | # define noipa optnone 13 | #elif __GNUC__ < 8 14 | # define noipa noinline,noclone 15 | #endif 16 | 17 | int *buf; 18 | 19 | __attribute__((noipa)) 20 | int error() { 21 | return buf[1]; 22 | } 23 | 24 | int main() { 25 | buf = (int *)malloc(1); 26 | return error(); 27 | } 28 | -------------------------------------------------------------------------------- /tests/system/parent.c: -------------------------------------------------------------------------------- 1 | /* * Copyright 2022 Yury Gribov 2 | * 3 | * The MIT License (MIT) 4 | *▫ 5 | * Use of this source code is governed by MIT license that can be 6 | * found in the LICENSE.txt file. 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | int main() { 13 | if (0 == system("./child")) { 14 | fprintf(stderr, "parent: child did not fail as expected\n"); 15 | } 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /tests/system/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2022 Yury Gribov 4 | # 5 | # The MIT License (MIT) 6 | # 7 | # Use of this source code is governed by MIT license that can be 8 | # found in the LICENSE.txt file. 9 | 10 | # This is a simple test for valgrind-preload functionality. 11 | 12 | set -eu 13 | 14 | cd $(dirname $0) 15 | 16 | if test -n "${GITHUB_ACTIONS:-}"; then 17 | set -x 18 | fi 19 | 20 | CFLAGS='-g -O0 -Wall -Wextra -Werror' 21 | 22 | if test -n "${COVERAGE:-}"; then 23 | CFLAGS="$CFLAGS --coverage -DNDEBUG" 24 | CFLAGS="$CFLAGS -fprofile-dir=coverage.%p" 25 | fi 26 | 27 | ROOT=$PWD/../.. 28 | 29 | ${CC:-gcc} $CFLAGS parent.c -o parent 30 | ${CC:-gcc} $CFLAGS child.c -o child 31 | 32 | export PREGRIND_FLAGS='-q --error-exitcode=1' 33 | 34 | if ! LD_PRELOAD=$ROOT/bin/libpregrind.so ./parent >test.log 2>&1 \ 35 | || ! grep -q 'Invalid read of size 4' test.log; then 36 | echo "system: test failed" >&2 37 | cat test.log >&2 38 | fi 39 | 40 | if test -n "${COVERAGE:-}"; then 41 | # Merge DLL coverage from both processes 42 | gcov-tool merge coverage.* 43 | rm -rf coverage.* 44 | fi 45 | --------------------------------------------------------------------------------