Add check-failmalloc.sh to test OOM conditions.
authorDan Fandrich <dan@coneharvesters.com>
Sat, 10 Nov 2018 13:16:47 +0000 (14:16 +0100)
committerDan Fandrich <dan@coneharvesters.com>
Sat, 10 Nov 2018 15:04:53 +0000 (16:04 +0100)
This requires Failmalloc, a library that can be used along with glibc to
cause malloc calls to fail in a defined way. Configure will search for
libfailmalloc.so.0 in the usual places by default, or in a
user-specified location. The tests are skipped if it's not available.
Enable Failmalloc on the Travis coverage build.

.travis.yml
configure.ac
m4m/failmalloc.m4 [new file with mode: 0644]
test/Makefile.am
test/check-failmalloc.sh [new file with mode: 0755]
test/check-vars.sh.in [new file with mode: 0644]

index cd4f25b..9623d1b 100644 (file)
@@ -79,6 +79,9 @@ matrix:
           - libpopt-dev
           - subversion
     compiler: gcc
+    cache:
+      directories:
+        - $HOME/failmalloc
     env:
       - CONFIG=coverage
 
@@ -94,6 +97,17 @@ install:
       ln -s "$TRAVIS_BUILD_DIR" src/libexif
       ./build-config.sh
       autoreconf -sivf
+      cd "$HOME"
+      # Failmalloc for improved test coverage
+      if [ ! -e "$HOME/failmalloc/lib/libfailmalloc.so.0" ] ; then
+        curl -fsSORL https://download.savannah.nongnu.org/releases/failmalloc/failmalloc-1.0.tar.gz
+        tar xaf failmalloc-1.0.tar.gz
+        cd failmalloc-1.0
+        sed -i -e 's/\(__malloc_initialize_hook\)/volatile \1/' failmalloc.c
+        ./configure --prefix="$HOME/failmalloc"
+        make
+        make install
+      fi
       cd "$TRAVIS_BUILD_DIR"
     fi
 
@@ -106,8 +120,8 @@ script:
   - if [ "$CONFIG" = "clang6" ] ; then CFLAGS='-Wall -Wextra -O3'; export CC=clang-6.0; fi
   - if [ "$CONFIG" = "gcc8" ] ; then CFLAGS='-Wall -Wextra -O3'; export export CC=gcc-8; fi
   - if [ "$CONFIG" = "sanitize" ] ; then CFLAGS='-g -Wall -Wextra -fsanitize=address -fsanitize=undefined'; export CC=clang-6.0; fi
-  - if [ "$CONFIG" = "coverage" ] ; then cd "$HOME"/libexif-testsuite; CFLAGS=--coverage; LDFLAGS=--coverage; fi
-  - ./configure --prefix="${HOME}"/install CFLAGS="$CFLAGS" ${LDFLAGS:+LDFLAGS=$LDFLAGS} || { tail -300 config.log; false; }
+  - if [ "$CONFIG" = "coverage" ] ; then cd "$HOME"/libexif-testsuite; CFLAGS=--coverage; CONFIGURE_OPTS="LDFLAGS=--coverage --with-failmalloc=$HOME/failmalloc/lib" ; fi
+  - ./configure --prefix="${HOME}"/install CFLAGS="$CFLAGS" $CONFIGURE_OPTS || { tail -300 config.log; false; }
   - make V=1
   - make V=1 check || { tail -300 test*/test-suite.log; false; }
   - make V=1 install
index aa82192..f39b4f3 100644 (file)
@@ -129,6 +129,9 @@ AC_LINK_IFELSE([AC_LANG_PROGRAM([
        ])
 ])
 
+# Check whether libfailmalloc is available for tests
+CHECK_FAILMALLOC
+
 # doc support
 GP_CHECK_DOC_DIR
 GP_CHECK_DOXYGEN
