-
Notifications
You must be signed in to change notification settings - Fork 69
Examples
This wiki page contains a collection of useful example scripts to help you minimize with halfempty.
- Minimizing Timeouts / Hangs
- Minimizing Input to a Graphical Application
- Minimizing Output of a Graphical Application
- Minimizing Memory Leaks
- Controlling Resource Limits
- Interacting with a Debugger
- Safely Cleaning Up Temporary Files
- Understanding Return Codes
You have found a timeout or hang in your application, and want halfempty to find you the minimal version that still hangs.
To achieve this, you can ask halfempty to send you a SIGALRM
using the --timeout
parameter, and then handle it in your script.
#!/bin/sh
tempfile=`mktemp` && cat > ${tempfile}
trap 'rm -f ${tempfile}; exit ${result:=1}' EXIT TERM
trap 'rm -f ${tempfile}; exit ${result:=0}' ALRM
yourprogram --yourargs ${tempfile}
Now the following command will find the simplest testcase that takes at least 10 seconds to run:
$ halfempty --timeout 10 ./timeout.sh testcase
You have a graphical application and a record of user interaction you want to replay, and want to find the minimal amount of input to reach the final state.
You can do this by creating a "golden" image of how you want the application to look, and then asking halfempty to find the minimum amount of input to reach that state. A useful set of tools for this is Xvfb, xdotool and ImageMagick.
-
Xvfb
is a virtual X server, it doesn't actually display any output but emulates a display for applications. -
xdotool
can automate sending input, keystrokes, mouse clicks, and so on. -
ImageMagick
can automate taking screenshots and comparing images.
Let's look at an example. Imagine we have a large amount of input to gnome-calculator
from testing or fuzzing that causes some error state we would like to examine. This would be much easier to debug if we have the minimum amount of input necessary to reproduce the state.
First, replay the input in Xvfb
and take a screenshot of how it looks.
For this example, I randomly generated some input like this:
$ tr -dc '0-9()%+/*.\r\033-' < /dev/urandom | head -c 1024 > input.txt
Now start Xvfb
, and replay the input using xdotool
:
$ Xvfb :1 -screen 0 640x480x24 &
$ DISPLAY=:1 gnome-calculator &
$ DISPLAY=:1 xdotool search --sync --onlyvisible --name "Calculator" windowmove 0 0 windowsize 100% 100% getwindowgeometry
Window 2097159
Position: 0,0 (screen: 0)
Geometry: 640x480
$ DISPLAY=:1 xdotool type --delay 100 --file input.txt
$ DISPLAY=:1 import -window 2097159 png:golden.png
This might take a few minutes, but halfempty can run many jobs in parallel.
You can verify the screenshot looks correct by looking at golden.png
.
If that looks correct, you can minimize the input using a script like this:
#!/bin/bash
# create a fifo for communication with Xserver
fifoname=$(mktemp -u) && mkfifo ${fifoname}
# cleanup on timeout or error
trap 'rm -f ${fifoname}; kill %Xvfb; exit ${result:=1}' TERM ALRM EXIT
# start a virtual X server
Xvfb -nolisten tcp -displayfd 1 -screen 0 640x480x24 :$$ > ${fifoname} &
# wait for Xvfb to say it's ready for a client
read display < ${fifoname} && export DISPLAY=:${display}
# start application
gnome-calculator &
# wait for calculator to appear and position at 0 0
eval $(xdotool search --sync --onlyvisible --name "Calculator" \
windowmove --sync 0 0 \
windowsize --sync 100% 100% \
getwindowgeometry --shell)
# give it a second to initialize
sleep 1
# send it the specified input
xdotool type --delay 100 --file -
# give it a second to finish processing
sleep 1
# compare the window to golden image
result=$(import -windowid $WINDOW png:- \
| compare -metric AE png:- png:golden.png /dev/null 2>&1)
# send exit command
xdotool key ctrl+q
# wait for it to finish
wait %gnome-calculator
Now use this command to find the minimal input required to make gnome-calculator
look the way it does in the golden image.
$ halfempty ./sendinput.sh input.txt
╭│ │ ── halfempty ───────────────────────────────────────────────── v0.30 ──
╰│ 2│ A fast, parallel testcase minimization tool
╰───╯ ───────────────────────────────────────────────────────── by @taviso ──
Input file "input.txt" is now 1024 bytes, starting strategy "bisect"...
Verifying the original input executes successfully... (skip with --noverify)
The original input file succeeded after 57.0 seconds.
New finalized size: 1024 (depth=2) real=0.0s, user=0.0s, speedup=~-0.0s
New finalized size: 768 (depth=6) real=43.1s, user=105.4s, speedup=~62.3s
New finalized size: 640 (depth=10) real=109.2s, user=230.8s, speedup=~121.5s
New finalized size: 576 (depth=16) real=172.2s, user=412.7s, speedup=~240.5s
New finalized size: 512 (depth=18) real=231.6s, user=472.0s, speedup=~240.4s
New finalized size: 448 (depth=21) real=287.7s, user=551.0s, speedup=~263.3s
New finalized size: 416 (depth=27) real=344.3s, user=703.8s, speedup=~359.6s
New finalized size: 384 (depth=37) real=441.8s, user=943.4s, speedup=~501.5s
New finalized size: 352 (depth=38) real=463.1s, user=964.6s, speedup=~501.5s
New finalized size: 336 (depth=40) real=486.1s, user=1004.8s, speedup=~518.7s
New finalized size: 320 (depth=42) real=525.4s, user=1044.0s, speedup=~518.6s
...
Reached the end of our path through tree, all nodes were finalized
597 nodes failed, 130 worked, 18 discarded, 1 collapsed
7026.639 seconds of compute was required for final path
Strategy "bisect" complete, output 85 bytes
All work complete, generating output halfempty.out (size: 85)
Halfempty reduced the testcase to just 85 inputs, much more manageable to debug.
Let's verify it worked by taking a new screenshot and comparing them:
$ Xvfb :1 -screen 0 640x480x24 &
$ DISPLAY=:1 gnome-calculator &
$ DISPLAY=:1 xdotool search --sync --onlyvisible --name "Calculator" windowmove 0 0 windowsize 100% 100% getwindowgeometry
Window 2097159
Position: 0,0 (screen: 0)
Geometry: 640x480
$ DISPLAY=:1 xdotool type --delay 100 --file halfempty.out
$ DISPLAY=:1 import -window 2097159 png:output.png
Looks Identical! You can use Xnest
instead of Xvfb
if you want to see and interact with the application, note that some of the parameters are different (For example, Xnest
uses -geometry
instead of -screen
).
Using the Xvfb method as above, we could minimize the amount of html required to produce the same output in a browser.
First, I saved a website to an html file, and took a screenshot of it when rendered:
$ Xvfb :1 -screen 0 1024x768x24 &
$ DISPLAY=:1 firefox --no-remote ./Hacker\ News.html &
$ DISPLAY=:1 xdotool search --sync --onlyvisible --name "Mozilla Firefox" windowmove 0 0 windowsize 100% 100% getwindowgeometry
Window 4194320
Position: 0,0 (screen: 0)
Geometry: 1024x768
$ DISPLAY=:1 import -window 4194320 png:golden.png
Note that firefox actually has a
--screenshot
option, but this example can be applied to other applications.
Using LSAN, Valgrind or other tools it's possible to minimize an input that causes memory leaks or other errors in your program.
For this example, I've used the following simple C program that leaks memory if the input contains the string "leaky".
$ cat leaky.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char *lineptr = NULL;
size_t size;
while (getline(&lineptr, &size, stdin) > 0) {
if (strstr(lineptr, "leaky"))
return 0;
}
free(lineptr);
return 0;
}
A minimal example using valgrind.
#!/bin/sh
# In this example, the test application reads stdin, see the halfempty wiki if
# you need an example of creating temporary files.
#
# choose an exitcode your application wont use, like 42 in this example
valgrind --error-exitcode=42 \
--quiet \
--leak-check=full \
leaky
if test $? -eq 42; then
exit 0 # leak occurred, keep this input
else
exit 1 # no leak, discard this input
fi
Use this command to find the leak.
$ halfempty ./leakcheck.sh /usr/share/dict/words
╭│ │ ── halfempty ───────────────────────────────────────────────── v0.30 ──
╰│ 2│ A fast, parallel testcase minimization tool
╰───╯ ───────────────────────────────────────────────────────── by @taviso ──
Input file "/usr/share/dict/words" is now 4953680 bytes, starting strategy "bisect"...
Verifying the original input executes successfully... (skip with --noverify)
The original input file succeeded after 0.3 seconds.
New finalized size: 4953680 (depth=2) real=0.0s, user=0.0s, speedup=~-0.0s
New finalized size: 2476840 (depth=4) real=0.9s, user=1.5s, speedup=~0.6s
New finalized size: 1238420 (depth=6) real=1.8s, user=2.4s, speedup=~0.6s
New finalized size: 619210 (depth=8) real=2.8s, user=3.4s, speedup=~0.6s
New finalized size: 309605 (depth=11) real=3.5s, user=4.9s, speedup=~1.4s
New finalized size: 154803 (depth=12) real=4.0s, user=5.4s, speedup=~1.4s
New finalized size: 77402 (depth=14) real=4.8s, user=6.4s, speedup=~1.6s
New finalized size: 38702 (depth=17) real=5.9s, user=7.8s, speedup=~1.9s
New finalized size: 19352 (depth=18) real=6.3s, user=8.3s, speedup=~2.0s
New finalized size: 9677 (depth=20) real=6.9s, user=9.5s, speedup=~2.6s
New finalized size: 4840 (depth=23) real=7.7s, user=10.8s, speedup=~3.1s
New finalized size: 2422 (depth=25) real=8.6s, user=11.8s, speedup=~3.2s
New finalized size: 1213 (depth=27) real=9.7s, user=12.6s, speedup=~2.9s
New finalized size: 609 (depth=28) real=10.1s, user=13.1s, speedup=~2.9s
New finalized size: 307 (depth=31) real=10.8s, user=14.2s, speedup=~3.5s
New finalized size: 156 (depth=33) real=11.6s, user=15.3s, speedup=~3.7s
New finalized size: 81 (depth=34) real=12.2s, user=15.9s, speedup=~3.7s
New finalized size: 44 (depth=36) real=13.0s, user=16.5s, speedup=~3.5s
New finalized size: 26 (depth=39) real=14.2s, user=17.8s, speedup=~3.6s
New finalized size: 17 (depth=41) real=14.8s, user=18.8s, speedup=~3.9s
New finalized size: 13 (depth=44) real=15.5s, user=20.2s, speedup=~4.7s
New finalized size: 9 (depth=45) real=15.7s, user=20.5s, speedup=~4.8s
New finalized size: 7 (depth=49) real=17.2s, user=22.7s, speedup=~5.5s
New finalized size: 6 (depth=55) real=18.9s, user=25.2s, speedup=~6.3s
Reached the end of our path through tree, all nodes were finalized
87 nodes failed, 54 worked, 26 discarded, 1 collapsed
56.959 seconds of compute was required for final path
Strategy "bisect" complete, output 6 bytes
All work complete, generating output halfempty.out (size: 6)
$ cat halfempty.out
leaky
$
By default, LeakSanitizer uses exit code 23 when a leak is detected, so a minimal example would be
#!/bin/sh
# In this example, the test application reads stdin, see the halfempty wiki if
# you need an example of creating temporary files.
leaky
if test $? -eq 23; then
exit 0 # leak occurred, keep this input
else
exit 1 # no leak, discard this input
fi
You're trying to minimize a testcase in an application that can consume significant resources when given malformed input, such as allocating lots of memory or creating large files.
Halfempty can limit the resources available to the application so that is harmlessly terminated. The full list of resources you can control is described in the setrlimit
manual, but let's examine some common scenarios.
You want to minimize a crash testcase, but the application is badly behaved and might allocate significant memory if given incorrect input.
Here is an example written in C, every letter 'M' in the input causes a 64k memory allocation!
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char *lineptr = NULL;
size_t size;
while (getline(&lineptr, &size, stdin) > 0) {
if (strchr(lineptr, 'm')) {
if (malloc(64 * 1024) == NULL) {
fprintf(stderr, "memory allocation failure\n");
return 1;
}
}
if (strstr(lineptr, "crash"))
*(char *)(0) = 'C';
}
return 0;
}
Let's try it with a 1GB virtual memory size limit, here's the test script:
#!/bin/sh
./eatmemory
if test $? -eq 139; then
exit 0 # Segmentation Fault, keep this input
else
exit 1 # discard this input
fi
And we run it with an address space limit of 0x40000000
(That's 1GB, 1 * 1024 * 1024 * 1024
bytes)
$ halfempty --limit=RLIMIT_AS=0x40000000 ./testcase.sh /usr/share/dict/words
╭│ │ ── halfempty ───────────────────────────────────────────────── v0.30 ──
╰│ 2│ A fast, parallel testcase minimization tool
╰───╯ ───────────────────────────────────────────────────────── by @taviso ──
Input file "/usr/share/dict/words" is now 4953680 bytes, starting strategy "bisect"...
Verifying the original input executes successfully... (skip with --noverify)
** Message: 15:29:41.591: This program expected `./testcase.sh` to return successfully
** Message: 15:29:41.591: for the original input (i.e. exitcode zero).
** Message: 15:29:41.591: Try it yourself to verify it's working.
** Message: 15:29:41.591: Use a command like: `cat /usr/share/dict/words | ./testcase.sh || echo failed`
Oops, I guess we were too aggressive, let's try 2GB instead:
$ halfempty --limit=RLIMIT_AS=0x80000000 ./testcase.sh /usr/share/dict/words
╭│ │ ── halfempty ───────────────────────────────────────────────── v0.30 ──
╰│ 2│ A fast, parallel testcase minimization tool
╰───╯ ───────────────────────────────────────────────────────── by @taviso ──
Input file "/usr/share/dict/words" is now 4953680 bytes, starting strategy "bisect"...
Verifying the original input executes successfully... (skip with --noverify)
The original input file succeeded after 2.3 seconds.
New finalized size: 4953680 (depth=2) real=0.0s, user=0.0s, speedup=~-0.0s
New finalized size: 2476840 (depth=4) real=0.2s, user=0.2s, speedup=~0.0s
New finalized size: 1238420 (depth=7) real=0.4s, user=0.5s, speedup=~0.1s
New finalized size: 619210 (depth=8) real=0.5s, user=0.6s, speedup=~0.1s
New finalized size: 309605 (depth=11) real=0.6s, user=0.7s, speedup=~0.1s
New finalized size: 154803 (depth=13) real=0.6s, user=0.8s, speedup=~0.1s
New finalized size: 77402 (depth=14) real=0.7s, user=0.8s, speedup=~0.1s
New finalized size: 38702 (depth=16) real=0.7s, user=0.9s, speedup=~0.1s
New finalized size: 19352 (depth=19) real=0.8s, user=0.9s, speedup=~0.1s
New finalized size: 9677 (depth=21) real=0.9s, user=1.0s, speedup=~0.1s
New finalized size: 4840 (depth=23) real=1.0s, user=1.1s, speedup=~0.1s
New finalized size: 2422 (depth=25) real=1.1s, user=1.2s, speedup=~0.1s
New finalized size: 1213 (depth=27) real=1.2s, user=1.3s, speedup=~0.1s
New finalized size: 609 (depth=28) real=1.3s, user=1.3s, speedup=~0.1s
New finalized size: 307 (depth=31) real=1.4s, user=1.5s, speedup=~0.1s
New finalized size: 156 (depth=32) real=1.4s, user=1.5s, speedup=~0.1s
New finalized size: 81 (depth=35) real=1.5s, user=1.7s, speedup=~0.1s
New finalized size: 44 (depth=36) real=1.6s, user=1.7s, speedup=~0.1s
New finalized size: 26 (depth=38) real=1.7s, user=1.8s, speedup=~0.1s
New finalized size: 22 (depth=42) real=1.7s, user=1.9s, speedup=~0.1s
New finalized size: 18 (depth=45) real=1.8s, user=1.9s, speedup=~0.1s
New finalized size: 14 (depth=46) real=1.8s, user=2.0s, speedup=~0.1s
New finalized size: 10 (depth=47) real=1.9s, user=2.0s, speedup=~0.1s
New finalized size: 8 (depth=48) real=1.9s, user=2.0s, speedup=~0.1s
New finalized size: 6 (depth=52) real=2.0s, user=2.1s, speedup=~0.1s
New finalized size: 5 (depth=53) real=2.0s, user=2.1s, speedup=~0.1s
Reached the end of our path through tree, all nodes were finalized
117 nodes failed, 66 worked, 51 discarded, 1 collapsed
8.263 seconds of compute was required for final path
Strategy "bisect" complete, output 5 bytes
Input file "/usr/share/dict/words" is now 5 bytes, starting strategy "zero"...
Verifying the original input executes successfully... (skip with --noverify)
The original input file succeeded after 0.0 seconds.
New finalized size: 5 (depth=2) real=0.0s, user=0.0s, speedup=~-0.0s
Reached the end of our path through tree, all nodes were finalized
8 nodes failed, 1 worked, 0 discarded, 1 collapsed
0.083 seconds of compute was required for final path
Strategy "zero" complete, output 5 bytes
All work complete, generating output halfempty.out (size: 5)
$ cat halfempty.out
crash
It works perfectly!
Note: When an application reaches the
RLIMIT_AS
limit, allocations will start to fail, no signal is sent.
You're trying to minimize a crash, but some inputs can cause your program to enter an infinite loop. One way to handle this would be using --timeout
$ halfempty --timeout 30 ./yourscript.sh yourinput
This sends a SIGALRM
after 30 seconds, and your application can clean up. However, if your application is very badly behaved, it might catch SIGALRM
and try to continue. You can enforce a hard limit that cannot be caught using RLIMIT_CPU
, once the limit is reached your application will be sent a SIGKILL
.
$ halfempty --limit=RLIMIT_CPU=30 ./yourscript.sh yourinput
Note: If you need to create temporary files, please read this FAQ question, you can't trap a
SIGKILL
and clean up so you need to be careful!
You need to prevent an application from creating huge files on error.
You can use RLIMIT_FSIZE
to limit the size of files an application can create.
If your application doesn't handle any signals, then by default signals that cause termination will cause your exit code to be 128 + signo
. Here is a table of the most common signals that cause crashes for easy reference.
Signal Name | Signal Number | Exit Code |
---|---|---|
SIGILL | 4 | 132 |
SIGTRAP | 5 | 133 |
SIGABRT | 6 | 134 |
SIGBUS | 7 | 135 |
SIGFPE | 8 | 136 |
SIGSEGV | 11 | 139 |
In your test script, the shell puts the return code of the last application in the variable $?
, so you would do something like this:
#!/bin/sh
yourprogram --yourargs
if test $? -eq 132; then
exit 0 # SIGILL, save this input!
else
exit 1 # Not interested, discard
fi
You can do anything you like in your script, simply exit 0
if the input was interesting (e.g. causes a crash) and exit 1
if it was not interesting.
Halfempty isn't just for minimizing crashes, you can minimize an input that succeeds when it shouldn't, or when output doesn't match the expected output, and so on.
Sometimes your target program might crash with a different crash accidentally found during minimization. One solution might be to use gdb to verify the crash site.
#!/bin/sh
exec gdb -q \
-ex 'r' \
-ex 'q !($_siginfo.si_signo == 11 && $pc == 0x00007ffff763f2e7)' \
-ex 'q 1' \
--args yourprogram --yourparams
This will exit 0 if the signal number and crash address match, or 1 otherwise.
You can test various things such as registers ($rip
, $eax
, etc), fault address ($_siginfo._sifields._sigfault.si_addr
), and many more. If you want to see more things you can test, try the command show conv
in gdb.