Skip to content

Regression in fix for get_options handling of special characters #1532

@mohammadmseet-hue

Description

@mohammadmseet-hue

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

  1. Build cupsd from OpenPrinting/cups master:
    git clone https://github.com/OpenPrinting/cups
    cd cups && ./configure --enable-debug && make && sudo make install && sudo cupsd
  2. Install a dummy raw queue:
    sudo lpadmin -p test -E -v file:///tmp/test.out -m raw
  3. 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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions