├── AUTHORS ├── LICENSE ├── README.md ├── covnavi.py ├── gather_coverage.sh └── import_coverage_info.py /AUTHORS: -------------------------------------------------------------------------------- 1 | This code was developed as part of a project with the Cisco Talos VULNDEV Team 2 | 3 | Authors: 4 | Aleksandar Nikolic 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Cisco Systems, Inc 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Covnavi 2 | 3 | Code coverage navigation and analysis. 4 | 5 | This tool is based on gcov (for gathering code coverage info) and joern (for making queries and analysis). 6 | 7 | Main idea is to analyze the progress of a coverage based fuzzer (such as AFL). Usually, one would run gcov/lcov over AFL generated files to check total code coverage and/or generate html reports for easy visual reference. This tool aims to supplement that workflow by pointing out the parts of the code where coverage stops. 8 | 9 | In short, we want to find places in the code where fuzzer got stuck. Places in conditional branches where at least one branch was never taken. In interesting cases, this would mean code locations where fuzzer was unable to synthetize an input which would be true for that branch. Non-interesting cases can be error checks, like post-malloc() null check, which depend on external events and don't really depend on fuzzer coverage, but these can easily be ignored. 10 | 11 | How does this work? 12 | 13 | There are a couple of steps: 14 | a) generating inputs for the target (by fuzzing with coverage based fuzzer), or just amassing a suitable corpus 15 | b) gathering code coverage with the said corpus 16 | - we use gcov for this meaning that code needs to be compiled with gcc with `-coverage` or `-fprofile-arcs -ftest-coverage` options 17 | - when properly compiled, it's simply a matter of running the testcases against the target which can be done in a simple for loop 18 | c) gather per line coverage info 19 | - using gcov to produce source code files augmented with per-line coverage information 20 | d) import source into a joern graph database for further queries 21 | e) craft joern queries to extract branch information 22 | - extract each if and switch statements and all their branches 23 | f) check coverage for each branch of previously extracted statements 24 | g) and finally, save the conditional statements where at least one branch was executed and at least one was not 25 | - this means that the conditional statement wasn't fully explored and might be of interest for manual analysis 26 | 27 | After all the above processing, we are left with a list of code locations where the fuzzer "got stuck". 28 | We can use this information to browse the code and see what kind of changes we can make, to both our fuzzing approach and the code being fuzzed, to improve the coverage. 29 | 30 | Installation requirements 31 | 32 | In a recommended setup, the box running the coverage analysis should have the following: 33 | - lcov 34 | - gcov 35 | - docker 36 | - python 37 | - sqlite3 38 | 39 | `lcov` is required to generate html coverage report for futher review. `gcov` is used to generate the per-line coverage info. Setting up joern can be a bit daunting so I find using a docker image to be pretty painless. 40 | 41 | How to run: 42 | 43 | To showcase how to actually use this , I'll go through an example of setting up and analyzing openjpeg fuzzing run coverage. 44 | 45 | First of all, I like to keep three separate instances of code: 46 | ``` 47 | ea@ubuntu:~/$ mkdir openjpeg 48 | ea@ubuntu:~/$ cd openjpeg 49 | ea@ubuntu:~/openjpeg$ mkdir fuzz cov covnavi 50 | ``` 51 | Directory `fuzz` is for AFL-instrumented code and binaries, `cov` for gcc `-coverage` instrumented binaries and `covnavi` is for reduced set of complete source code. I won't go through the fuzzing process, obviouslly... 52 | 53 | Let's start with gathering coverage. 54 | 55 | Get the code: 56 | ``` 57 | ea@ubuntu:~/openjpeg$ cd cov 58 | ea@ubuntu:~/openjpeg/cov$ git clone https://github.com/uclouvain/openjpeg.git 59 | Cloning into 'openjpeg'... 60 | remote: Counting objects: 29011, done. 61 | remote: Compressing objects: 100% (126/126), done. 62 | remote: Total 29011 (delta 135), reused 227 (delta 95), pack-reused 28725 63 | Receiving objects: 100% (29011/29011), 68.13 MiB | 5.33 MiB/s, done. 64 | Resolving deltas: 100% (20593/20593), done. 65 | Checking connectivity... done. 66 | ea@ubuntu:~/openjpeg/cov$ cd openjpeg 67 | ea@ubuntu:~/openjpeg/cov/openjpeg$ 68 | ``` 69 | Set compiler flags (make sure CC isn't set to afl-clang-fast or anything other than gcc). All we need is `-coverage` which is synonymous with `-fprofile-arcs -ftest-coverage` on newer gcc versions. 70 | ``` 71 | ea@ubuntu:~/openjpeg/cov/openjpeg$ unset CC 72 | ea@ubuntu:~/openjpeg/cov/openjpeg$ unset CXX 73 | ea@ubuntu:~/openjpeg/cov/openjpeg$ export CFLAGS="-coverage" 74 | ea@ubuntu:~/openjpeg/cov/openjpeg$ export CXXFLAGS="-coverage" 75 | ``` 76 | And make the code: 77 | ``` 78 | ea@ubuntu:~/openjpeg/cov/openjpeg$ cmake . 79 | -- Your system seems to have a Z lib available, we will use it to generate PNG lib 80 | -- Your system seems to have a PNG lib available, we will use it 81 | -- Could NOT find TIFF (missing: TIFF_LIBRARY TIFF_INCLUDE_DIR) 82 | -- TIFF lib not found, activate BUILD_THIRDPARTY if you want build it 83 | -- Could NOT find LCMS2 (missing: LCMS2_LIBRARY LCMS2_INCLUDE_DIR) 84 | -- Could NOT find LCMS (missing: LCMS_LIBRARY LCMS_INCLUDE_DIR) 85 | -- LCMS2 or LCMS lib not found, activate BUILD_THIRDPARTY if you want build it 86 | -- Configuring done 87 | -- Generating done 88 | -- Build files have been written to: /home/ea/openjpeg/cov/openjpeg 89 | ea@ubuntu:~/openjpeg/cov/openjpeg$ make 90 | [ 1%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/thread.c.o 91 | [ 3%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/bio.c.o 92 | [ 4%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/cio.c.o 93 | [ 6%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/dwt.c.o 94 | [ 7%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/event.c.o 95 | [ 9%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/image.c.o 96 | [ 10%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/invert.c.o 97 | [ 12%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/j2k.c.o 98 | [ 13%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/jp2.c.o 99 | [ 15%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/mct.c.o 100 | [ 16%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/mqc.c.o 101 | [ 18%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/openjpeg.c.o 102 | [ 19%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/opj_clock.c.o 103 | [ 21%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/pi.c.o 104 | [ 22%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/t1.c.o 105 | [ 24%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/t2.c.o 106 | [ 25%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/tcd.c.o 107 | [ 27%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/tgt.c.o 108 | [ 28%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/function_list.c.o 109 | [ 30%] Building C object src/lib/openjp2/CMakeFiles/openjp2_static.dir/opj_malloc.c.o 110 | [ 31%] Linking C static library ../../../bin/libopenjp2.a 111 | [ 31%] Built target openjp2_static 112 | [ 33%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/thread.c.o 113 | [ 34%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/bio.c.o 114 | [ 36%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/cio.c.o 115 | [ 37%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/dwt.c.o 116 | [ 39%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/event.c.o 117 | [ 40%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/image.c.o 118 | [ 42%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/invert.c.o 119 | [ 43%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/j2k.c.o 120 | [ 45%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/jp2.c.o 121 | [ 46%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/mct.c.o 122 | [ 48%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/mqc.c.o 123 | [ 50%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/openjpeg.c.o 124 | [ 51%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/opj_clock.c.o 125 | [ 53%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/pi.c.o 126 | [ 54%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/t1.c.o 127 | [ 56%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/t2.c.o 128 | [ 57%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/tcd.c.o 129 | [ 59%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/tgt.c.o 130 | [ 60%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/function_list.c.o 131 | [ 62%] Building C object src/lib/openjp2/CMakeFiles/openjp2.dir/opj_malloc.c.o 132 | [ 63%] Linking C shared library ../../../bin/libopenjp2.so 133 | [ 63%] Built target openjp2 134 | [ 65%] Building C object src/bin/jp2/CMakeFiles/opj_compress.dir/opj_compress.c.o 135 | [ 66%] Building C object src/bin/jp2/CMakeFiles/opj_compress.dir/convert.c.o 136 | [ 68%] Building C object src/bin/jp2/CMakeFiles/opj_compress.dir/convertbmp.c.o 137 | [ 69%] Building C object src/bin/jp2/CMakeFiles/opj_compress.dir/index.c.o 138 | [ 71%] Building C object src/bin/jp2/CMakeFiles/opj_compress.dir/__/common/color.c.o 139 | [ 72%] Building C object src/bin/jp2/CMakeFiles/opj_compress.dir/__/common/opj_getopt.c.o 140 | [ 74%] Building C object src/bin/jp2/CMakeFiles/opj_compress.dir/convertpng.c.o 141 | [ 75%] Linking C executable ../../../bin/opj_compress 142 | [ 75%] Built target opj_compress 143 | [ 77%] Building C object src/bin/jp2/CMakeFiles/opj_dump.dir/opj_dump.c.o 144 | [ 78%] Building C object src/bin/jp2/CMakeFiles/opj_dump.dir/convert.c.o 145 | [ 80%] Building C object src/bin/jp2/CMakeFiles/opj_dump.dir/convertbmp.c.o 146 | [ 81%] Building C object src/bin/jp2/CMakeFiles/opj_dump.dir/index.c.o 147 | [ 83%] Building C object src/bin/jp2/CMakeFiles/opj_dump.dir/__/common/color.c.o 148 | [ 84%] Building C object src/bin/jp2/CMakeFiles/opj_dump.dir/__/common/opj_getopt.c.o 149 | [ 86%] Building C object src/bin/jp2/CMakeFiles/opj_dump.dir/convertpng.c.o 150 | [ 87%] Linking C executable ../../../bin/opj_dump 151 | [ 87%] Built target opj_dump 152 | [ 89%] Building C object src/bin/jp2/CMakeFiles/opj_decompress.dir/opj_decompress.c.o 153 | [ 90%] Building C object src/bin/jp2/CMakeFiles/opj_decompress.dir/convert.c.o 154 | [ 92%] Building C object src/bin/jp2/CMakeFiles/opj_decompress.dir/convertbmp.c.o 155 | [ 93%] Building C object src/bin/jp2/CMakeFiles/opj_decompress.dir/index.c.o 156 | [ 95%] Building C object src/bin/jp2/CMakeFiles/opj_decompress.dir/__/common/color.c.o 157 | [ 96%] Building C object src/bin/jp2/CMakeFiles/opj_decompress.dir/__/common/opj_getopt.c.o 158 | [ 98%] Building C object src/bin/jp2/CMakeFiles/opj_decompress.dir/convertpng.c.o 159 | [100%] Linking C executable ../../../bin/opj_decompress 160 | [100%] Built target opj_decompress 161 | ea@ubuntu:~/openjpeg/cov/openjpeg$ 162 | ``` 163 | 164 | In order to confirm that code was compiled with coverage profiling enabled do something like: 165 | ``` 166 | ea@ubuntu:~/openjpeg/cov/openjpeg$ find . -name *.gcno 167 | ./src/bin/jp2/CMakeFiles/opj_compress.dir/__/common/opj_getopt.c.gcno 168 | ./src/bin/jp2/CMakeFiles/opj_compress.dir/__/common/color.c.gcno 169 | ./src/bin/jp2/CMakeFiles/opj_compress.dir/convertbmp.c.gcno 170 | ./src/bin/jp2/CMakeFiles/opj_compress.dir/convertpng.c.gcno 171 | ./src/bin/jp2/CMakeFiles/opj_compress.dir/index.c.gcno 172 | ./src/bin/jp2/CMakeFiles/opj_compress.dir/convert.c.gcno 173 | ./src/bin/jp2/CMakeFiles/opj_compress.dir/opj_compress.c.gcno 174 | ./src/bin/jp2/CMakeFiles/opj_dump.dir/opj_dump.c.gcno 175 | ./src/bin/jp2/CMakeFiles/opj_dump.dir/__/common/opj_getopt.c. 176 | ``` 177 | For each compilation module, there should be a `gcno` file associated with it. 178 | 179 | Ok, now we are ready to run our target against our testcases. I usually do something like a simple for loop through all the files with specific target options, but this depends on your target. Coverage information accumulates over multiple runs. In this case, something like this would do: 180 | 181 | ``` 182 | ea@ubuntu:~/openjpeg/cov/openjpeg/bin$ for i in `ls ../jp2_corpus/`; do ./opj_decompress -i ../jp2_corpus/$i -o /tmp/dump.pgm ; done 183 | ``` 184 | 185 | Now, once all the files have been run through (note that some might cause an infinite loop or a crash, so handle that appropriatelly), we can actually gather coverage info. This is what the `gather_coverage.sh` script is for. Get it in the `cov` directory and run like so: 186 | 187 | ``` 188 | ea@ubuntu:~/openjpeg/cov$ sh gather_coverage.sh openjpeg 189 | Running lcov... 190 | Generating html coverage report... 191 | Processing per-line coverage info... 192 | -gathering files 193 | -removing prefixes 194 | -processing lines 195 | Importing into database... 196 | ea@ubuntu:~/openjpeg/cov$ 197 | ``` 198 | 199 | This script does a couple of things, so let's go through it real quick: 200 | 1. runs lcov over the project: 201 | `lcov --rc lcov_branch_coverage=1 --no-checksum --capture --directory $SOURCE_ROOT --output-file coverage.in --quiet` 202 | This results in `coverage.in` file. 203 | 2. runs `genhtml` which uses `coverage.in` from the previous step: 204 | `genhtml coverage.in --output-directory ./web --quiet` 205 | 3. runs `gcov` over each `gcda` file found in the project. This results in extracted source files with added coverate info per line. 206 | 4. Processes those source files and imports them into a sqlite database for later use. 207 | 208 | The product of this script is `web` directory which contains the generated html report of the coverage and `openjpeg.db` database which contains a single table consisting of filename-line-coverage info. 209 | 210 | For example, this is how the extracted files look like: 211 | ``` 212 | ea@ubuntu:~/openjpeg/cov/tmp$ ls -l 213 | total 2240 214 | -rw-rw-r-- 1 ea ea 50923 Aug 28 09:31 #src#bin#common#color.c.gcov 215 | -rw-rw-r-- 1 ea ea 15106 Aug 28 09:31 #src#bin#common#opj_getopt.c.gcov 216 | -rw-rw-r-- 1 ea ea 4160 Aug 28 09:31 #src#bin#common#opj_string.h.gcov 217 | -rw-rw-r-- 1 ea ea 58876 Aug 28 09:31 #src#bin#jp2#convertbmp.c.gcov 218 | -rw-rw-r-- 1 ea ea 120952 Aug 28 09:31 #src#bin#jp2#convert.c.gcov 219 | -rw-rw-r-- 1 ea ea 24758 Aug 28 09:31 #src#bin#jp2#convertpng.c.gcov 220 | -rw-rw-r-- 1 ea ea 29793 Aug 28 09:31 #src#bin#jp2#index.c.gcov 221 | -rw-rw-r-- 1 ea ea 90785 Aug 28 09:31 #src#bin#jp2#opj_decompress.c.gcov 222 | -rw-rw-r-- 1 ea ea 9736 Aug 28 09:31 #src#lib#openjp2#bio.c.gcov 223 | -rw-rw-r-- 1 ea ea 35691 Aug 28 09:31 #src#lib#openjp2#cio.c.gcov 224 | -rw-rw-r-- 1 ea ea 125010 Aug 28 09:31 #src#lib#openjp2#dwt.c.gcov 225 | -rw-rw-r-- 1 ea ea 7807 Aug 28 09:31 #src#lib#openjp2#event.c.gcov 226 | -rw-rw-r-- 1 ea ea 6610 Aug 28 09:31 #src#lib#openjp2#function_list.c.gcov 227 | -rw-rw-r-- 1 ea ea 14406 Aug 28 09:31 #src#lib#openjp2#image.c.gcov 228 | -rw-rw-r-- 1 ea ea 15483 Aug 28 09:31 #src#lib#openjp2#invert.c.gcov 229 | -rw-rw-r-- 1 ea ea 635614 Aug 28 09:31 #src#lib#openjp2#j2k.c.gcov 230 | -rw-rw-r-- 1 ea ea 170945 Aug 28 09:31 #src#lib#openjp2#jp2.c.gcov 231 | -rw-rw-r-- 1 ea ea 27047 Aug 28 09:31 #src#lib#openjp2#mct.c.gcov 232 | -rw-rw-r-- 1 ea ea 28227 Aug 28 09:31 #src#lib#openjp2#mqc.c.gcov 233 | -rw-rw-r-- 1 ea ea 9779 Aug 28 09:31 #src#lib#openjp2#mqc_inl.h.gcov 234 | -rw-rw-r-- 1 ea ea 50562 Aug 28 09:31 #src#lib#openjp2#openjpeg.c.gcov 235 | -rw-rw-r-- 1 ea ea 4229 Aug 28 09:31 #src#lib#openjp2#opj_clock.c.gcov 236 | -rw-rw-r-- 1 ea ea 11729 Aug 28 09:31 #src#lib#openjp2#opj_includes.h.gcov 237 | -rw-rw-r-- 1 ea ea 11405 Aug 28 09:31 #src#lib#openjp2#opj_intmath.h.gcov 238 | -rw-rw-r-- 1 ea ea 12677 Aug 28 09:31 #src#lib#openjp2#opj_malloc.c.gcov 239 | -rw-rw-r-- 1 ea ea 115606 Aug 28 09:31 #src#lib#openjp2#pi.c.gcov 240 | -rw-rw-r-- 1 ea ea 115707 Aug 28 09:31 #src#lib#openjp2#t1.c.gcov 241 | -rw-rw-r-- 1 ea ea 83342 Aug 28 09:31 #src#lib#openjp2#t2.c.gcov 242 | -rw-rw-r-- 1 ea ea 135209 Aug 28 09:31 #src#lib#openjp2#tcd.c.gcov 243 | -rw-rw-r-- 1 ea ea 16408 Aug 28 09:31 #src#lib#openjp2#tgt.c.gcov 244 | -rw-rw-r-- 1 ea ea 39701 Aug 28 09:31 #src#lib#openjp2#thread.c.gcov 245 | -rw-rw-r-- 1 ea ea 76126 Aug 28 09:31 #usr#lib#gcc#x86_64-linux-gnu#5#include#emmintrin.h.gcov 246 | -rw-rw-r-- 1 ea ea 62733 Aug 28 09:31 #usr#lib#gcc#x86_64-linux-gnu#5#include#xmmintrin.h.gcov 247 | ea@ubuntu:~/openjpeg/cov/tmp$ 248 | ``` 249 | 250 | Sharps are used to denote dir delimiters to preserve paths. And the files themselves look like: 251 | ``` 252 | 2105: 482: assert(cio != 00); 253 | 2105: 483: assert(box != 00); 254 | 2105: 484: assert(p_number_bytes_read != 00); 255 | 2105: 485: assert(p_manager != 00); 256 | -: 486: 257 | 2105: 487: *p_number_bytes_read = (OPJ_UINT32)opj_stream_read_data(cio, l_data_header, 8, 258 | -: 488: p_manager); 259 | 2105: 489: if (*p_number_bytes_read != 8) { 260 | 13: 490: return OPJ_FALSE; 261 | -: 491: } 262 | -: 492: 263 | -: 493: /* process read data */ 264 | 2092: 494: opj_read_bytes(l_data_header, &(box->length), 4); 265 | 2092: 495: opj_read_bytes(l_data_header + 4, &(box->type), 4); 266 | -: 496: 267 | 2092: 497: if (box->length == 0) { /* last box */ 268 | -: 496: 269 | 2092: 497: if (box->length == 0) { /* last box */ 270 | 120: 498: const OPJ_OFF_T bleft = opj_stream_get_number_byte_left(cio); 271 | 120: 499: if (bleft > (OPJ_OFF_T)(0xFFFFFFFFU - 8U)) { 272 | #####: 500: opj_event_msg(p_manager, EVT_ERROR, 273 | -: 501: "Cannot handle box sizes higher than 2^32\n"); 274 | #####: 502: return OPJ_FALSE; 275 | -: 503: } 276 | 120: 504: box->length = (OPJ_UINT32)bleft + 8U; 277 | 120: 505: assert((OPJ_OFF_T)box->length == bleft + 8); 278 | 120: 506: return OPJ_TRUE; 279 | -: 507: } 280 | ``` 281 | 282 | 283 | First column of numbers is coverage. Number means the time certain line was executed, `-` means it's not code, and `#####` means it wasn't executed. For example: 284 | ``` 285 | 120: 499: if (bleft > (OPJ_OFF_T)(0xFFFFFFFFU - 8U)) { 286 | #####: 500: opj_event_msg(p_manager, EVT_ERROR, 287 | -: 501: "Cannot handle box sizes higher than 2^32\n"); 288 | #####: 502: return OPJ_FALSE; 289 | -: 503: } 290 | 120: 504: box->length = (OPJ_UINT32)bleft + 8U; 291 | ``` 292 | In the above, we can see line 499 was executed 120 times, but the condition was never true so inside of the IF was never reached. 293 | 294 | This is what we want to find automatically... 295 | 296 | 297 | 298 | Setting up joern 299 | 300 | Now the second part. Since joern is written in java and depends on a number of very specific versions of very specific libraries, the easiest way to get it running is to use an existing docker image. This makes the whole thing pretty simple. 301 | 302 | Other than docker, we'll need python-joern installed on the box that runs the analysis. It's simpler that way than running the whole thing inside docker. And installing `python-joern` is fairly painless. First we need py2neo version 2.0 for neo4j access 303 | 304 | ``` 305 | ea@ubuntu:~/openjpeg/cov$ sudo pip install py2neo==2.0 306 | ``` 307 | The rest is: 308 | ``` 309 | wget https://github.com/fabsx00/python-joern/archive/0.3.1.tar.gz 310 | tar xfzv 0.3.1.tar.gz 311 | cd python-joern-0.3.1 312 | sudo python2 setup.py install 313 | ``` 314 | 315 | Now that that's settled, we can get those docker images: 316 | 317 | ``` 318 | sudo docker pull neepl/joern 319 | ``` 320 | 321 | Next step is to import target source code into a joern db. I mentioned earlier how we want a stripped down version of the repository. The reason for this is that joern indexes everythin in the repository including makefiles, readmes ... We can speed it up quite a bit by removing unneeded stuff. For example with openjpeg: 322 | 323 | ``` 324 | ea@ubuntu:~/openjpeg/covnavi$ git clone https://github.com/uclouvain/openjpeg.git 325 | Cloning into 'openjpeg'... 326 | remote: Counting objects: 29030, done. 327 | remote: Compressing objects: 100% (131/131), done. 328 | remote: Total 29030 (delta 145), reused 244 (delta 103), pack-reused 28725 329 | Receiving objects: 100% (29030/29030), 68.19 MiB | 4.79 MiB/s, done. 330 | Resolving deltas: 100% (20603/20603), done. 331 | Checking connectivity... done. 332 | ea@ubuntu:~/openjpeg/covnavi$ ls 333 | openjpeg 334 | ea@ubuntu:~/openjpeg/covnavi$ cd openjpeg/ 335 | ea@ubuntu:~/openjpeg/covnavi/openjpeg$ ls 336 | appveyor.yml CHANGELOG.md CMakeLists.txt doc LICENSE README.md src THANKS.md tools 337 | AUTHORS.md cmake CTestConfig.cmake INSTALL.md NEWS.md scripts tests thirdparty wrapping 338 | ea@ubuntu:~/openjpeg/covnavi/openjpeg$ rm appveyor.yml AUTHORS.md CHANGELOG.md cmake/ CMakeLists.txt CTestConfig.cmake doc/ INSTALL.md LICENSE NEWS.md README.md scripts/ tests/ THANKS.md thirdparty/ tools/ wrapping/ -rf 339 | ea@ubuntu:~/openjpeg/covnavi/openjpeg$ ls 340 | src 341 | ea@ubuntu:~/openjpeg/covnavi/openjpeg$ 342 | ``` 343 | 344 | This essentially leaves us with just `src` dir. 345 | 346 | To actually import the code into joern db, we'll be mounting it at `/code` inside a docker container , like so: 347 | ``` 348 | sudo docker run -v /home/ea/openjpeg/covnavi/openjpeg/:/code -p 7474:7474 --rm -w /code -it neepl/joern java -jar /joern/bin/joern.jar . 349 | ``` 350 | 351 | The above cmd tells docker to mount our reduced repository at `/code` ,which is where joern in docker expects it, and then run joern indexing on it. 352 | 353 | ``` 354 | ea@ubuntu:~/openjpeg/cov$ sudo docker run -v /home/ea/openjpeg/covnavi/openjpeg/:/code -p 7474:7474 --rm -w /code -it neepl/joern java -jar /joern/bin/joern.jar . 355 | [sudo] password for ea: 356 | Warning: your JVM has a maximum heap size of less than 2 Gig. You may need to import large code bases in batches. 357 | If you have additional memory, you may want to allow your JVM to access it by using the -Xmx flag. 358 | /code/./src/bin/common/color.c 359 | /code/./src/bin/common/color.h 360 | /code/./src/bin/common/format_defs.h 361 | /code/./src/bin/common/opj_getopt.c 362 | /code/./src/bin/common/opj_getopt.h 363 | /code/./src/bin/common/opj_string.h 364 | /code/./src/bin/jp2/convert.c 365 | /code/./src/bin/jp2/convert.h 366 | /code/./src/bin/jp2/convertbmp.c 367 | /code/./src/bin/jp2/convertpng.c 368 | /code/./src/bin/jp2/converttif.c 369 | 370 | ``` 371 | 372 | With this, the initial import is done, we can now start the db and start executing queries. To start the DB, execute: 373 | ``` 374 | sudo docker run -v /home/ea/openjpeg/covnavi/openjpeg/:/code -p 7474:7474 -it neepl/joern /var/lib/neo4j/bin/neo4j console 375 | ``` 376 | You should get something like: 377 | ``` 378 | Starting Neo4j Server console-mode... 379 | Using additional JVM arguments: -server -XX:+DisableExplicitGC -Dorg.neo4j.server.properties=conf/neo4j-server.properties -Djava.util.logging.config.file=conf/logging.properties -Dlog4j.configuration=file:conf/log4j.properties -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled -XX:-OmitStackTraceInFastThrow -XX:hashCode=5 -Dneo4j.ext.udc.source=debian 380 | 2017-08-28 16:51:36.990+0000 INFO [API] Setting startup timeout to: 120000ms based on -1 381 | Detected incorrectly shut down database, performing recovery.. 382 | 2017-08-28 16:51:38.167+0000 INFO [API] Successfully started database 383 | 2017-08-28 16:51:38.218+0000 INFO [API] Starting HTTP on port :7474 with 20 threads available 384 | 2017-08-28 16:51:38.396+0000 INFO [API] Enabling HTTPS on port :7473 385 | 2017-08-28 16:51:38.491+0000 INFO [API] Mounted discovery module at [/] 386 | 2017-08-28 16:51:38.519+0000 INFO [API] Loaded server plugin "GremlinPlugin" 387 | 2017-08-28 16:51:38.519+0000 INFO [API] GraphDatabaseService.execute_script: execute a Gremlin script with 'g' set to the Neo4j2Graph and 'results' containing the results. Only results of one object type is supported. 388 | 2017-08-28 16:51:38.520+0000 INFO [API] Mounted REST API at [/db/data] 389 | 2017-08-28 16:51:38.522+0000 INFO [API] Mounted management API at [/db/manage] 390 | 2017-08-28 16:51:38.522+0000 INFO [API] Mounted webadmin at [/webadmin] 391 | 2017-08-28 16:51:38.523+0000 INFO [API] Mounted Neo4j Browser at [/browser] 392 | 2017-08-28 16:51:38.566+0000 INFO [API] Mounting static content at [/webadmin] from [webadmin-html] 393 | 2017-08-28 16:51:38.605+0000 INFO [API] Mounting static content at [/browser] from [browser] 394 | 16:51:38.607 [main] WARN o.e.j.server.handler.ContextHandler - o.e.j.s.ServletContextHandler@178270b2{/,null,null} contextPath ends with / 395 | 16:51:38.608 [main] WARN o.e.j.server.handler.ContextHandler - Empty contextPath 396 | 16:51:38.611 [main] INFO org.eclipse.jetty.server.Server - jetty-9.0.5.v20130815 397 | 16:51:38.639 [main] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.s.h.MovedContextHandler@773e2eb5{/,null,AVAILABLE} 398 | 16:51:38.718 [main] INFO o.e.j.w.StandardDescriptorProcessor - NO JSP Support for /webadmin, did not find org.apache.jasper.servlet.JspServlet 399 | 16:51:38.730 [main] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.w.WebAppContext@5a67e962{/webadmin,jar:file:/usr/share/neo4j/system/lib/neo4j-server-2.1.5-static-web.jar!/webadmin-html,AVAILABLE} 400 | 16:51:39.178 [main] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@48eb9836{/db/manage,null,AVAILABLE} 401 | 16:51:39.422 [main] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@a565cbd{/db/data,null,AVAILABLE} 402 | 16:51:39.439 [main] INFO o.e.j.w.StandardDescriptorProcessor - NO JSP Support for /browser, did not find org.apache.jasper.servlet.JspServlet 403 | 16:51:39.442 [main] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.w.WebAppContext@7569ea63{/browser,jar:file:/usr/share/neo4j/system/lib/neo4j-browser-2.1.5.jar!/browser,AVAILABLE} 404 | 16:51:39.530 [main] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@178270b2{/,null,AVAILABLE} 405 | 16:51:39.540 [main] INFO o.e.jetty.server.ServerConnector - Started ServerConnector@2e6f610d{HTTP/1.1}{0.0.0.0:7474} 406 | 16:51:39.626 [main] INFO o.e.jetty.server.ServerConnector - Started ServerConnector@6f3e19b3{SSL-HTTP/1.1}{0.0.0.0:7473} 407 | 2017-08-28 16:51:39.626+0000 INFO [API] Server started on: http://0.0.0.0:7474/ 408 | 2017-08-28 16:51:39.627+0000 INFO [API] Remote interface ready and available at [http://0.0.0.0:7474/] 409 | ``` 410 | Keep this screen open as long as you need the db running. 411 | 412 | 413 | A side note: 414 | To run joern queries manually, you would start another docker screen like so: 415 | 416 | ea@ubuntu:~/openjpeg/cov$ sudo docker ps 417 | [sudo] password for ea: 418 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 419 | 3e8de591c5c1 neepl/joern "/var/lib/neo4j/bin/n" About a minute ago Up About a minute 7473/tcp, 0.0.0.0:7474->7474/tcp goofy_bell 420 | ea@ubuntu:~/openjpeg/cov$ sudo docker exec -it 3e8de591c5c1 bash 421 | ``` 422 | This puts you in a shell inside docker instance where you can run sample queries: 423 | ``` 424 | root@3e8de591c5c1:/joern-tools# echo "queryNodeIndex('type:Function')" | joern-lookup -g 425 | (n30 {location:"74:0:2791:3402",name:"sycc_to_rgb",type:"Function"}) 426 | (n216 {location:"106:0:3405:4639",name:"sycc444_to_rgb",type:"Function"}) 427 | (n626 {location:"157:0:4664:6895",name:"sycc422_to_rgb",type:"Function"}) 428 | (n1335 {location:"245:0:6920:10775",name:"sycc420_to_rgb",type:"Function"}) 429 | (n2520 {location:"403:0:10800:11998",name:"color_sycc_to_rgb",type:"Function"}) 430 | (n2787 {location:"457:0:12609:23525",name:"color_apply_icc_profile",type:"Function"}) 431 | (n4927 {location:"812:0:23559:23866",name:"are_comps_same_dimensions",type:"Function"}) 432 | (n5008 {location:"824:0:23869:28334",name:"color_cielab_to_rgb",type:"Function"}) 433 | (n6072 {location:"986:0:28417:30383",name:"color_cmyk_to_rgb",type:"Function"}) 434 | (n6655 {location:"1052:0:30484:32499",name:"color_esycc_to_rgb",type:"Function"}) 435 | (n7200 {location:"57:0:2649:2728",name:"opj_reset_options_reading",type:"Function"}) 436 | (n7260 {location:"40:0:1845:2058",name:"opj_strnlen_s",type:"Function"}) 437 | (n7315 {location:"53:0:2193:2612",name:"opj_strcpy_s",type:"Function"}) 438 | (n7414 {location:"54:0:2264:2376",name:"int_floorlog2",type:"Function"}) 439 | (n7449 {location:"64:0:2413:3311",name:"clip_component",type:"Function"}) 440 | ... 441 | ``` 442 | 443 | Now that the database is running, we can run `covnavi` to combine CFG with coverage info to get interesting conditional statements. Back in the `cov` dir, run `covnavi.py` like so: 444 | ``` 445 | python covnavi.py createdb openjpeg_coverage.db openjpeg.json 446 | ``` 447 | File `openjpeg_coverage.db` is generated by `gather_coverage.sh`, and `openjpeg.json` is a JSON database containing all interesting IF and SWITCH statements as well as their branchs and code... 448 | 449 | Running the tool looks like: 450 | 451 | ``` 452 | ea@ubuntu:~/openjpeg/cov$ python covnavi.py createdb openjpeg_coverage.db openjpeg.json 453 | Total number of IfStatements:7084 454 | Total number of SwitchStatement:163 455 | Processing conditional 7230 out of 7247 total. 456 | Total of 551 not fully covered. 457 | Done! 458 | ea@ubuntu:~/openjpeg/cov$ 459 | ``` 460 | 461 | That's it! We are done witn processing. The resulting `openjpeg.json` file can be used for further analysis. 462 | 463 | Analysis phase: 464 | 465 | Covnavi has another comand: `show` which goes through saved conditionals one by one and displays important info about them. 466 | Also, if you have sublime set up, it opens the corresponding file and jumps to the relevant line. You can use this information to navigate aroud the coverage information, find places where fuzzer indeed got stuck due to some checksum not being right, or some magic value and so on... Like the following example: 467 | 468 | ``` 469 | Conditional(3923): 470 | code: if ( p_colr_header_size == 35 ) 471 | node id: 282296 472 | location: ../src/lib/openjp2/jp2.c +1531 473 | branches: 2 474 | True branch: 475 | code: opj_read_bytes ( p_colr_header_data , & rl , 4 ) 476 | node id: 282430 477 | location: ./src/lib/openjp2/jp2.c +1532 478 | Is covered: False 479 | False branch: 480 | code: p_colr_header_size != 7 481 | node id: 282313 482 | location: ./src/lib/openjp2/jp2.c +1548 483 | Is covered: True 484 | ``` 485 | 486 | We can look this code up and see why `p_colr_header_size` wasn't ever 35, then we can synthetize a sample which would be true in this case. Note that for this example I've ran coverage on a rather small set of testcases, more extensive corpus would probably cover more. 487 | 488 | After we are done with analysis and have found a place in the code which we need to modify or have somehow augmented our fuzzer to get past the condition, we can resume fuzzing. Rerunning the coverage analysis later would reveal if the improvement was succesfull. 489 | 490 | 491 | -------------------------------------------------------------------------------- /covnavi.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import os 3 | import sys 4 | import json 5 | from termcolor import colored 6 | from optparse import OptionParser 7 | import signal 8 | import shutil 9 | 10 | DIR_PREFIX_LEN = len("/a") 11 | conn = None 12 | j = None 13 | 14 | 15 | def lookup_coverage(filename,line): 16 | """given a file and linu number, see how many times it was executed""" 17 | cur = conn.cursor() 18 | query = "SELECT num_executions from line_coverage where filename = '%s' and line = '%d' " 19 | cur.execute(query%(filename.replace("/","#") + ".gcov",line)) 20 | r = cur.fetchall() 21 | if r: 22 | return r[0][0] 23 | else: 24 | return 0 25 | 26 | def tosimplelocation(joern_location): 27 | """transform joern's filename/line/column representation to more usuall filename+line_number""" 28 | return joern_location[DIR_PREFIX_LEN:].split(":")[0].split("+") 29 | 30 | def get_branches(id): 31 | """ given an joern node id of a conditional, get information on its branches""" 32 | 33 | branch_nodes_query = "g.v(%d).out()" \ 34 | ".filter{it.isCFGNode == 'True' && it.type == 'Condition'}" \ 35 | ".outE('FLOWS_TO').inV" 36 | branch_nodes = j.runGremlinQuery(branch_nodes_query%id) 37 | branches = [] 38 | for b in branch_nodes: 39 | branch = {} 40 | branch["parent_id"] = id 41 | branch["id"] = int(b.ref.split("/")[-1]) 42 | location_query = "g.v(%d).functions().functionToFile().filepath" 43 | branch["filename"] = tosimplelocation(j.runGremlinQuery(location_query%branch["id"])[0])[0] 44 | 45 | #gcov ignores labels, so we have to dig deeper a bit if that's the case 46 | tmp_b = b 47 | tmp_id = branch["id"] 48 | while tmp_b.properties["type"] == "Label": 49 | tmp_b = j.runGremlinQuery("g.v(%d).outE('FLOWS_TO').inV"%tmp_id)[0] 50 | tmp_id = int(tmp_b.ref.split("/")[-1]) 51 | b = tmp_b 52 | if not b.properties["location"] : 53 | #"Warning: branch at end of function has no location, take condition location instead" 54 | location_query = "g.v(%d).out()" \ 55 | ".filter{it.isCFGNode == 'True' && it.type == 'Condition'}" \ 56 | ".location" 57 | location = j.runGremlinQuery(location_query%id) 58 | branch["line"] = int(location[0].split(":")[0]) 59 | else: 60 | branch["line"] = int(b.properties["location"].split(":")[0]) 61 | branch["code"] = b.properties["code"] 62 | cfg_label_query = "g.v(%d).out()" \ 63 | ".filter{it.isCFGNode == 'True' && it.type == 'Condition'}" \ 64 | ".outE('FLOWS_TO').sideEffect{label = it.flowLabel}" \ 65 | ".inV.filter{it.id == %d}.transform{label}" 66 | branch["cfg_label"] = j.runGremlinQuery(cfg_label_query%(id,branch["id"]))[0] 67 | branch["num_executions"] = lookup_coverage(branch["filename"], branch["line"]) 68 | branch["is_covered"] = True if branch["num_executions"] != 0 else False 69 | branches.append(branch) 70 | #sort by lines 71 | branches.sort(key = lambda b: b["line"]) 72 | return branches 73 | 74 | 75 | def get_conditional_info(id,idx): 76 | """ heart of the whole thing, analyze get all necessary information about the 77 | conditional statement including branches and coverage information""" 78 | if_node = j.runGremlinQuery("g.v(%d)"%id) 79 | conditional = {} 80 | try: 81 | conditional["id"] = id 82 | conditional["code"] = if_node["code"] 83 | location_query = "g.v(%d).out()" \ 84 | ".filter{it.type == 'Condition'}" \ 85 | ".sideEffect{loc = it.location}" \ 86 | ".functions()" \ 87 | ".functionToFile()" \ 88 | ".sideEffect{filename = it.filepath}" \ 89 | ".transform{filename+'+'+loc}" 90 | location = tosimplelocation(j.runGremlinQuery(location_query%id)[0]) 91 | conditional["filename"] = "."+location[0] 92 | conditional["line"] = int(location[1]) 93 | conditional["branches"] = get_branches(id) 94 | conditional["importance"] = "show" 95 | 96 | except: 97 | print sys.exc_info() 98 | return {} 99 | bids = set([b["id"] for b in conditional["branches"]]) 100 | if len(bids) <= 1: 101 | #this means both branches of if end up at same code (for example: empty if body) 102 | #just flip one doesn't matter coverage-wise 103 | conditional["branches"][0]["cfg_label"] = "False" if conditional["branches"][0]["cfg_label"] == "True" else "True" 104 | conditional["index"] = idx 105 | branch_true = None 106 | branch_false = None 107 | for b in conditional["branches"]: 108 | if b["cfg_label"] == "True": 109 | branch_true = b 110 | break 111 | for b in conditional["branches"]: 112 | if b["cfg_label"] == "False": 113 | branch_false = b 114 | break 115 | conditional["branch_true"] = branch_true 116 | conditional["branch_false"] = branch_false 117 | return conditional 118 | 119 | def print_branch(branch): 120 | print "\t\t\tcode:\t",colored("%.50s"%branch["code"],"blue") 121 | print "\t\t\tnode id:\t",branch["id"] 122 | print "\t\t\tlocation: .%s +%d\t"%(branch["filename"],branch["line"]) 123 | print "\t\t\tIs covered:",colored(branch["is_covered"],"cyan" if branch["is_covered"] else "red"), " (%d)"%branch["num_executions"] 124 | 125 | def print_conditional(conditional,highlight=False): 126 | if "importance" in conditional and conditional["importance"] == "highlight": 127 | print colored("Conditional(%s):"%conditional["index"],"red","on_green") 128 | else: 129 | print "Conditional(%s):"%conditional["index"] 130 | print "\tcode:\t",colored("%.80s"%conditional["code"],"blue") 131 | print "\tnode id:\t ",conditional["id"] 132 | print "\tlocation:\t .%s +%d"%(conditional["filename"],conditional["line"]) 133 | print "\tbranches:\t",len(conditional["branches"]) 134 | if conditional["branch_true"] != None and conditional["branch_false"] != None: 135 | print "\t\tTrue branch:" 136 | print_branch(conditional["branch_true"]) 137 | print "\t\tFalse branch:" 138 | print_branch(conditional["branch_false"]) 139 | else: # we are dealing with a switch statement 140 | for b in conditional["branches"]: 141 | print_branch(b) 142 | 143 | 144 | 145 | def createdb(coverage_db,json_dbname,joern_url='http://localhost:7474/db/data/'): 146 | """ combine coverage information with joern queries and create json db with results""" 147 | global j,conn 148 | from joern.all import JoernSteps 149 | j = JoernSteps() 150 | j.setGraphDbURL(joern_url) 151 | j.connectToDatabase() 152 | conditionals = {} # filename is key 153 | if_ids = j.runGremlinQuery('queryNodeIndex("type:IfStatement").id') 154 | print "Total number of IfStatements:%d"%len(if_ids) 155 | 156 | switch_ids = j.runGremlinQuery('queryNodeIndex("type:SwitchStatement").id') 157 | print "Total number of SwitchStatement:%d"%len(switch_ids) 158 | if_ids += switch_ids 159 | 160 | conn = sqlite3.connect(coverage_db) 161 | cur = conn.cursor() 162 | idx = 0 163 | 164 | for id in if_ids: # iterate over each conditional and gather branch info 165 | conditional = get_conditional_info(id,idx) 166 | if conditional == {}: 167 | continue 168 | idx+=1 169 | sys.stdout.write("Processing conditional %d out of %d total.\r"%(idx,len(if_ids))) 170 | sys.stdout.flush() 171 | if conditional["filename"] not in conditionals: #group by file name 172 | conditionals[conditional["filename"]] = [] 173 | conditionals[conditional["filename"]].append(conditional) 174 | #now sort them by filenames and line numbers 175 | sorted_conditionals = [] 176 | for filename in conditionals: 177 | conditionals[filename].sort(key = lambda c: c["line"]) 178 | sorted_conditionals += conditionals[filename] 179 | #save as json 180 | json.dump(sorted_conditionals,open(json_dbname,"wb")) 181 | print "\nDone!" 182 | 183 | def is_of_interest(conditional,options): 184 | """filter which conditional statements are interesting to us based on threshold 185 | or a probability of each branch""" 186 | if options.hlighted_only: 187 | if not conditional.has_key("importance") or conditional["importance"] != "highlight": 188 | return False 189 | if conditional.has_key("importance"): 190 | if conditional["importance"] == "ignore": 191 | return False 192 | if conditional["importance"] == "highlight": 193 | return True 194 | if options.filter_file: 195 | if options.filter_file not in conditional["filename"]: 196 | return False 197 | if conditional["index"] < options.start_index: 198 | return False 199 | total_executions = float(sum(b["num_executions"] for b in conditional["branches"])) 200 | if total_executions == 0: # not reached at all 201 | return False 202 | branch_probabilities = [b["num_executions"] / total_executions for b in conditional["branches"]] 203 | #is there a branch whose number of executions is below the threshold ? 204 | for prob in branch_probabilities: 205 | if prob <= (1 - options.threshold): 206 | return True 207 | return False 208 | 209 | def signal_handler(signal, frame): 210 | print "\nSaving and exiting\n" 211 | 212 | sys.exit(0) 213 | 214 | def show(options): 215 | """ go through json database and pretty print the results one by one""" 216 | conditionals = None 217 | last_file = "" 218 | 219 | # signal.signal(signal.SIGINT, signal_handler) 220 | with open(options.dbname,"rb") as f: 221 | conditionals = json.load(f) 222 | print "Total conditionals: %d"%len(conditionals) 223 | for conditional in conditionals: 224 | try: 225 | if not is_of_interest(conditional,options): 226 | continue 227 | print_conditional(conditional) 228 | if not last_file == "": 229 | os.system("subl -b --command close_file "+last_file) 230 | os.system("subl -b --command open_file "+os.path.join(options.code_root,conditional["filename"])+":"+str(conditional["line"])) 231 | last_file = os.path.join(options.code_root,conditional["filename"]) 232 | c = raw_input("[i]gnore [h]ighlight - any key for next: ") 233 | if "i" in c: 234 | conditional["importance"] = "ignore" 235 | if "h" in c: 236 | conditional["importance"] = "highlight" 237 | os.system("clear") 238 | except KeyboardInterrupt: # catch ctrl+c , if we made any changes, we want to save them 239 | break 240 | print "\nSaving and exiting\n" 241 | shutil.copyfile(options.dbname,options.dbname+"~") 242 | json.dump(conditionals,open(options.dbname,"wb")) 243 | sys.exit(0) 244 | 245 | 246 | def usage(): 247 | print "Create database: covnavi createdb coverage_db output_dbname.json " 248 | print "Show results: covnavi show -h " 249 | sys.exit(0) 250 | 251 | def main(): 252 | if len(sys.argv) < 2: 253 | usage() 254 | if sys.argv[1] == "createdb": 255 | if len(sys.argv) < 4: 256 | usage() 257 | coverage_db = sys.argv[2] 258 | if len(sys.argv) == 5: 259 | createdb(coverage_db,sys.argv[3],sys.argv[4]) 260 | else: 261 | createdb(coverage_db,sys.argv[3]) 262 | elif sys.argv[1] == "show": 263 | parser = OptionParser("usage: %prog show [options]") 264 | parser.add_option("-j", "--json", dest="dbname",help="path to json coverage db",metavar="FILE") 265 | parser.add_option("-c", "--coderoot", dest="code_root",help="path to code root") 266 | parser.add_option("-i", "--startidx", dest="start_index",help="index to start from",default=0) 267 | parser.add_option("-f", "--filterfilename", dest="filter_file",help="only show results from filenames containing keyword") 268 | parser.add_option("-t", "--threshold", dest="threshold",help="branch execution treshhold - show all branches with higher bias (0.0 - 1.0)",default=1.0,type="float") 269 | parser.add_option("-l", "--highlighted", dest="hlighted_only",help="show only highlighted conditionals",action="store_true") 270 | (options,args) = parser.parse_args(sys.argv[2:]) 271 | if not options.dbname: 272 | parser.error("dbname is required") 273 | if not options.code_root: 274 | parser.error("code root is required") 275 | if options.threshold: 276 | if options.threshold > 1 or options.threshold < 0: 277 | parser.error("threshold must be between 0.0 and 1.0") 278 | show(options) 279 | else: 280 | usage() 281 | 282 | if __name__ == '__main__': 283 | main() 284 | 285 | -------------------------------------------------------------------------------- /gather_coverage.sh: -------------------------------------------------------------------------------- 1 | 2 | SOURCE_ROOT=$1 3 | SOURCE_ROOT_ABS=`realpath $SOURCE_ROOT` 4 | PATH_DIFF=$PWD 5 | echo "Running lcov..." 6 | lcov --rc lcov_branch_coverage=1 --no-checksum --capture --directory $SOURCE_ROOT_ABS --output-file coverage.in --quiet 7 | echo "Generating html coverage report..." 8 | genhtml coverage.in --output-directory ./web --quiet 9 | echo "Processing per-line coverage info..." 10 | mkdir tmp 11 | cd tmp 12 | echo "\t-gathering files" 13 | for i in `find $SOURCE_ROOT_ABS -name \*.o` 14 | do 15 | gcov -p $i > /dev/null 2>&1 16 | done 17 | #remove prefix from abs paths 18 | PATH_DIFF_SHARPS=`echo $SOURCE_ROOT_ABS| tr "/" "#"` 19 | echo "\t-removing prefixes" 20 | for i in `ls` 21 | do 22 | mv $i ${i#$PATH_DIFF_SHARPS} 2>/dev/null 23 | done 24 | echo "\t-processing lines" 25 | for i in `ls`; do cat $i | tr ":" " "|awk -v i=$i '{print i "\t" $2 "\t" $1}'; done >> ../line_coverage.txt 26 | cd .. 27 | echo "Importing into database..." 28 | python ./import_coverage_info.py $SOURCE_ROOT_ABS 29 | rm tmp -rf 30 | rm line_coverage.txt 31 | -------------------------------------------------------------------------------- /import_coverage_info.py: -------------------------------------------------------------------------------- 1 | 2 | import sqlite3 3 | import sys 4 | source_root = sys.argv[1] 5 | conn = sqlite3.connect(source_root+"_coverage.db") 6 | 7 | 8 | conn.execute(''' CREATE TABLE line_coverage (filename,line, num_executions)''') 9 | 10 | conn.commit() 11 | 12 | with open("line_coverage.txt","r") as f: 13 | for line in f.readlines(): 14 | filename, line,coverage = line.strip().split("\t") 15 | num_executions = 0 16 | if not coverage == "#####" and not coverage == "-": 17 | num_executions = int(coverage) 18 | conn.execute("INSERT INTO line_coverage VALUES (?,?,?)",(filename,line,num_executions)) 19 | conn.commit() 20 | conn.close() 21 | --------------------------------------------------------------------------------