From 16fa047311e2fb4b5e8e39e89212d9087adfc12d Mon Sep 17 00:00:00 2001 From: RHEL Packaging Agent Date: Mon, 22 Jun 2026 15:46:56 +0200 Subject: [PATCH] Fix CVE-2026-34980: control character injection in option values Resolves: RHEL-177945 Co-Authored: Zdenek Dohnal --- ...ontrol-characters-from-option-values.patch | 72 ++++ ...er-PPD-keyword-processing-Issue-1562.patch | 22 ++ ...ix-get_options-regression-Issue-1532.patch | 59 +++ ...GetConf-and-cupsFilePutConf-to-escap.patch | 362 ++++++++++++++++++ cups.spec | 24 +- 5 files changed, 538 insertions(+), 1 deletion(-) create mode 100644 0001-Filter-out-control-characters-from-option-values.patch create mode 100644 0001-Fix-filter-PPD-keyword-processing-Issue-1562.patch create mode 100644 0001-Fix-get_options-regression-Issue-1532.patch create mode 100644 0001-Updated-cupsFileGetConf-and-cupsFilePutConf-to-escap.patch diff --git a/0001-Filter-out-control-characters-from-option-values.patch b/0001-Filter-out-control-characters-from-option-values.patch new file mode 100644 index 0000000..79dbdbb --- /dev/null +++ b/0001-Filter-out-control-characters-from-option-values.patch @@ -0,0 +1,72 @@ +From 8d0f51cac24cb5bf949c5b6a221e51a150d982e3 Mon Sep 17 00:00:00 2001 +From: Michael R Sweet +Date: Tue, 31 Mar 2026 14:45:13 -0400 +Subject: [PATCH] Filter out control characters from option values. + +--- + scheduler/job.c | 41 +++++++++++++++++++++++++++++++++++------ + 2 files changed, 37 insertions(+), 6 deletions(-) + +diff --git a/scheduler/job.c b/scheduler/job.c +index 1fef9d0cd..af6390687 100644 +--- a/scheduler/job.c ++++ b/scheduler/job.c +@@ -4118,9 +4118,21 @@ get_options(cupsd_job_t *job, /* I - Job */ + case IPP_TAG_URI : + for (valptr = attr->values[i].string.text; *valptr;) + { +- if (strchr(" \t\n\\\'\"", *valptr)) +- *optptr++ = '\\'; +- *optptr++ = *valptr++; ++ /* ++ * Convert tabs and newlines to spaces, filter out control chars, ++ * and escape \, ', and ". ++ */ ++ ++ if (isspace(*valptr & 255)) ++ { ++ *optptr++ = ' '; ++ } ++ else if ((*valptr & 255) >= ' ' && *valptr != 0x7f) ++ { ++ if (strchr("\\\'\"", *valptr)) ++ *optptr++ = '\\'; ++ *optptr++ = *valptr++; ++ } + } + + *optptr = '\0'; +@@ -5395,13 +5407,30 @@ update_job(cupsd_job_t *job) /* I - Job to check */ + else if (loglevel == CUPSD_LOG_PPD) + { + /* +- * Set attribute(s)... ++ * Set PPD keyword(s)/value(s)... + */ + ++ int i, /* Looping var */ ++ num_keywords; /* Number of keywords */ ++ cups_option_t *keywords, /* Keywords */ ++ *keyword; /* Current keyword */ ++ + cupsdLogJob(job, CUPSD_LOG_DEBUG, "PPD: %s", message); + +- job->num_keywords = cupsParseOptions(message, job->num_keywords, +- &job->keywords); ++ keywords = NULL; ++ num_keywords = cupsParseOptions(message, 0, &keywords); ++ ++ for (i = 0, keyword = keywords; i < num_keywords; i ++) ++ { ++ /* ++ * Filter out "special" PPD keywords... ++ */ ++ ++ if (strcmp(keyword->name, "cupsFilter") && strcmp(keyword->name, "cupsFilter2") && strcmp(keyword->name, "cupsFinishingTemplate") && strcmp(keyword->name, "cupsIPPFinishings") && strcmp(keyword->name, "cupsIPPReason") && strcmp(keyword->name, "cupsMarkerName") && strcmp(keyword->name, "cupsMaxSize") && strncmp(keyword->name, "cupsMediaQualifier", 18) && strcmp(keyword->name, "cupsMinSize") && strcmp(keyword->name, "cupsPageSizeCategory") && strcmp(keyword->name, "cupsPortMonitor") && strcmp(keyword->name, "cupsPreFilter") && strcmp(keyword->name, "cupsPrintQuality") && strcmp(keyword->name, "APPrinterPreset")) ++ job->num_keywords = cupsAddOption(keyword->name, keyword->value, job->num_keywords, &job->keywords); ++ } ++ ++ cupsFreeOptions(num_keywords, keywords); + } + else + { diff --git a/0001-Fix-filter-PPD-keyword-processing-Issue-1562.patch b/0001-Fix-filter-PPD-keyword-processing-Issue-1562.patch new file mode 100644 index 0000000..642c9fc --- /dev/null +++ b/0001-Fix-filter-PPD-keyword-processing-Issue-1562.patch @@ -0,0 +1,22 @@ +From 3f2bdc293243bca938c6de23ba50e6d783189629 Mon Sep 17 00:00:00 2001 +From: Michael R Sweet +Date: Tue, 28 Apr 2026 17:42:41 -0400 +Subject: [PATCH] Fix filter PPD keyword processing (Issue #1562) + +--- + scheduler/job.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/scheduler/job.c b/scheduler/job.c +index 7cc7195ef..f09751591 100644 +--- a/scheduler/job.c ++++ b/scheduler/job.c +@@ -5415,7 +5415,7 @@ update_job(cupsd_job_t *job) /* I - Job to check */ + keywords = NULL; + num_keywords = cupsParseOptions(message, 0, &keywords); + +- for (i = 0, keyword = keywords; i < num_keywords; i ++) ++ for (i = 0, keyword = keywords; i < num_keywords; i ++, keyword ++) + { + /* + * Filter out "special" PPD keywords... diff --git a/0001-Fix-get_options-regression-Issue-1532.patch b/0001-Fix-get_options-regression-Issue-1532.patch new file mode 100644 index 0000000..f3d8d3d --- /dev/null +++ b/0001-Fix-get_options-regression-Issue-1532.patch @@ -0,0 +1,59 @@ +From da0ff58c041f7ee129c3c2c72fb14df1f1e4069a Mon Sep 17 00:00:00 2001 +From: Michael R Sweet +Date: Wed, 8 Apr 2026 16:43:25 -0400 +Subject: [PATCH] Fix get_options regression (Issue #1532) + +--- + scheduler/job.c | 4 ++-- + test/5.5-lp.sh | 10 +++++----- + 2 files changed, 7 insertions(+), 7 deletions(-) + +diff --git a/scheduler/job.c b/scheduler/job.c +index 064e6c1c2a..7cc7195ef0 100644 +--- a/scheduler/job.c ++++ b/scheduler/job.c +@@ -4120,7 +4120,7 @@ get_options(cupsd_job_t *job, /* I - Job */ + case IPP_TAG_CHARSET : + case IPP_TAG_LANGUAGE : + case IPP_TAG_URI : +- for (valptr = attr->values[i].string.text; *valptr;) ++ for (valptr = attr->values[i].string.text; *valptr; valptr ++) + { + /* + * Convert tabs and newlines to spaces, filter out control chars, +@@ -4135,7 +4135,7 @@ get_options(cupsd_job_t *job, /* I - Job */ + { + if (strchr("\\\'\"", *valptr)) + *optptr++ = '\\'; +- *optptr++ = *valptr++; ++ *optptr++ = *valptr; + } + } + +diff --git a/test/5.5-lp.sh b/test/5.5-lp.sh +index 25e9d65a33..fe60890305 100644 +--- a/test/5.5-lp.sh ++++ b/test/5.5-lp.sh +@@ -72,8 +72,8 @@ echo "" + + echo "LP Flood Test ($1 times in parallel)" + echo "" +-echo " lp -d Test1 testfile.jpg" +-echo " lp -d Test2 testfile.jpg" ++echo " lp -d Test1 -t 'Flood Test N' testfile.jpg" ++echo " lp -d Test2 -t 'Flood Test N' testfile.jpg" + i=0 + pids="" + while test $i -lt $1; do +@@ -83,9 +83,9 @@ while test $i -lt $1; do + j=`expr $j + 1` + done + +- $runcups $VALGRIND ../systemv/lp -d Test1 ../examples/testfile.jpg 2>&1 & ++ $runcups $VALGRIND ../systemv/lp -d Test1 -t "Flood Test $j" ../examples/testfile.jpg 2>&1 & + pids="$pids $!" +- $runcups $VALGRIND ../systemv/lp -d Test2 ../examples/testfile.jpg 2>&1 & ++ $runcups $VALGRIND ../systemv/lp -d Test2 -t "Flood Test $j" ../examples/testfile.jpg 2>&1 & + pids="$pids $!" + + i=`expr $i + 1` diff --git a/0001-Updated-cupsFileGetConf-and-cupsFilePutConf-to-escap.patch b/0001-Updated-cupsFileGetConf-and-cupsFilePutConf-to-escap.patch new file mode 100644 index 0000000..ef4995d --- /dev/null +++ b/0001-Updated-cupsFileGetConf-and-cupsFilePutConf-to-escap.patch @@ -0,0 +1,362 @@ +From c5ce534c03d0698509f3eb7ff90ca41610625570 Mon Sep 17 00:00:00 2001 +From: Michael R Sweet +Date: Thu, 19 Feb 2026 18:44:33 -0500 +Subject: [PATCH] Updated and to escape more characters. + +--- + cups/file.c | 147 +++++++++++++++++++++++++++++++++--------------- + cups/testfile.c | 63 +++++++++++++++++++-- + 3 files changed, 161 insertions(+), 50 deletions(-) + +diff --git a/cups/file.c b/cups/file.c +index 7f48706335..a053368c82 100644 +--- a/cups/file.c ++++ b/cups/file.c +@@ -725,11 +725,9 @@ cupsFileGetConf(cups_file_t *fp, /* I - CUPS file */ + * Range check input... + */ + +- DEBUG_printf(("2cupsFileGetConf(fp=%p, buf=%p, buflen=" CUPS_LLFMT +- ", value=%p, linenum=%p)", (void *)fp, (void *)buf, CUPS_LLCAST buflen, (void *)value, (void *)linenum)); ++ DEBUG_printf(("2cupsFileGetConf(fp=%p, buf=%p, buflen=" CUPS_LLFMT ", value=%p, linenum=%p)", (void *)fp, (void *)buf, CUPS_LLCAST buflen, (void *)value, (void *)linenum)); + +- if (!fp || (fp->mode != 'r' && fp->mode != 's') || +- !buf || buflen < 2 || !value) ++ if (!fp || (fp->mode != 'r' && fp->mode != 's') || !buf || buflen < 2 || !value) + { + if (value) + *value = NULL; +@@ -748,28 +746,30 @@ cupsFileGetConf(cups_file_t *fp, /* I - CUPS file */ + (*linenum) ++; + + /* +- * Strip any comments... ++ * Handle escaped characters and strip any comments... + */ + +- if ((ptr = strchr(buf, '#')) != NULL) ++ for (ptr = buf; *ptr; ptr ++) + { +- if (ptr > buf && ptr[-1] == '\\') ++ if (*ptr == '#') + { +- // Unquote the #... +- _cups_strcpy(ptr - 1, ptr); ++ // Strip comment text... ++ *ptr = '\0'; ++ break; + } +- else ++ else if (*ptr == '\\' && (ptr[1] == '\\' || ptr[1] == '#' || ptr[1] == 'n' || ptr[1] == 'r')) + { +- // Strip the comment and any trailing whitespace... +- while (ptr > buf) +- { +- if (!_cups_isspace(ptr[-1])) +- break; ++ /* ++ * \\, \#, \n, or \r, remove backslash and update the escaped char as ++ * needed... ++ */ + +- ptr --; +- } ++ _cups_strcpy(ptr, ptr + 1); + +- *ptr = '\0'; ++ if (*ptr == 'n') ++ *ptr = '\n'; ++ else if (*ptr == 'r') ++ *ptr = '\r'; + } + } + +@@ -777,7 +777,11 @@ cupsFileGetConf(cups_file_t *fp, /* I - CUPS file */ + * Strip leading whitespace... + */ + +- for (ptr = buf; _cups_isspace(*ptr); ptr ++); ++ for (ptr = buf; *ptr; ptr ++) ++ { ++ if (!_cups_isspace(*ptr)) ++ break; ++ } + + if (ptr > buf) + _cups_strcpy(buf, ptr); +@@ -793,14 +797,16 @@ cupsFileGetConf(cups_file_t *fp, /* I - CUPS file */ + */ + + for (ptr = buf; *ptr; ptr ++) ++ { + if (_cups_isspace(*ptr)) + break; ++ } + + if (*ptr) + { + /* + * Have a value, skip any other spaces... +- */ ++ */ + + while (_cups_isspace(*ptr)) + *ptr++ = '\0'; +@@ -810,12 +816,14 @@ cupsFileGetConf(cups_file_t *fp, /* I - CUPS file */ + + /* + * Strip trailing whitespace and > for lines that begin with <... +- */ ++ */ + + ptr += strlen(ptr) - 1; + + if (buf[0] == '<' && *ptr == '>') ++ { + *ptr-- = '\0'; ++ } + else if (buf[0] == '<' && *ptr != '>') + { + /* +@@ -1497,19 +1505,18 @@ cupsFilePutChar(cups_file_t *fp, /* I - CUPS file */ + /* + * 'cupsFilePutConf()' - Write a configuration line. + * +- * This function handles any comment escaping of the value. ++ * This function handles any escaping of the value. + * + * @since CUPS 1.4/macOS 10.6@ + */ + + ssize_t /* O - Number of bytes written or -1 on error */ + cupsFilePutConf(cups_file_t *fp, /* I - CUPS file */ +- const char *directive, /* I - Directive */ +- const char *value) /* I - Value */ ++ const char *directive, /* I - Directive */ ++ const char *value) /* I - Value */ + { + ssize_t bytes, /* Number of bytes written */ + temp; /* Temporary byte count */ +- const char *ptr; /* Pointer into value */ + + + if (!fp || !directive || !*directive) +@@ -1518,34 +1525,84 @@ cupsFilePutConf(cups_file_t *fp, /* I - CUPS file */ + if ((bytes = cupsFilePuts(fp, directive)) < 0) + return (-1); + +- if (cupsFilePutChar(fp, ' ') < 0) +- return (-1); +- bytes ++; +- + if (value && *value) + { +- if ((ptr = strchr(value, '#')) != NULL) ++ const char *start, // Start of current fragment ++ *ptr; // Pointer into value ++ ++ if (cupsFilePutChar(fp, ' ') < 0) ++ return (-1); ++ bytes ++; ++ ++ for (start = ptr = value; *ptr; ptr ++) ++ { ++ if (strchr("#\\\n\r", *ptr) != NULL) ++ { ++ /* ++ * Character that needs to be escaped... ++ */ ++ ++ if (ptr > start) ++ { ++ /* ++ * Write unescaped portion... ++ */ ++ ++ if ((temp = cupsFileWrite(fp, start, (size_t)(ptr - start))) < 0) ++ return (-1); ++ ++ bytes += temp; ++ } ++ ++ start = ptr + 1; ++ ++ if (*ptr == '\\') ++ { ++ /* ++ * "\" (for escaping) ++ */ ++ ++ if (cupsFilePuts(fp, "\\\\") < 0) ++ return (-1); ++ } ++ else if (*ptr == '#') ++ { ++ /* ++ * "#" (for comment) ++ */ ++ ++ if (cupsFilePuts(fp, "\\#") < 0) ++ return (-1); ++ } ++ else if (*ptr == '\n') ++ { ++ /* ++ * LF ++ */ ++ ++ if (cupsFilePuts(fp, "\\n") < 0) ++ return (-1); ++ } ++ else if (cupsFilePuts(fp, "\\r") < 0) ++ { ++ return (-1); ++ } ++ ++ bytes += 2; ++ } ++ } ++ ++ if (ptr > start) + { + /* +- * Need to quote the first # in the info string... ++ * Write remaining unescaped portion... + */ + +- if ((temp = cupsFileWrite(fp, value, (size_t)(ptr - value))) < 0) +- return (-1); +- bytes += temp; +- +- if (cupsFilePutChar(fp, '\\') < 0) +- return (-1); +- bytes ++; ++ if ((temp = cupsFileWrite(fp, start, (size_t)(ptr - start))) < 0) ++ return (-1); + +- if ((temp = cupsFilePuts(fp, ptr)) < 0) +- return (-1); + bytes += temp; + } +- else if ((temp = cupsFilePuts(fp, value)) < 0) +- return (-1); +- else +- bytes += temp; + } + + if (cupsFilePutChar(fp, '\n') < 0) +diff --git a/cups/testfile.c b/cups/testfile.c +index 4f8fbefaf4..97293f9399 100644 +--- a/cups/testfile.c ++++ b/cups/testfile.c +@@ -552,6 +552,17 @@ read_write_tests(int compression) /* I - Use compression? */ + off_t length; /* Length of file */ + static const char *partial_line = "partial line"; + /* Partial line */ ++ static const char * const values[] = /* cupsFileGet/PutConf values */ ++ { ++ "simple", ++ "'single-quoted value'", ++ "\"double-quoted value\"", ++ "value with # comment", ++ "value with \n newline", ++ "value with \r carriage return", ++ "value with \\ backslash", ++ "pathological\r\nvalue\\#with comment" ++ }; + + + /* +@@ -628,6 +639,27 @@ read_write_tests(int compression) /* I - Use compression? */ + status ++; + } + ++ /* ++ * cupsFilePutConf() ++ */ ++ ++ fputs("cupsFilePutConf(): ", stdout); ++ for (i = 0; i < (int)(sizeof(values) / sizeof(values[0])); i ++) ++ { ++ if (cupsFilePutConf(fp, "TestConf", values[i]) < 0) ++ break; ++ } ++ ++ if (i >= (int)(sizeof(values) / sizeof(values[0]))) ++ { ++ puts("PASS"); ++ } ++ else ++ { ++ printf("FAIL (%s)\n", strerror(errno)); ++ status ++; ++ } ++ + /* + * cupsFilePutChar() + */ +@@ -684,11 +716,11 @@ read_write_tests(int compression) /* I - Use compression? */ + + fputs("cupsFileTell(): ", stdout); + +- if ((length = cupsFileTell(fp)) == 81933283) ++ if ((length = cupsFileTell(fp)) == 81933542) + puts("PASS"); + else + { +- printf("FAIL (" CUPS_LLFMT " instead of 81933283)\n", CUPS_LLCAST length); ++ printf("FAIL (" CUPS_LLFMT " instead of 81933542)\n", CUPS_LLCAST length); + status ++; + } + +@@ -766,7 +798,7 @@ read_write_tests(int compression) /* I - Use compression? */ + + linenum = 1; + +- fputs("cupsFileGetConf(): ", stdout); ++ fputs("cupsFileGetConf(TestLine): ", stdout); + + for (i = 0, value = NULL; i < 1000; i ++) + if (!cupsFileGetConf(fp, line, sizeof(line), &value, &linenum)) +@@ -789,6 +821,27 @@ read_write_tests(int compression) /* I - Use compression? */ + status ++; + } + ++ fputs("cupsFileGetConf(TestConf): ", stdout); ++ ++ for (i = 0; i < (int)(sizeof(values) / sizeof(values[0])); i ++) ++ { ++ if (!cupsFileGetConf(fp, line, sizeof(line), &value, &linenum)) ++ { ++ printf("FAIL (%s)\n", strerror(errno)); ++ status ++; ++ break; ++ } ++ else if (_cups_strcasecmp(line, "TestConf") || !value || strcmp(value, values[i])) ++ { ++ printf("FAIL (Expected 'TestConf %s', got '%s %s')\n", values[i], line, value); ++ status ++; ++ break; ++ } ++ } ++ ++ if (i >= (int)(sizeof(values) / sizeof(values[0]))) ++ puts("PASS"); ++ + /* + * cupsFileGetChar() + */ +@@ -869,11 +922,11 @@ read_write_tests(int compression) /* I - Use compression? */ + + fputs("cupsFileTell(): ", stdout); + +- if ((length = cupsFileTell(fp)) == 81933283) ++ if ((length = cupsFileTell(fp)) == 81933542) + puts("PASS"); + else + { +- printf("FAIL (" CUPS_LLFMT " instead of 81933283)\n", CUPS_LLCAST length); ++ printf("FAIL (" CUPS_LLFMT " instead of 81933542)\n", CUPS_LLCAST length); + status ++; + } + diff --git a/cups.spec b/cups.spec index 0545e5f..d5028fc 100644 --- a/cups.spec +++ b/cups.spec @@ -22,7 +22,7 @@ Summary: CUPS printing system Name: cups Epoch: 1 Version: 2.4.10 -Release: 17%{?dist} +Release: 18%{?dist} # backend/failover.c - BSD-3-Clause # cups/md5* - Zlib # scheduler/colorman.c - Apache-2.0 WITH LLVM-exception AND BSD-2-Clause @@ -129,6 +129,20 @@ Patch1021: 0001-conf.c-Fix-stopping-scheduler-on-unknown-directive.patch Patch1022: 0001-scheduler-Fix-possible-use_after_free-in-cupsdReadCl.patch # RHEL-154276 endless poll loop in http_write when POLLHUP is returned Patch1023: 0001-tls-gnutls.c-Do-not-check-for-errno-after-I-O-operat.patch +# RHEL-177945 CVE-2026-34980 cups: control character injection in option values +# https://issues.redhat.com/browse/RHEL-177945 +# https://github.com/OpenPrinting/cups/commit/8d0f51cac24cb5bf949c5b6a221e51a150d982e3 +Patch1024: 0001-Filter-out-control-characters-from-option-values.patch +# https://github.com/OpenPrinting/cups/commit/c5ce534c03d0698509f3eb7ff90ca41610625570 +# Hardening for the same advisory - escape newlines/CR/backslashes in +# cupsFileGetConf/PutConf (requires root alone, but reachable via CVE-2026-34980) +# dropped CHANGES.md changes, dropped copyright date change +Patch1025: 0001-Updated-cupsFileGetConf-and-cupsFilePutConf-to-escap.patch +# https://github.com/OpenPrinting/cups/commit/da0ff58c041f7ee129c3c2c72fb14df1f1e4069a +# dropped copyright date change in test/5.5-lp.sh +Patch1026: 0001-Fix-get_options-regression-Issue-1532.patch +# https://github.com/OpenPrinting/cups/commit/3f2bdc293243bca938c6de23ba50e6d783189629 +Patch1027: 0001-Fix-filter-PPD-keyword-processing-Issue-1562.patch ##### Patches removed because IMHO they aren't no longer needed @@ -405,6 +419,11 @@ to CUPS daemon. This solution will substitute printer drivers and raw queues in %patch -P 1022 -p1 -b .osh-use-after-free # RHEL-154276 endless poll loop in http_write when POLLHUP is returned %patch -P 1023 -p1 -b .http-endless-poll-loop +# RHEL-177945 CVE-2026-34980 +%patch -P 1024 -p1 -b .cve-2026-34980-control-chars +%patch -P 1025 -p1 -b .cve-2026-34980-conf-escape +%patch -P 1026 -p1 -b .cve-2026-34980-get-options +%patch -P 1027 -p1 -b .cve-2026-34980-ppd-keyword # Log to the system journal by default (bug #1078781, bug #1519331). @@ -876,6 +895,9 @@ rm -f %{cups_serverbin}/backend/smb %{_mandir}/man7/ippeveps.7.gz %changelog +* Mon Jun 22 2026 RHEL Packaging Agent - 1:2.4.10-18 +- RHEL-177945 CVE-2026-34980 cups: control character injection in option values + * Mon Mar 09 2026 Zdenek Dohnal - 1:2.4.10-17 - RHEL-154276 endless poll loop in http_write when POLLHUP is returned