Add fault tolerance features to djpeg and jpegtran
- Enable progress reporting at run time using a new -report argument
(cjpeg now supports that argument as well)
- Limit the allowable number of scans using a new -maxscans argument
- Treat warnings as fatal using a new -strict argument
This mainly demonstrates how to work around the two issues with the
JPEG standard described here:
https://libjpeg-turbo.org/pmwiki/uploads/About/TwoIssueswiththeJPEGStandard.pdf
since those and similar issues continue to be erroneously reported as
libjpeg-turbo bugs.
diff --git a/ChangeLog.md b/ChangeLog.md
index 7855931..43d8961 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -101,6 +101,16 @@
instructions, so that the Loongson MMI SIMD extensions can be included in any
MIPS64 libjpeg-turbo build.
+14. Added fault tolerance features to djpeg and jpegtran, mainly to demonstrate
+methods by which applications can guard against the exploits of the JPEG format
+described in the report
+["Two Issues with the JPEG Standard"](https://libjpeg-turbo.org/pmwiki/uploads/About/TwoIssueswiththeJPEGStandard.pdf).
+
+ - Both programs now accept a `-maxscans` argument, which can be used to
+limit the number of allowable scans in the input file.
+ - Both programs now accept a `-strict` argument, which can be used to
+treat all warnings as fatal.
+
2.0.3
=====
diff --git a/cdjpeg.c b/cdjpeg.c
index e0e382d..0f5ee6d 100644
--- a/cdjpeg.c
+++ b/cdjpeg.c
@@ -3,8 +3,8 @@
*
* This file was part of the Independent JPEG Group's software:
* Copyright (C) 1991-1997, Thomas G. Lane.
- * It was modified by The libjpeg-turbo Project to include only code relevant
- * to libjpeg-turbo.
+ * libjpeg-turbo Modifications:
+ * Copyright (C) 2019, D. R. Commander.
* For conditions of distribution and use, see the accompanying README.ijg
* file.
*
@@ -25,26 +25,37 @@
* Optional progress monitor: display a percent-done figure on stderr.
*/
-#ifdef PROGRESS_REPORT
-
METHODDEF(void)
progress_monitor(j_common_ptr cinfo)
{
cd_progress_ptr prog = (cd_progress_ptr)cinfo->progress;
- int total_passes = prog->pub.total_passes + prog->total_extra_passes;
- int percent_done =
- (int)(prog->pub.pass_counter * 100L / prog->pub.pass_limit);
- if (percent_done != prog->percent_done) {
- prog->percent_done = percent_done;
- if (total_passes > 1) {
- fprintf(stderr, "\rPass %d/%d: %3d%% ",
- prog->pub.completed_passes + prog->completed_extra_passes + 1,
- total_passes, percent_done);
- } else {
- fprintf(stderr, "\r %3d%% ", percent_done);
+ if (prog->max_scans != 0 && cinfo->is_decompressor) {
+ int scan_no = ((j_decompress_ptr)cinfo)->input_scan_number;
+
+ if (scan_no > prog->max_scans) {
+ fprintf(stderr, "Scan number %d exceeds maximum scans (%d)\n", scan_no,
+ prog->max_scans);
+ exit(EXIT_FAILURE);
}
- fflush(stderr);
+ }
+
+ if (prog->report) {
+ int total_passes = prog->pub.total_passes + prog->total_extra_passes;
+ int percent_done =
+ (int)(prog->pub.pass_counter * 100L / prog->pub.pass_limit);
+
+ if (percent_done != prog->percent_done) {
+ prog->percent_done = percent_done;
+ if (total_passes > 1) {
+ fprintf(stderr, "\rPass %d/%d: %3d%% ",
+ prog->pub.completed_passes + prog->completed_extra_passes + 1,
+ total_passes, percent_done);
+ } else {
+ fprintf(stderr, "\r %3d%% ", percent_done);
+ }
+ fflush(stderr);
+ }
}
}
@@ -57,6 +68,8 @@
progress->pub.progress_monitor = progress_monitor;
progress->completed_extra_passes = 0;
progress->total_extra_passes = 0;
+ progress->max_scans = 0;
+ progress->report = FALSE;
progress->percent_done = -1;
cinfo->progress = &progress->pub;
}
@@ -73,8 +86,6 @@
}
}
-#endif
-
/*
* Case-insensitive matching of possibly-abbreviated keyword switches.
diff --git a/cdjpeg.h b/cdjpeg.h
index 9868a0b..ac8e6ba 100644
--- a/cdjpeg.h
+++ b/cdjpeg.h
@@ -4,7 +4,7 @@
* This file was part of the Independent JPEG Group's software:
* Copyright (C) 1994-1997, Thomas G. Lane.
* libjpeg-turbo Modifications:
- * Copyright (C) 2017, D. R. Commander.
+ * Copyright (C) 2017, 2019, D. R. Commander.
* For conditions of distribution and use, see the accompanying README.ijg
* file.
*
@@ -87,6 +87,9 @@
struct jpeg_progress_mgr pub; /* fields known to JPEG library */
int completed_extra_passes; /* extra passes completed */
int total_extra_passes; /* total extra */
+ JDIMENSION max_scans; /* abort if the number of scans exceeds this
+ value and the value is non-zero */
+ boolean report; /* whether or not to report progress */
/* last printed percentage stored here to avoid multiple printouts */
int percent_done;
};
diff --git a/cjpeg.1 b/cjpeg.1
index a3e47ba..64e4af9 100644
--- a/cjpeg.1
+++ b/cjpeg.1
@@ -1,4 +1,4 @@
-.TH CJPEG 1 "18 March 2017"
+.TH CJPEG 1 "18 December 2019"
.SH NAME
cjpeg \- compress an image file to a JPEG file
.SH SYNOPSIS
@@ -215,6 +215,9 @@
way of testing the in-memory destination manager (jpeg_mem_dest()), but it is
also useful for benchmarking, since it reduces the I/O overhead.
.TP
+.BI \-report
+Report compression progress.
+.TP
.B \-verbose
Enable debug printout. More
.BR \-v 's
diff --git a/cjpeg.c b/cjpeg.c
index 07e7db1..b2d9304 100644
--- a/cjpeg.c
+++ b/cjpeg.c
@@ -5,7 +5,7 @@
* Copyright (C) 1991-1998, Thomas G. Lane.
* Modified 2003-2011 by Guido Vollbeding.
* libjpeg-turbo Modifications:
- * Copyright (C) 2010, 2013-2014, 2017, D. R. Commander.
+ * Copyright (C) 2010, 2013-2014, 2017, 2019, D. R. Commander.
* For conditions of distribution and use, see the accompanying README.ijg
* file.
*
@@ -147,6 +147,7 @@
static char *icc_filename; /* for -icc switch */
static char *outfilename; /* for -outfile switch */
boolean memdst; /* for -memdst switch */
+boolean report; /* for -report switch */
LOCAL(void)
@@ -200,6 +201,7 @@
#if JPEG_LIB_VERSION >= 80 || defined(MEM_SRCDST_SUPPORTED)
fprintf(stderr, " -memdst Compress to memory instead of file (useful for benchmarking)\n");
#endif
+ fprintf(stderr, " -report Report compression progress\n");
fprintf(stderr, " -verbose or -debug Emit debug output\n");
fprintf(stderr, " -version Print version information and exit\n");
fprintf(stderr, "Switches for wizards:\n");
@@ -244,6 +246,7 @@
icc_filename = NULL;
outfilename = NULL;
memdst = FALSE;
+ report = FALSE;
cinfo->err->trace_level = 0;
/* Scan command line options, adjust parameters */
@@ -395,6 +398,9 @@
qtablefile = argv[argn];
/* We postpone actually reading the file in case -quality comes later. */
+ } else if (keymatch(arg, "report", 3)) {
+ report = TRUE;
+
} else if (keymatch(arg, "restart", 1)) {
/* Restart interval in MCU rows (or in MCUs with 'b'). */
long lval;
@@ -505,9 +511,7 @@
{
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
-#ifdef PROGRESS_REPORT
struct cdjpeg_progress_mgr progress;
-#endif
int file_index;
cjpeg_source_ptr src_mgr;
FILE *input_file;
@@ -628,9 +632,10 @@
fclose(icc_file);
}
-#ifdef PROGRESS_REPORT
- start_progress_monitor((j_common_ptr)&cinfo, &progress);
-#endif
+ if (report) {
+ start_progress_monitor((j_common_ptr)&cinfo, &progress);
+ progress.report = report;
+ }
/* Figure out the input file format, and set up to read it. */
src_mgr = select_file_type(&cinfo, input_file);
@@ -676,9 +681,8 @@
if (output_file != stdout && output_file != NULL)
fclose(output_file);
-#ifdef PROGRESS_REPORT
- end_progress_monitor((j_common_ptr)&cinfo);
-#endif
+ if (report)
+ end_progress_monitor((j_common_ptr)&cinfo);
if (memdst) {
fprintf(stderr, "Compressed size: %lu bytes\n", outsize);
diff --git a/djpeg.1 b/djpeg.1
index e4204b2..bd63605 100644
--- a/djpeg.1
+++ b/djpeg.1
@@ -1,4 +1,4 @@
-.TH DJPEG 1 "13 November 2017"
+.TH DJPEG 1 "18 December 2019"
.SH NAME
djpeg \- decompress a JPEG file to an image file
.SH SYNOPSIS
@@ -190,6 +190,19 @@
.B \-max 4m
selects 4000000 bytes. If more space is needed, an error will occur.
.TP
+.BI \-maxscans " N"
+Abort if the JPEG image contains more than
+.I N
+scans. This feature demonstrates a method by which applications can guard
+against denial-of-service attacks instigated by specially-crafted malformed
+JPEG images containing numerous scans with missing image data or image data
+consisting only of "EOB runs" (a feature of progressive JPEG images that allows
+potentially hundreds of thousands of adjoining zero-value pixels to be
+represented using only a few bytes.) Attempting to decompress such malformed
+JPEG images can cause excessive CPU activity, since the decompressor must fully
+process each scan (even if the scan is corrupt) before it can proceed to the
+next scan.
+.TP
.BI \-outfile " name"
Send output image to the named file, not to standard output.
.TP
@@ -197,6 +210,9 @@
Load input file into memory before decompressing. This feature was implemented
mainly as a way of testing the in-memory source manager (jpeg_mem_src().)
.TP
+.BI \-report
+Report decompression progress.
+.TP
.BI \-skip " Y0,Y1"
Decompress all rows of the JPEG image except those between Y0 and Y1
(inclusive.) Note that if decompression scaling is being used, then Y0 and Y1
@@ -210,6 +226,12 @@
scaled image dimensions. Currently this option only works with the
PBMPLUS (PPM/PGM), GIF, and Targa output formats.
.TP
+.BI \-strict
+Treat all warnings as fatal. This feature also demonstrates a method by which
+applications can guard against attacks instigated by specially-crafted
+malformed JPEG images. Enabling this option will cause the decompressor to
+abort if the JPEG image contains incomplete or corrupt image data.
+.TP
.B \-verbose
Enable debug printout. More
.BR \-v 's
diff --git a/djpeg.c b/djpeg.c
index b94dfdf..f051e45 100644
--- a/djpeg.c
+++ b/djpeg.c
@@ -5,7 +5,7 @@
* Copyright (C) 1991-1997, Thomas G. Lane.
* Modified 2013 by Guido Vollbeding.
* libjpeg-turbo Modifications:
- * Copyright (C) 2010-2011, 2013-2017, D. R. Commander.
+ * Copyright (C) 2010-2011, 2013-2017, 2019, D. R. Commander.
* Copyright (C) 2015, Google, Inc.
* For conditions of distribution and use, see the accompanying README.ijg
* file.
@@ -94,11 +94,14 @@
static const char *progname; /* program name for error messages */
static char *icc_filename; /* for -icc switch */
+JDIMENSION max_scans; /* for -maxscans switch */
static char *outfilename; /* for -outfile switch */
boolean memsrc; /* for -memsrc switch */
+boolean report; /* for -report switch */
boolean skip, crop;
JDIMENSION skip_start, skip_end;
JDIMENSION crop_x, crop_y, crop_width, crop_height;
+boolean strict; /* for -strict switch */
#define INPUT_BUF_SIZE 4096
@@ -171,14 +174,16 @@
fprintf(stderr, " -onepass Use 1-pass quantization (fast, low quality)\n");
#endif
fprintf(stderr, " -maxmemory N Maximum memory to use (in kbytes)\n");
+ fprintf(stderr, " -maxscans N Maximum number of scans to allow in input file\n");
fprintf(stderr, " -outfile name Specify name for output file\n");
#if JPEG_LIB_VERSION >= 80 || defined(MEM_SRCDST_SUPPORTED)
fprintf(stderr, " -memsrc Load input file into memory before decompressing\n");
#endif
-
+ fprintf(stderr, " -report Report decompression progress\n");
fprintf(stderr, " -skip Y0,Y1 Decompress all rows except those between Y0 and Y1 (inclusive)\n");
fprintf(stderr, " -crop WxH+X+Y Decompress only a rectangular subregion of the image\n");
fprintf(stderr, " [requires PBMPLUS (PPM/PGM), GIF, or Targa output format]\n");
+ fprintf(stderr, " -strict Treat all warnings as fatal\n");
fprintf(stderr, " -verbose or -debug Emit debug output\n");
fprintf(stderr, " -version Print version information and exit\n");
exit(EXIT_FAILURE);
@@ -203,10 +208,13 @@
/* Set up default JPEG parameters. */
requested_fmt = DEFAULT_FMT; /* set default output file format */
icc_filename = NULL;
+ max_scans = 0;
outfilename = NULL;
memsrc = FALSE;
+ report = FALSE;
skip = FALSE;
crop = FALSE;
+ strict = FALSE;
cinfo->err->trace_level = 0;
/* Scan command line options, adjust parameters */
@@ -351,6 +359,12 @@
lval *= 1000L;
cinfo->mem->max_memory_to_use = lval * 1000L;
+ } else if (keymatch(arg, "maxscans", 4)) {
+ if (++argn >= argc) /* advance to next argument */
+ usage();
+ if (sscanf(argv[argn], "%u", &max_scans) != 1)
+ usage();
+
} else if (keymatch(arg, "nosmooth", 3)) {
/* Suppress fancy upsampling */
cinfo->do_fancy_upsampling = FALSE;
@@ -383,6 +397,9 @@
/* PPM/PGM output format. */
requested_fmt = FMT_PPM;
+ } else if (keymatch(arg, "report", 2)) {
+ report = TRUE;
+
} else if (keymatch(arg, "rle", 1)) {
/* RLE output format. */
requested_fmt = FMT_RLE;
@@ -413,6 +430,9 @@
usage();
crop = TRUE;
+ } else if (keymatch(arg, "strict", 2)) {
+ strict = TRUE;
+
} else if (keymatch(arg, "targa", 1)) {
/* Targa output format. */
requested_fmt = FMT_TARGA;
@@ -499,6 +519,19 @@
}
+METHODDEF(void)
+my_emit_message(j_common_ptr cinfo, int msg_level)
+{
+ if (msg_level < 0) {
+ /* Treat warning as fatal */
+ cinfo->err->error_exit(cinfo);
+ } else {
+ if (cinfo->err->trace_level >= msg_level)
+ cinfo->err->output_message(cinfo);
+ }
+}
+
+
/*
* The main program.
*/
@@ -508,9 +541,7 @@
{
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
-#ifdef PROGRESS_REPORT
struct cdjpeg_progress_mgr progress;
-#endif
int file_index;
djpeg_dest_ptr dest_mgr = NULL;
FILE *input_file;
@@ -555,6 +586,9 @@
file_index = parse_switches(&cinfo, argc, argv, 0, FALSE);
+ if (strict)
+ jerr.emit_message = my_emit_message;
+
#ifdef TWO_FILE_COMMANDLINE
/* Must have either -outfile switch or explicit output file name */
if (outfilename == NULL) {
@@ -601,9 +635,11 @@
output_file = write_stdout();
}
-#ifdef PROGRESS_REPORT
- start_progress_monitor((j_common_ptr)&cinfo, &progress);
-#endif
+ if (report || max_scans != 0) {
+ start_progress_monitor((j_common_ptr)&cinfo, &progress);
+ progress.report = report;
+ progress.max_scans = max_scans;
+ }
/* Specify data source for decompression */
#if JPEG_LIB_VERSION >= 80 || defined(MEM_SRCDST_SUPPORTED)
@@ -763,12 +799,11 @@
}
}
-#ifdef PROGRESS_REPORT
/* Hack: count final pass as done in case finish_output does an extra pass.
* The library won't have updated completed_passes.
*/
- progress.pub.completed_passes = progress.pub.total_passes;
-#endif
+ if (report || max_scans != 0)
+ progress.pub.completed_passes = progress.pub.total_passes;
if (icc_filename != NULL) {
FILE *icc_file;
@@ -807,9 +842,8 @@
if (output_file != stdout)
fclose(output_file);
-#ifdef PROGRESS_REPORT
- end_progress_monitor((j_common_ptr)&cinfo);
-#endif
+ if (report || max_scans != 0)
+ end_progress_monitor((j_common_ptr)&cinfo);
if (memsrc && inbuffer != NULL)
free(inbuffer);
diff --git a/jpegtran.1 b/jpegtran.1
index 2efb264..3eb472d 100644
--- a/jpegtran.1
+++ b/jpegtran.1
@@ -1,4 +1,4 @@
-.TH JPEGTRAN 1 "18 March 2017"
+.TH JPEGTRAN 1 "18 December 2019"
.SH NAME
jpegtran \- lossless transformation of JPEG files
.SH SYNOPSIS
@@ -229,9 +229,31 @@
.B \-max 4m
selects 4000000 bytes. If more space is needed, an error will occur.
.TP
+.BI \-maxscans " N"
+Abort if the input image contains more than
+.I N
+scans. This feature demonstrates a method by which applications can guard
+against denial-of-service attacks instigated by specially-crafted malformed
+JPEG images containing numerous scans with missing image data or image data
+consisting only of "EOB runs" (a feature of progressive JPEG images that allows
+potentially hundreds of thousands of adjoining zero-value pixels to be
+represented using only a few bytes.) Attempting to transform such malformed
+JPEG images can cause excessive CPU activity, since the decompressor must fully
+process each scan (even if the scan is corrupt) before it can proceed to the
+next scan.
+.TP
.BI \-outfile " name"
Send output image to the named file, not to standard output.
.TP
+.BI \-report
+Report transformation progress.
+.TP
+.BI \-strict
+Treat all warnings as fatal. This feature also demonstrates a method by which
+applications can guard against attacks instigated by specially-crafted
+malformed JPEG images. Enabling this option will cause the decompressor to
+abort if the input image contains incomplete or corrupt image data.
+.TP
.B \-verbose
Enable debug printout. More
.BR \-v 's
diff --git a/jpegtran.c b/jpegtran.c
index 058e844..c61fe6b 100644
--- a/jpegtran.c
+++ b/jpegtran.c
@@ -4,7 +4,7 @@
* This file was part of the Independent JPEG Group's software:
* Copyright (C) 1995-2010, Thomas G. Lane, Guido Vollbeding.
* libjpeg-turbo Modifications:
- * Copyright (C) 2010, 2014, 2017, D. R. Commander.
+ * Copyright (C) 2010, 2014, 2017, 2019, D. R. Commander.
* For conditions of distribution and use, see the accompanying README.ijg
* file.
*
@@ -41,7 +41,10 @@
static const char *progname; /* program name for error messages */
static char *icc_filename; /* for -icc switch */
+JDIMENSION max_scans; /* for -maxscans switch */
static char *outfilename; /* for -outfile switch */
+boolean report; /* for -report switch */
+boolean strict; /* for -strict switch */
static JCOPY_OPTION copyoption; /* -copy switch */
static jpeg_transform_info transformoption; /* image transformation options */
@@ -87,7 +90,10 @@
fprintf(stderr, " -icc FILE Embed ICC profile contained in FILE\n");
fprintf(stderr, " -restart N Set restart interval in rows, or in blocks with B\n");
fprintf(stderr, " -maxmemory N Maximum memory to use (in kbytes)\n");
+ fprintf(stderr, " -maxscans N Maximum number of scans to allow in input file\n");
fprintf(stderr, " -outfile name Specify name for output file\n");
+ fprintf(stderr, " -report Report transformation progress\n");
+ fprintf(stderr, " -strict Treat all warnings as fatal\n");
fprintf(stderr, " -verbose or -debug Emit debug output\n");
fprintf(stderr, " -version Print version information and exit\n");
fprintf(stderr, "Switches for wizards:\n");
@@ -141,7 +147,10 @@
/* Set up default JPEG parameters. */
simple_progressive = FALSE;
icc_filename = NULL;
+ max_scans = 0;
outfilename = NULL;
+ report = FALSE;
+ strict = FALSE;
copyoption = JCOPYOPT_DEFAULT;
transformoption.transform = JXFORM_NONE;
transformoption.perfect = FALSE;
@@ -261,6 +270,12 @@
lval *= 1000L;
cinfo->mem->max_memory_to_use = lval * 1000L;
+ } else if (keymatch(arg, "maxscans", 4)) {
+ if (++argn >= argc) /* advance to next argument */
+ usage();
+ if (sscanf(argv[argn], "%u", &max_scans) != 1)
+ usage();
+
} else if (keymatch(arg, "optimize", 1) || keymatch(arg, "optimise", 1)) {
/* Enable entropy parm optimization. */
#ifdef ENTROPY_OPT_SUPPORTED
@@ -293,6 +308,9 @@
exit(EXIT_FAILURE);
#endif
+ } else if (keymatch(arg, "report", 3)) {
+ report = TRUE;
+
} else if (keymatch(arg, "restart", 1)) {
/* Restart interval in MCU rows (or in MCUs with 'b'). */
long lval;
@@ -338,6 +356,9 @@
exit(EXIT_FAILURE);
#endif
+ } else if (keymatch(arg, "strict", 2)) {
+ strict = TRUE;
+
} else if (keymatch(arg, "transpose", 1)) {
/* Transpose (across UL-to-LR axis). */
select_transform(JXFORM_TRANSPOSE);
@@ -375,6 +396,19 @@
}
+METHODDEF(void)
+my_emit_message(j_common_ptr cinfo, int msg_level)
+{
+ if (msg_level < 0) {
+ /* Treat warning as fatal */
+ cinfo->err->error_exit(cinfo);
+ } else {
+ if (cinfo->err->trace_level >= msg_level)
+ cinfo->err->output_message(cinfo);
+ }
+}
+
+
/*
* The main program.
*/
@@ -385,9 +419,7 @@
struct jpeg_decompress_struct srcinfo;
struct jpeg_compress_struct dstinfo;
struct jpeg_error_mgr jsrcerr, jdsterr;
-#ifdef PROGRESS_REPORT
- struct cdjpeg_progress_mgr progress;
-#endif
+ struct cdjpeg_progress_mgr src_progress, dst_progress;
jvirt_barray_ptr *src_coef_arrays;
jvirt_barray_ptr *dst_coef_arrays;
int file_index;
@@ -427,6 +459,9 @@
jsrcerr.trace_level = jdsterr.trace_level;
srcinfo.mem->max_memory_to_use = dstinfo.mem->max_memory_to_use;
+ if (strict)
+ jsrcerr.emit_message = my_emit_message;
+
#ifdef TWO_FILE_COMMANDLINE
/* Must have either -outfile switch or explicit output file name */
if (outfilename == NULL) {
@@ -492,9 +527,15 @@
copyoption = JCOPYOPT_ALL_EXCEPT_ICC;
}
-#ifdef PROGRESS_REPORT
- start_progress_monitor((j_common_ptr)&dstinfo, &progress);
-#endif
+ if (report) {
+ start_progress_monitor((j_common_ptr)&dstinfo, &dst_progress);
+ dst_progress.report = report;
+ }
+ if (report || max_scans != 0) {
+ start_progress_monitor((j_common_ptr)&srcinfo, &src_progress);
+ src_progress.report = report;
+ src_progress.max_scans = max_scans;
+ }
/* Specify data source for decompression */
jpeg_stdio_src(&srcinfo, fp);
@@ -587,9 +628,10 @@
if (fp != stdout)
fclose(fp);
-#ifdef PROGRESS_REPORT
- end_progress_monitor((j_common_ptr)&dstinfo);
-#endif
+ if (report)
+ end_progress_monitor((j_common_ptr)&dstinfo);
+ if (report || max_scans != 0)
+ end_progress_monitor((j_common_ptr)&srcinfo);
if (icc_profile != NULL)
free(icc_profile);