From 186ea1ef2603483ad49f4493605b4ccecf40eec0 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Date: Sat, 8 Aug 2026 08:11:11 +0530 Subject: [PATCH] test-pdftoraster-copy-height: extract copy_image_rows() helper and test it under ASan --- Makefile.am | 17 +- cupsfilters/pdftoraster.c | 172 ++++++++------ cupsfilters/test-pdftoraster-copy-height.c | 238 +++++++------------- cupsfilters/test-pdftoraster-copy-height.sh | 100 ++++++++ 4 files changed, 284 insertions(+), 243 deletions(-) create mode 100755 cupsfilters/test-pdftoraster-copy-height.sh diff --git a/Makefile.am b/Makefile.am index 175d1ef1f..2689a6047 100644 --- a/Makefile.am +++ b/Makefile.am @@ -92,7 +92,8 @@ lib_LTLIBRARIES = libcupsfilters.la check_SCRIPTS = \ cupsfilters/testfilters.sh \ - cupsfilters/test-pclm-overflow.sh + cupsfilters/test-pclm-overflow.sh \ + cupsfilters/test-pdftoraster-copy-height.sh check_PROGRAMS = \ testcmyk \ @@ -105,8 +106,7 @@ check_PROGRAMS = \ test-analyze \ test-pdf \ test-ps \ - testfilters \ - test-pdftoraster-copy-height + testfilters TESTS = \ testdither \ @@ -115,9 +115,9 @@ TESTS = \ test-analyze \ test-pdf \ test-ps \ - test-pdftoraster-copy-height \ cupsfilters/testfilters.sh \ - cupsfilters/test-pclm-overflow.sh + cupsfilters/test-pclm-overflow.sh \ + cupsfilters/test-pdftoraster-copy-height.sh # testcmyk # fails as it opens some image.ppm which is nowerhe to be found. # testimage # requires also some ppm file as argument @@ -323,9 +323,6 @@ test_ps_SOURCES = cupsfilters/fontembed/test-ps.c test_ps_LDADD = libcupsfilters.la $(CUPS_LIBS) test_ps_CFLAGS = $(CUPS_CFLAGS) -test_pdftoraster_copy_height_SOURCES = \ - cupsfilters/test-pdftoraster-copy-height.c - testfilters_SOURCES = \ cupsfilters/testfilters.c \ $(pkgfiltersinclude_DATA) @@ -343,6 +340,10 @@ testfilters_LDFLAGS = \ noinst_PROGRAMS = gen-lorem-text gen_lorem_text_SOURCES = cupsfilters/gen-lorem-text.c EXTRA_DIST += cupsfilters/gen-lorem-text.c +# Built by cupsfilters/test-pdftoraster-copy-height.sh under AddressSanitizer +# (not a check_PROGRAM, so it can skip when ASan is absent); named here so it +# ships in "make dist". +EXTRA_DIST += cupsfilters/test-pdftoraster-copy-height.c # Generated deterministic lorem text for texttopdf tests BUILT_SOURCES = cupsfilters/test_files/test_text_lorem.txt CLEANFILES = cupsfilters/test_files/test_text_lorem.txt diff --git a/cupsfilters/pdftoraster.c b/cupsfilters/pdftoraster.c index a406f471d..dd2359f13 100644 --- a/cupsfilters/pdftoraster.c +++ b/cupsfilters/pdftoraster.c @@ -1710,6 +1710,101 @@ read_ppm_data(FILE *img, // I - Image file return data; } +// +// 'copy_image_rows()' - Copy the rendered image rows into the CUPS raster +// page, clamped by copy_height/copy_width so a rendered +// image smaller than the page is padded with background +// rows. Extracted from write_page_image() so the +// copy_height boundary can be exercised directly by the +// unit test against a controlled buffer under ASan. +// + +static void +copy_image_rows(cups_raster_t *raster, // I - CUPS raster output + pdftoraster_doc_t *doc, // I - Document attributes + pdf_conversion_function_t *convert, // I - conversion rules + int pageNo, // I - page number (parity) + unsigned char *colordata, // I - rendered image data + unsigned int image_rowsize, // I - bytes per image row + unsigned int copy_height, // I - image rows to copy + unsigned int copy_width, // I - width to copy + unsigned char *lineBuf, // I - scratch line buffer + int bg_color) // I - background fill value +{ + unsigned char *dp; + convert_line_func convertLine; + + if ((pageNo & 1) == 0) + convertLine = convert->convertLineEven; + else + convertLine = convert->convertLineOdd; + + if (doc->header.Duplex && (pageNo & 1) == 0 && doc->swap_image_y) + { + for (unsigned int plane = 0; plane < doc->nplanes; plane ++) + { + unsigned char *bp = colordata + (copy_height - 1) * image_rowsize; + + for (unsigned int h = doc->header.cupsHeight; h > 0; h--) + { + if (h <= copy_height) // inside valid page/image area + { + if (doc->allocLineBuf) + memset(lineBuf, bg_color, doc->bytesPerLine); + for (unsigned int band = 0; band < doc->nbands; band ++) + { + dp = convertLine(bp, lineBuf, h - 1, plane + band, + copy_width, + doc->bytesPerLine, doc, convert->convertCSpace); + cupsRasterWritePixels(raster, dp, doc->bytesPerLine); + } + bp -= image_rowsize; + } + else // Image shorter than page, thus whitespace + { + if (doc->allocLineBuf) + { + memset(lineBuf, bg_color, doc->bytesPerLine); + cupsRasterWritePixels(raster, lineBuf, doc->bytesPerLine); + } + } + } + } + } + else + { + for (unsigned int plane = 0; plane < doc->nplanes; plane++) + { + unsigned char *bp = colordata; + for (unsigned int h = 0; h < doc->header.cupsHeight; h++) + { + if (h < copy_height) // inside valid page/image area + { + if (doc->allocLineBuf) + memset(lineBuf, bg_color, doc->bytesPerLine); + + for (unsigned int band = 0; band < doc->nbands; band++) + { + dp = convertLine(bp, lineBuf, h, plane + band, copy_width, + doc->bytesPerLine, doc, convert->convertCSpace); + cupsRasterWritePixels(raster, dp, doc->bytesPerLine); + } + bp += image_rowsize; + } + else // Image shorter than page, thus whitespace + { + if (doc->allocLineBuf) + { + memset(lineBuf, bg_color, doc->bytesPerLine); + cupsRasterWritePixels(raster, lineBuf, doc->bytesPerLine); + } + } + } + } + } +} + + // // 'write_page_image()' - bridge between PDF rendering tool and CUPS raster Output // @@ -1724,9 +1819,7 @@ write_page_image(cups_raster_t *raster, // I - Cups raster output data struct void *icd) { int i; - convert_line_func convertLine; unsigned char *lineBuf = NULL; - unsigned char *dp; unsigned int image_rowsize = 0; int fakeres[2]; int bg_color = 255; @@ -1958,82 +2051,15 @@ write_page_image(cups_raster_t *raster, // I - Cups raster output data struct } } - if ((pageNo & 1) == 0) - convertLine = convert->convertLineEven; - else - convertLine = convert->convertLineOdd; - - // This will be the safe copy limit; - // In some cases, the PDFtoppm might output where image sizes are + // This will be the safe copy limit; + // In some cases, the PDFtoppm might output where image sizes are // smaller than expected page size. // copy_height and copy_width act as safe copy limit in this. unsigned int copy_height = (height < doc->header.cupsHeight) ? height : doc->header.cupsHeight; unsigned int copy_width = (width < doc->header.cupsWidth) ? width : doc->header.cupsWidth; - if (doc->header.Duplex && (pageNo & 1) == 0 && doc->swap_image_y) - { - for (unsigned int plane = 0; plane < doc->nplanes; plane ++) - { - unsigned char *bp = colordata + (copy_height - 1) * image_rowsize; - - for (unsigned int h = doc->header.cupsHeight; h > 0; h--) - { - if (h <= copy_height) // inside valid page/image area - { - if (doc->allocLineBuf) - memset(lineBuf, bg_color, doc->bytesPerLine); - for (unsigned int band = 0; band < doc->nbands; band ++) - { - dp = convertLine(bp, lineBuf, h - 1, plane + band, - copy_width, - doc->bytesPerLine, doc, convert->convertCSpace); - cupsRasterWritePixels(raster, dp, doc->bytesPerLine); - } - bp -= image_rowsize; - } - else // Image shorter than page, thus whitespace - { - if (doc->allocLineBuf) - { - memset(lineBuf, bg_color, doc->bytesPerLine); - cupsRasterWritePixels(raster, lineBuf, doc->bytesPerLine); - } - } - } - } - } - else - { - for (unsigned int plane = 0; plane < doc->nplanes; plane++) - { - unsigned char *bp = colordata; - for (unsigned int h = 0; h < doc->header.cupsHeight; h++) - { - if (h < copy_height) // inside valid page/image area - { - if (doc->allocLineBuf) - memset(lineBuf, bg_color, doc->bytesPerLine); - - for (unsigned int band = 0; band < doc->nbands; band++) - { - dp = convertLine(bp, lineBuf, h, plane + band, copy_width, - doc->bytesPerLine, doc, convert->convertCSpace); - cupsRasterWritePixels(raster, dp, doc->bytesPerLine); - } - bp += image_rowsize; - } - else // Image shorter than page, thus whitespace - { - if (doc->allocLineBuf) - { - memset(lineBuf, bg_color, doc->bytesPerLine); - cupsRasterWritePixels(raster, lineBuf, doc->bytesPerLine); - } - } - } - } - } - + copy_image_rows(raster, doc, convert, pageNo, colordata, image_rowsize, + copy_height, copy_width, lineBuf, bg_color); free(colordata); if (lineBuf) free(lineBuf); diff --git a/cupsfilters/test-pdftoraster-copy-height.c b/cupsfilters/test-pdftoraster-copy-height.c index 3d6ca6420..5b97b38dc 100644 --- a/cupsfilters/test-pdftoraster-copy-height.c +++ b/cupsfilters/test-pdftoraster-copy-height.c @@ -1,179 +1,93 @@ // -// Test program for pdftoraster copy_height off-by-one fix. +// Regression test for the pdftoraster copy_height off-by-one / out-of-bounds +// fix. It pulls in the real cupsfilters/pdftoraster.c so it exercises the +// actual copy_image_rows() helper (the code write_page_image() calls in +// production), then drives it with a colordata buffer sized to EXACTLY +// copy_height rows while the page (cupsHeight) is one row taller -- i.e. the +// "rendered image shorter than page" case where the off-by-one lived. Built +// with AddressSanitizer, the buggy `h <= copy_height` reads one row past the +// buffer and ASan aborts; the fixed `h < copy_height` stays in bounds. // -// This test validates the copy loop logic used in write_page_image() -// when the rendered image is smaller than the page height. The bug -// used `h <= copy_height` instead of `h < copy_height`, causing one -// extra iteration past the allocated buffer. The fix also adds a guard -// for `allocLineBuf` to prevent NULL-ptr memset. -// -// Licensed under Apache License v2.0. See the file "LICENSE" for more -// information. -// - #include #include #include - -static int n_errors = 0; -#define TEST(cond, msg) do { \ - if (!(cond)) { \ - fprintf(stderr, "FAIL: %s\n", msg); \ - n_errors++; \ - } else { \ - fprintf(stderr, "OK: %s\n", msg); \ - } \ -} while (0) - -typedef struct { - unsigned int cupsHeight; - unsigned int cupsWidth; - unsigned int bytesPerLine; - int allocLineBuf; -} doc_t; - -/* Simulate the forward copy loop from write_page_image(). - * Returns the number of times the "inside valid page/image area" branch executed. - * If allocLineBuf is 0 and the branch would execute, returns 0 on crash (caller - * interprets). */ -static int -simulate_forward_loop(unsigned int image_height, - unsigned int page_height, - int allocLineBuf) +#include +#include + +// Include the unit under test so the static helper and its types are visible. +#include "cupsfilters/pdftoraster.c" + +// A convert-line callback that READS the source row (this is the access that +// goes out of bounds when the copy loop over-runs) and returns a valid dst. +static unsigned char * +test_convert_line(unsigned char *src, unsigned char *dst, + unsigned int row, unsigned int plane, + unsigned int pixels, unsigned int size, + pdftoraster_doc_t *doc, convert_cspace_func convertCSpace) { - doc_t doc; - memset(&doc, 0, sizeof(doc)); - doc.cupsHeight = page_height; - doc.cupsWidth = 16; - doc.bytesPerLine = 16; - doc.allocLineBuf = allocLineBuf; - - unsigned int copy_height = (image_height < page_height) ? image_height : page_height; - unsigned char *colordata = (unsigned char *)malloc(image_height * 16); - unsigned char *lineBuf = NULL; - if (allocLineBuf) - lineBuf = (unsigned char *)malloc(doc.bytesPerLine); - - unsigned char *bp = colordata; - int inside_count = 0; - - for (unsigned int h = 0; h < doc.cupsHeight; h++) - { - if (h < copy_height) // FIXED: was h <= copy_height - { - inside_count++; - if (doc.allocLineBuf) - memset(lineBuf, 0, doc.bytesPerLine); - bp += 16; // image_rowsize - } - else - { - if (doc.allocLineBuf) - memset(lineBuf, 0, doc.bytesPerLine); - } - } - - /* bp should never point past the allocated region. - * After the fix: copy_height iterations, each advancing by 16, - * so bp == colordata + copy_height * 16 == end of allocation. */ - if (bp > colordata + image_height * 16) { - fprintf(stderr, "FAIL: bp=%p exceeds colordata+size=%p (copy_height=%u image_height=%u)\n", - (void*)bp, (void*)(colordata + image_height * 16), copy_height, image_height); - n_errors++; - } - - free(lineBuf); - free(colordata); - return inside_count; + (void)row; (void)plane; (void)doc; (void)convertCSpace; + unsigned int n = pixels < size ? pixels : size; + for (unsigned int i = 0; i < n; i++) + dst[i] = src[i]; // OOB read of src when copy_height is over-run + return dst; } -/* Simulate the reverse (duplex) copy loop. - * - * The reverse loop iterates h from page_height DOWN TO 1, with bp - * starting at the last valid row. Here h <= copy_height is correct - * (unlike the forward loop) because h = copy_height corresponds to - * the last valid row index (copy_height-1), and the loop ends before - * bp can be dereferenced past the allocation start. - */ -static int -simulate_reverse_loop(unsigned int image_height, - unsigned int page_height, - int allocLineBuf) +int +main(void) { - doc_t doc; - memset(&doc, 0, sizeof(doc)); - doc.cupsHeight = page_height; - doc.cupsWidth = 16; - doc.bytesPerLine = 16; - doc.allocLineBuf = allocLineBuf; + const unsigned int copy_height = 4; + const unsigned int rowsize = 8; + const unsigned int copy_width = 8; - unsigned int copy_height = (image_height < page_height) ? image_height : page_height; - unsigned char *colordata = (unsigned char *)malloc(image_height * 16); - unsigned char *lineBuf = NULL; - if (allocLineBuf) - lineBuf = (unsigned char *)malloc(doc.bytesPerLine); + // colordata holds EXACTLY copy_height rows; reading row copy_height is OOB. + unsigned char *colordata = (unsigned char *)malloc((size_t)copy_height * rowsize); + memset(colordata, 0x55, (size_t)copy_height * rowsize); + unsigned char *lineBuf = (unsigned char *)malloc(rowsize); - unsigned char *bp = colordata + (copy_height - 1) * 16; - int inside_count = 0; - - for (unsigned int h = doc.cupsHeight; h > 0; h--) + pdftoraster_doc_t doc; + memset(&doc, 0, sizeof(doc)); + doc.header.cupsWidth = copy_width; + doc.header.cupsHeight = copy_height + 1; // page one row taller + doc.header.cupsBitsPerColor = 8; + doc.header.cupsBitsPerPixel = 8; + doc.header.cupsBytesPerLine = rowsize; + doc.header.cupsColorOrder = CUPS_ORDER_CHUNKED; + doc.header.cupsColorSpace = CUPS_CSPACE_K; + doc.header.cupsNumColors = 1; + doc.header.HWResolution[0] = doc.header.HWResolution[1] = 72; + doc.header.PageSize[0] = copy_width; + doc.header.PageSize[1] = copy_height + 1; + doc.bytesPerLine = rowsize; + doc.nplanes = 1; + doc.nbands = 1; + doc.allocLineBuf = true; + doc.swap_image_y = false; + + pdf_conversion_function_t convert; + memset(&convert, 0, sizeof(convert)); + convert.convertLineEven = test_convert_line; + convert.convertLineOdd = test_convert_line; + convert.convertCSpace = NULL; + + int fd = open("/dev/null", O_WRONLY); + cups_raster_t *raster = cupsRasterOpen(fd, CUPS_RASTER_WRITE); + if (!raster) { - if (h <= copy_height) // correct (h descends from page_height to 1) - { - inside_count++; - if (doc.allocLineBuf) - memset(lineBuf, 0, doc.bytesPerLine); - bp -= 16; - } - else - { - if (doc.allocLineBuf) - memset(lineBuf, 0, doc.bytesPerLine); - } + fprintf(stderr, "cupsRasterOpen failed\n"); + free(lineBuf); + free(colordata); + close(fd); + return 2; } + cupsRasterWriteHeader(raster, &doc.header); + copy_image_rows(raster, &doc, &convert, 0, colordata, rowsize, + copy_height, copy_width, lineBuf, 255); + + cupsRasterClose(raster); + close(fd); free(lineBuf); free(colordata); - return inside_count; -} - -int -main(void) -{ - fprintf(stderr, "--- pdftoraster copy_height tests ---\n"); - - /* Test 1: image < page, allocLineBuf=true — the OOB scenario */ - TEST(simulate_forward_loop(1, 2, 1) == 1, - "forward: image=1 page=2 -> inside_count=1 (was 2 before fix)"); - TEST(simulate_reverse_loop(1, 2, 1) == 1, - "reverse: image=1 page=2 -> inside_count=1 (was 2 before fix)"); - - /* Test 2: image == page — normal case */ - TEST(simulate_forward_loop(5, 5, 1) == 5, - "forward: image=5 page=5 -> inside_count=5"); - TEST(simulate_reverse_loop(5, 5, 1) == 5, - "reverse: image=5 page=5 -> inside_count=5"); - - /* Test 3: image > page — normal case (clamped by copy_height) */ - TEST(simulate_forward_loop(10, 5, 1) == 5, - "forward: image=10 page=5 -> inside_count=5 (clamped)"); - TEST(simulate_reverse_loop(10, 5, 1) == 5, - "reverse: image=10 page=5 -> inside_count=5 (clamped)"); - - /* Test 4: image=0 (edge case: should not crash) */ - /* copy_height = 0, loop should never enter the "inside" branch */ - TEST(simulate_forward_loop(0, 5, 1) == 0, - "forward: image=0 page=5 -> inside_count=0"); - TEST(simulate_reverse_loop(0, 5, 1) == 0, - "reverse: image=0 page=5 -> inside_count=0"); - - /* Test 5: image < page, allocLineBuf=false — the NULL-ptr crash scenario */ - /* Must not crash; function returns early from inside branch since allocLineBuf=false */ - TEST(simulate_forward_loop(1, 2, 0) == 1, - "forward: image=1 page=2 allocLineBuf=0 -> no crash, inside_count=1"); - TEST(simulate_reverse_loop(1, 2, 0) == 1, - "reverse: image=1 page=2 allocLineBuf=0 -> no crash, inside_count=1"); - - fprintf(stderr, "--- %s ---\n", n_errors ? "SOME TESTS FAILED" : "ALL TESTS PASSED"); - return n_errors ? 1 : 0; + fprintf(stderr, "OK: copy_image_rows completed without overrun\n"); + return 0; } diff --git a/cupsfilters/test-pdftoraster-copy-height.sh b/cupsfilters/test-pdftoraster-copy-height.sh new file mode 100755 index 000000000..224c3cb5a --- /dev/null +++ b/cupsfilters/test-pdftoraster-copy-height.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# Regression test for the pdftoraster copy_height off-by-one / out-of-bounds fix. +# +# The C harness (test-pdftoraster-copy-height.c) #includes the in-tree +# cupsfilters/pdftoraster.c so it drives the REAL copy_image_rows() helper that +# write_page_image() uses in production -- not a re-implementation. It is built +# with AddressSanitizer and fed a colordata buffer sized to exactly copy_height +# rows while the page is one row taller, i.e. the "rendered image shorter than +# page" case where the off-by-one lived. The buggy `h <= copy_height` then +# reads one row past the buffer and ASan aborts; the fixed `h < copy_height` +# stays in bounds. Any future regression of that loop is therefore caught. +# +# Skips (Automake exit 77) when AddressSanitizer is unavailable. +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_ROOT="$(cd "${ROOT}/.." && pwd)" +LIBTOOL="${BUILD_ROOT}/libtool" +CC="${CC:-cc}" +SAN_FLAGS="${SAN_FLAGS:--fsanitize=address -fno-omit-frame-pointer}" + +# AddressSanitizer is what gives this test teeth. It must be both linkable AND +# runnable here: without libasan the link fails, and under qemu-user emulation +# (the armhf/riscv64 legs) the ASan runtime aborts at init -- both are +# environment gaps, not libcupsfilters bugs. Compile and RUN a trivial probe; +# skip (Automake exit 77) when ASan cannot actually run. +asan_probe="$(mktemp "${TMPDIR:-/tmp}/asan-probe.XXXXXX")" +if ! printf 'int main(void){return 0;}\n' \ + | "${CC}" ${SAN_FLAGS} -x c - -o "${asan_probe}" >/dev/null 2>&1 \ + || ! "${asan_probe}" >/dev/null 2>&1; then + echo "AddressSanitizer not usable in this environment; skipping." >&2 + rm -f "${asan_probe}" + exit 77 +fi +rm -f "${asan_probe}" + +if [[ ! -x "${LIBTOOL}" ]]; then + echo "libtool helper not found at ${LIBTOOL}" >&2 + exit 99 +fi + +SRC="${ROOT}/test-pdftoraster-copy-height.c" +if [[ ! -f "${SRC}" ]]; then + echo "test source not found: ${SRC}" >&2 + exit 99 +fi + +TMP_PARENT="${TMPDIR:-/tmp}" +WORKDIR="$(mktemp -d "${TMP_PARENT%/}/pdftoraster-copy-height.XXXXXX")" +cleanup() { rm -rf "${WORKDIR}"; } +trap cleanup EXIT + +OBJ="${WORKDIR}/test-pdftoraster-copy-height.o" +BIN="${WORKDIR}/test-pdftoraster-copy-height" +RUN_LOG="${WORKDIR}/run.log" + +# Flags to compile the harness (it pulls in pdftoraster.c -> needs config.h, the +# internal headers and pdftoraster.c's own dependencies). Fall back to cups3. +PKG_CFLAGS="$(pkg-config --cflags lcms2 pdfio cups 2>/dev/null \ + || pkg-config --cflags lcms2 pdfio cups3 2>/dev/null || true)" +PKG_LIBS="$(pkg-config --libs lcms2 pdfio cups 2>/dev/null \ + || pkg-config --libs lcms2 pdfio cups3 2>/dev/null || true)" +INCLUDES="-I${BUILD_ROOT} -I${BUILD_ROOT}/cupsfilters" + +# Compile the harness (which #includes the real pdftoraster.c) under ASan. +"${CC}" -std=gnu11 -O0 -D_GNU_SOURCE ${SAN_FLAGS} \ + ${INCLUDES} ${PKG_CFLAGS} \ + -c "${SRC}" -o "${OBJ}" + +# Link against libcupsfilters.la for the symbols pdftoraster.c references. +# ${PKG_LIBS} carries the right CUPS library (-lcups or -lcups3); do not force +# -lcups, which is absent on the libcups3 (source-3.x) leg. +"${LIBTOOL}" --mode=link --tag=CC "${CC}" ${SAN_FLAGS} \ + "${OBJ}" "${BUILD_ROOT}/libcupsfilters.la" ${PKG_LIBS} -lm \ + -o "${BIN}" >/dev/null + +: > "${RUN_LOG}" +ASAN_OPTS="${ASAN_OPTIONS:-detect_leaks=0,abort_on_error=1}" + +set +e +"${LIBTOOL}" --mode=execute env ASAN_OPTIONS="${ASAN_OPTS}" \ + "${BIN}" >>"${RUN_LOG}" 2>&1 +STATUS=$? +set -e + +if grep -q "AddressSanitizer" "${RUN_LOG}"; then + cat "${RUN_LOG}" >&2 + echo "AddressSanitizer reported a memory error in copy_image_rows()" >&2 + exit 1 +fi + +if [[ ${STATUS} -ne 0 ]]; then + cat "${RUN_LOG}" >&2 + echo "harness exited with status ${STATUS}" >&2 + exit 1 +fi + +exit 0