diff --git a/ChangeLog.md b/ChangeLog.md index 7855931f..43d8961f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -101,6 +101,16 @@ modern ARMv8 CPUs. 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 e0e382d0..0f5ee6da 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); + } + } + + 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); } - fflush(stderr); } } @@ -57,6 +68,8 @@ start_progress_monitor(j_common_ptr cinfo, cd_progress_ptr progress) 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 @@ end_progress_monitor(j_common_ptr cinfo) } } -#endif - /* * Case-insensitive matching of possibly-abbreviated keyword switches. diff --git a/cdjpeg.h b/cdjpeg.h index 9868a0b3..ac8e6ba1 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 cdjpeg_progress_mgr { 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 a3e47bab..64e4af95 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 @@ Compress to memory instead of a file. This feature was implemented mainly as a 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 07e7db14..b2d93048 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 const char *progname; /* program name for error messages */ 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 @@ usage(void) #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 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, icc_filename = NULL; outfilename = NULL; memdst = FALSE; + report = FALSE; cinfo->err->trace_level = 0; /* Scan command line options, adjust parameters */ @@ -395,6 +398,9 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, 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 @@ main(int argc, char **argv) { 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 @@ main(int argc, char **argv) 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 @@ main(int argc, char **argv) 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 e4204b26..bd636057 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 @@ number. For example, .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 @@ Send output image to the named file, not to standard output. 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 @@ decompression scaling is being used, then X, Y, W, and H are relative to the 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 b94dfdfe..f051e45e 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 IMAGE_FORMATS requested_fmt; 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 @@ usage(void) 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 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, /* 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 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, 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 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, /* 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 @@ parse_switches(j_decompress_ptr cinfo, int argc, char **argv, 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 @@ print_text_marker(j_decompress_ptr cinfo) } +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 @@ main(int argc, char **argv) { 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 @@ main(int argc, char **argv) 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 @@ main(int argc, char **argv) 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 @@ main(int argc, char **argv) } } -#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 @@ main(int argc, char **argv) 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 2efb2647..3eb472d9 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 @@ number. For example, .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 058e8443..c61fe6be 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 @@ usage(void) 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 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, /* 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 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, 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 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, 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 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, 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 @@ parse_switches(j_compress_ptr cinfo, int argc, char **argv, } +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 @@ main(int argc, char **argv) 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 @@ main(int argc, char **argv) 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 @@ main(int argc, char **argv) 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 @@ main(int argc, char **argv) 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);