Summary
Commit 399ad15e09 ("Filter out control characters from option values", merged 2026-03-31) — the public fix for CVE-2026-34979 / GHSA-6qxf-7jx6-86fh — introduces a regression in scheduler/job.c::get_options(). The new filtering loop forgets to advance the read pointer in the whitespace branch and provides no fall-through branch for control bytes. The result is an infinite loop with two distinct memory-safety primitives in the same function the original CVE was supposed to harden.
Per the project's SECURITY.md ("security vulnerabilities that only affect unreleased code [should be reported] as regular bugs"), I'm filing this as a normal issue. The regression is on master only — no released CUPS version is affected yet, but every distro and downstream that builds from upstream after 2026-03-31 will inherit it.
Vulnerable code
Pre-patch loop:
for (valptr = attr->values[i].string.text; *valptr;)
{
if (strchr(" \t\n\\\'\"", *valptr))
*optptr++ = '\\';
*optptr++ = *valptr++; /* valptr ALWAYS advances */
}
Post-patch loop, current master, scheduler/job.c lines 4147-4164:
for (valptr = attr->values[i].string.text; *valptr;)
{
if (isspace(*valptr & 255))
{
*optptr++ = ' '; /* BUG 1A: writes one byte but valptr is NOT advanced */
}
else if ((*valptr & 255) >= ' ' && *valptr != 0x7f)
{
if (strchr("\\\'\"", *valptr))
*optptr++ = '\\';
*optptr++ = *valptr++; /* only branch that advances valptr */
}
/* BUG 1B: no else for control bytes 0x01-0x08, 0x0e-0x1f, 0x7f */
}
Two distinct primitives
Bug 1A — whitespace infinite loop + heap-buffer-overflow WRITE. The loop condition is *valptr (re-evaluated every iteration). When valptr points at any whitespace byte (0x20 0x09 0x0a 0x0b 0x0c 0x0d), the first branch writes 0x20 to *optptr++, falls through to the loop condition, finds the same whitespace byte still there, and re-enters the same branch. optptr is incremented every iteration; valptr is not. The function therefore writes constant 0x20 past the end of the options heap allocation that ipp_length() sized for the original-string length, until the allocator faults or cupsd is killed. The trigger is a single ASCII space, so a normal job-name like "My Document" is enough — the regression breaks ordinary print workflows.
Bug 1B — control-byte infinite spin (CPU DoS). When valptr instead points at a byte in {0x01..0x08, 0x0e..0x1f, 0x7f}, neither branch matches, the loop body becomes a no-op, and the loop spins on the same unchanging byte. cupsd worker thread pinned at 100% CPU forever.
Both primitives are reachable from any IPP Print-Job / Send-Document / Create-Job request because every string-typed IPP attribute (job-name, document-name, requesting-user-name, attributes-charset, attributes-natural-language, every string-typed printer-attribute echoed back into the job ticket) is funneled through get_options().
Steps to reproduce
- Build
cupsd from OpenPrinting/cups master:
git clone https://github.com/OpenPrinting/cups
cd cups && ./configure --enable-debug && make && sudo make install && sudo cupsd
- Install a dummy raw queue:
sudo lpadmin -p test -E -v file:///tmp/test.out -m raw
- Submit one Print-Job whose
job-name contains a single space:
echo hello | lp -d test -t "My Document"
Expected results
cupsd accepts the job, builds the filter argv, escapes / replaces the space, spawns the filter chain, returns the job ID. CPU stays near zero. No memory growth.
Actual results
cupsd's job-processing thread enters get_options() at scheduler/job.c:4147, reads the space byte from attr->values[i].string.text, takes the if (isspace(*valptr & 255)) branch at line 4154, writes 0x20 to *optptr++, returns to the loop condition, and re-reads the same unchanged space byte. The loop never terminates. optptr is incremented every iteration; valptr never moves. Within milliseconds the process either:
- exhausts the
options heap allocation (ipp_length() reserved space for the original-string length only) and writes constant 0x20 into adjacent heap chunks until the allocator detects corruption and aborts — heap-buffer-overflow WRITE in a process running as root, or
- pins one CPU core at 100% indefinitely if the heap allocation happens to be large enough that the immediate corruption is not fatal —
cupsd worker hung, all subsequent print jobs blocked
Replacing the space character with any byte in {0x01..0x08, 0x0e..0x1f, 0x7f} reproduces only Bug 1B: pure CPU spin, because that byte matches neither branch and the loop body is silently empty.
Suggested patch
if (isspace(*valptr & 255))
{
*optptr++ = ' ';
+ valptr++;
}
else if ((*valptr & 255) >= ' ' && *valptr != 0x7f)
{
if (strchr("\\\'\"", *valptr))
*optptr++ = '\\';
*optptr++ = *valptr++;
}
+ else
+ {
+ valptr++; /* silently drop control characters per the patch's intent */
+ }
One valptr++ per missing branch. No public API change. Restores the original loop's per-iteration progress invariant.
Affected versions
OpenPrinting/cups master from commit 399ad15e09 (2026-03-31) onward — currently HEAD
- No released CUPS version is affected yet
apple-oss-distributions/cups is at tag cups-522 (Oct 2024) — predates the regression; their next sync from upstream will inherit it unless caught
Impact classification
- CWE-835 Loop with Unreachable Exit Condition (Infinite Loop) — Bug 1A and 1B
- CWE-787 Out-of-bounds Write — Bug 1A heap-write component
References
Summary
Commit
399ad15e09("Filter out control characters from option values", merged 2026-03-31) — the public fix for CVE-2026-34979 / GHSA-6qxf-7jx6-86fh — introduces a regression inscheduler/job.c::get_options(). The new filtering loop forgets to advance the read pointer in the whitespace branch and provides no fall-through branch for control bytes. The result is an infinite loop with two distinct memory-safety primitives in the same function the original CVE was supposed to harden.Per the project's SECURITY.md ("security vulnerabilities that only affect unreleased code [should be reported] as regular bugs"), I'm filing this as a normal issue. The regression is on
masteronly — no released CUPS version is affected yet, but every distro and downstream that builds from upstream after 2026-03-31 will inherit it.Vulnerable code
Pre-patch loop:
Post-patch loop, current
master,scheduler/job.clines 4147-4164:Two distinct primitives
Bug 1A — whitespace infinite loop + heap-buffer-overflow WRITE. The loop condition is
*valptr(re-evaluated every iteration). Whenvalptrpoints at any whitespace byte (0x200x090x0a0x0b0x0c0x0d), the first branch writes0x20to*optptr++, falls through to the loop condition, finds the same whitespace byte still there, and re-enters the same branch.optptris incremented every iteration;valptris not. The function therefore writes constant0x20past the end of theoptionsheap allocation thatipp_length()sized for the original-string length, until the allocator faults orcupsdis killed. The trigger is a single ASCII space, so a normal job-name like"My Document"is enough — the regression breaks ordinary print workflows.Bug 1B — control-byte infinite spin (CPU DoS). When
valptrinstead points at a byte in{0x01..0x08, 0x0e..0x1f, 0x7f}, neither branch matches, the loop body becomes a no-op, and the loop spins on the same unchanging byte.cupsdworker thread pinned at 100% CPU forever.Both primitives are reachable from any IPP
Print-Job/Send-Document/Create-Jobrequest because every string-typed IPP attribute (job-name,document-name,requesting-user-name,attributes-charset,attributes-natural-language, every string-typed printer-attribute echoed back into the job ticket) is funneled throughget_options().Steps to reproduce
cupsdfromOpenPrinting/cupsmaster:sudo lpadmin -p test -E -v file:///tmp/test.out -m rawjob-namecontains a single space:Expected results
cupsdaccepts the job, builds the filter argv, escapes / replaces the space, spawns the filter chain, returns the job ID. CPU stays near zero. No memory growth.Actual results
cupsd's job-processing thread entersget_options()atscheduler/job.c:4147, reads the space byte fromattr->values[i].string.text, takes theif (isspace(*valptr & 255))branch at line 4154, writes0x20to*optptr++, returns to the loop condition, and re-reads the same unchanged space byte. The loop never terminates.optptris incremented every iteration;valptrnever moves. Within milliseconds the process either:optionsheap allocation (ipp_length()reserved space for the original-string length only) and writes constant0x20into adjacent heap chunks until the allocator detects corruption and aborts — heap-buffer-overflow WRITE in a process running asroot, orcupsdworker hung, all subsequent print jobs blockedReplacing the space character with any byte in
{0x01..0x08, 0x0e..0x1f, 0x7f}reproduces only Bug 1B: pure CPU spin, because that byte matches neither branch and the loop body is silently empty.Suggested patch
if (isspace(*valptr & 255)) { *optptr++ = ' '; + valptr++; } else if ((*valptr & 255) >= ' ' && *valptr != 0x7f) { if (strchr("\\\'\"", *valptr)) *optptr++ = '\\'; *optptr++ = *valptr++; } + else + { + valptr++; /* silently drop control characters per the patch's intent */ + }One
valptr++per missing branch. No public API change. Restores the original loop's per-iteration progress invariant.Affected versions
OpenPrinting/cupsmasterfrom commit399ad15e09(2026-03-31) onward — currently HEADapple-oss-distributions/cupsis at tagcups-522(Oct 2024) — predates the regression; their next sync from upstream will inherit it unless caughtImpact classification
References
399ad15e09scheduler/job.clines 4147-4164