@@ -204,6 +207,7 @@ AC_CONFIG_FILES([  po/Makefile.in
   libexif.spec
   libexif/Makefile
   test/Makefile
+  test/check-vars.sh
   test/nls/Makefile
   m4m/Makefile
   doc/Makefile
diff --git a/m4m/failmalloc.m4 b/m4m/failmalloc.m4
new file mode 100644 (file)
index 0000000..dff9599
--- /dev/null
@@ -0,0 +1,44 @@
+dnl Search for libfailmalloc to use for testing
+AC_DEFUN([CHECK_FAILMALLOC],[dnl
+  dnl Libtool sets the default library paths
+  AM_PROG_LIBTOOL
+  path_provided=
+  AC_ARG_WITH(failmalloc, [  --with-failmalloc=PATH  use Failmalloc for tests], [
+      if test x"$withval" = "x" -o x"$withval" = "xyes"; then
+        failmalloc_search_path="$sys_lib_search_path_spec"
+      elif test x"$withval" = "xno"; then
+        failmalloc_search_path=""
+      else
+        failmalloc_search_path="$withval"
+        path_provided=1
+      fi
+    ], [failmalloc_search_path="$sys_lib_search_path_spec"]
+  )
+  libfailmalloc_file=libfailmalloc.so.0
+  FAILMALLOC_PATH=
+  dnl Check if the argument is a directory
+  for d in $failmalloc_search_path; do
+      AC_CHECK_FILE([$d/$libfailmalloc_file], [
+        FAILMALLOC_PATH="$d/$libfailmalloc_file"
+        break
+      ], [])
+  done
+  if test -z "$FAILMALLOC_PATH" -a -n "$path_provided"; then
+    dnl Check if the argument is a file
+    AC_CHECK_FILE([$failmalloc_search_path], [FAILMALLOC_PATH="$failmalloc_search_path"], [])
+  fi
+  AC_MSG_CHECKING([for failmalloc])
+  dnl Make sure AC_CHECK_FILE didn't find a directory by mistake
+  if test -n "$FAILMALLOC_PATH" -a -f "$FAILMALLOC_PATH"; then
+    AC_MSG_RESULT([yes])
+  else
+    if test -n "$path_provided"; then
+      AC_MSG_ERROR([$libfailmalloc_file was not found at $failmalloc_search_path])
+    else
+      AC_MSG_RESULT([no])
+    fi
+  fi
+  AC_SUBST(FAILMALLOC_PATH)
+  AM_CONDITIONAL(USE_FAILMALLOC, [test "x$FAILMALLOC_PATH" != "x"])
+])
+
index 21189da..4083e50 100644 (file)
@@ -14,12 +14,17 @@ SUBDIRS = nls
 TESTS = test-mem test-value test-integers test-parse test-tagtable test-sorted \
        test-fuzzer parse-regression.sh swap-byte-order.sh
 
+if USE_FAILMALLOC
+TESTS += check-failmalloc.sh
+endif
+
 check_PROGRAMS = test-mem test-mnote test-value test-integers test-parse \
        test-tagtable test-sorted test-fuzzer
 
 LDADD = $(top_builddir)/libexif/libexif.la $(LTLIBINTL)
 
-EXTRA_DIST = parse-regression.sh swap-byte-order.sh \
+EXTRA_DIST = check-vars.sh.in parse-regression.sh swap-byte-order.sh \
+       check-failmalloc.sh \
        testdata/canon_makernote_variant_1.jpg \
        testdata/canon_makernote_variant_1.jpg.parsed \
        testdata/fuji_makernote_variant_1.jpg \
diff --git a/test/check-failmalloc.sh b/test/check-failmalloc.sh
new file mode 100755 (executable)
index 0000000..602ce66
--- /dev/null
@@ -0,0 +1,71 @@
+#!/bin/sh
+# Use Failmalloc to test behaviour in the face of out-of-memory conditions.
+# The test runs a binary multiple times while configuring Failmalloc to fail a
+# different malloc() call each time, while looking for abnormal program exits
+# due to segfaults. See https://www.nongnu.org/failmalloc/
+#
+# Ideally, it would ensure that the test binary returns an error code on each
+# failure, but this often doesn't happen. This is a problem that should be
+# rectified, but the API doesn't allow returning an error code in many
+# functions that could encounter a problem. The issue could be solve in more
+# cases with more judicious use of log calls with EXIF_LOG_CODE_NO_MEMORY
+# codes.
+. ./check-vars.sh
+
+VERBOSE=
+if [ "$1" = "-v" ] ; then
+    VERBOSE=1
+fi
+
+if [ x"$FAILMALLOC_PATH" = x ]; then
+    echo libfailmalloc is not available
+    echo SKIPPING
+    exit
+fi
+
+BINARY_PREFIX=./
+if [ -e .libs/lt-test-value ]; then
+    # If libtool is in use, the normal "binary" is actually a shell script which
+    # would be interfered with by libfailmalloc. Instead, use the special lt-
+    # binary which should work properly.
+    BINARY_PREFIX=".libs/lt-"
+fi
+
+# Usage: failmalloc_binary_test #iterations binary <optional arguments>
+# FIXME: auto-determine #iterations by comparing the output of each run
+# with the output of a normal run, and exiting when that happens.
+failmalloc_binary_test () {
+  binary="$BINARY_PREFIX$2"
+  iterations="$1"
+  shift
+  shift
+  echo Checking "$binary" for "$iterations" iterations
+  for n in $(seq "$iterations"); do
+      test "$VERBOSE" = 1 && { echo "$n"; set -x; }
+      FAILMALLOC_INTERVAL="$n" LD_PRELOAD="$FAILMALLOC_PATH" "$binary" "$@" >/dev/null
+      s=$?
+      test "$VERBOSE" = 1 && set +x;
+      if test "$s" -ge 128; then
+          # Such status codes only happen due to termination due to a signal
+          # like SIGSEGV.
+          echo "Abnormal binary exit status $s at malloc #$n on $binary"
+          echo FAILURE
+          exit 1
+      fi
+  done
+}
+
+# The number of iterations is determined empirically to be about twice as
+# high as the maximum number of mallocs performed by the test program in order
+# to avoid lowering code coverage in the case of future code changes that cause
+# more allocations.
+
+failmalloc_binary_test 500 test-value
+failmalloc_binary_test 300 test-mem
+for f in $SRCDIR/testdata/*jpg; do
+    echo "Testing `basename "$f"`"
+    failmalloc_binary_test 500 test-parse "$f"
+    # N.B., test-parse --swap-byte-order doesn't test any new paths
+done
+
+echo PASSED
diff --git a/test/check-vars.sh.in b/test/check-vars.sh.in
new file mode 100644 (file)
index 0000000..fa0b4c9
--- /dev/null
@@ -0,0 +1,4 @@
+# Specifies autoconf variables for use by the test scripts
+
+SRCDIR=@srcdir@
+FAILMALLOC_PATH=@FAILMALLOC_PATH@