Before you start#
Review sockets and the network stack, userspace process control, and desktop application activation. This chapter pins OpenPrinting CUPS v2.4.10 to commit f3e5cc069c25647619aba8e0aefaa1969d71447e. All jobs and bytes below are synthetic; no print service, queue, printer or document is accessed.
Learning goals#
Decode a small IPP request, trace parser and scheduler failure branches, distinguish client, scheduler, filter and backend lifetimes, and analyze a denied job without printing or discovering devices.
Source walkthrough#
- Client
Encodes IPP request and attributes
- Scheduler
Parses, authenticates and tracks job state
- Pipeline
Selected filters and backend handle data
- Printer
Receives output using a configured protocol
CUPS includes client libraries and a scheduler. A client can submit Internet Printing Protocol, or IPP, requests using HTTP transport; the scheduler parses them, applies policy and tracks printers and jobs. Depending on configuration and format, processing may involve filters and a backend that sends data to a printer. These are separate processes and trust boundaries. A user-facing print dialog, a scheduler job object and a physical printer are not one object, and acceptance of a job does not prove that paper was produced.
The first excerpt is from CUPS 2.4.10 cups/ipp.c, ippReadIO. Its file notice states Apache-2.0. Surrounding debug output and state-machine parsing are omitted:
if (!src || !ipp)
return (IPP_STATE_ERROR);
The parser rejects a missing data source or IPP message object before continuing. It then obtains a temporary read buffer and returns an error if allocation fails. Later code uses the message state, callback, lengths and tags to decode data. A caller must handle incomplete input and parser errors in the applicable path; successfully receiving some HTTP bytes is not proof of a complete valid IPP message. The message object and temporary parser buffer have separate lifetimes.
The second excerpt is from CUPS 2.4.10 scheduler/ipp.c, within the local-printer creation path, also Apache-2.0. Surrounding policy checks are omitted:
if (!httpAddrLocalhost(httpGetAddress(con->http)))
{
send_ipp_status(con, IPP_STATUS_ERROR_FORBIDDEN, _("Only local users can create a local printer."));
return;
}
The scheduler checks the connection address and returns a forbidden IPP status for this operation when it is not local. This is a specific administrative operation, not a universal rule that every remote print job is forbidden or that every local request is authorized. Later policy and authentication checks are separate. A client must inspect the IPP response status and request identifier; an HTTP connection alone does not establish that an operation succeeded.
Message framing and attributes#
IPP messages begin with a version, a two-byte operation or status code and a four-byte request ID in network byte order. Requests then carry delimiter tags and typed attributes. An attribute has a value tag, name length, name bytes, value length and value bytes; repeated values and collections add further rules. An end-of-attributes tag marks the end of the attribute sequence. Additional document data can follow for operations that carry it. HTTP framing and IPP framing are related layers but have different fields and errors.
In cups/ipp.c, ippReadIO, CUPS 2.4.10, Apache-2.0 (surrounding debug output and attribute parsing omitted; the comment block is elided):
if ((*cb)(src, buffer, 8) < 8)
{
DEBUG_puts("1ippReadIO: Unable to read header.");
goto rollback;
}
/* ... */
ipp->request.any.version[0] = buffer[0];
ipp->request.any.version[1] = buffer[1];
ipp->request.any.op_status = (buffer[2] << 8) | buffer[3];
ipp->request.any.request_id = (buffer[4] << 24) | (buffer[5] << 16) | (buffer[6] << 8) | buffer[7];
The header branch requests exactly eight bytes, then stores the version, the operation or status word and the request ID at fixed positions. It does not show attribute decoding, whether an operation is valid, or whether a request ID is unique; those are later steps and separate checks.
Consider this synthetic header and one illustrative operation attribute, expressed as hex bytes:
01 01 00 02 00 00 00 01
01 47 00 12 61 74 74 72 69 62 75 74 65 73 2d 63 68 61 72 73 65 74
00 05 75 74 66 2d 38 03
The first two bytes select IPP version 1.1; 00 02 is Print-Job and the final four bytes are request ID 1. The next 01 starts the operation-attributes group. Tag 47 is the charset syntax; 00 12 is the 18-byte name length for attributes-charset; 00 05 introduces the five bytes spelling utf-8; 03 ends attributes. RFC 8010 requires attributes-charset and attributes-natural-language as the first two operation attributes. This fragment omits the latter, printer URI, document-format and document data, so it is deliberately incomplete and must not be described as a valid Print-Job request. See RFC 8010 §§3.1, 3.2, 3.4 and RFC 8011 §4.2.1.
Synthetic teaching data, not a device capture.
Select a field to connect its position, bytes, and meaning.
9 bytes in file order. Offsets start at zero.
Selected: version. IPP version 1.1, major then minor.
Read the byte table
| Offset | Field | Hex bytes | Interpretation |
|---|---|---|---|
| 0 | version | 01 01 | IPP version 1.1, major then minor |
| 2 | operation | 00 02 | Print-Job operation code, network byte order |
| 4 | request ID | 00 00 00 01 | Correlation value copied into the response; not a job ID |
| 8 | attribute group | 01 | Operation-attributes group tag begins the attribute sequence |
A malformed name length can claim more bytes than remain in the message. A parser must not read beyond its available buffer or treat following data as a valid attribute without validating lengths and state. Partial transport reads can also split a valid length or value across callbacks, depending on the library's input contract. A short read, allocation failure, malformed length and unsupported operation are different failures. ippReadIO maintains state in the ipp_t object and uses an I/O callback; exact partial-read behavior depends on the callback and blocking mode, so do not infer that every short callback is treated identically.
The request ID is a 32-bit correlation value supplied by the client and copied into the response; it is not a globally unique job ID or a permission token. A job ID is a separate integer attribute allocated in the printer's job namespace. IPP attribute groups and tags give meaning to values; treating the whole payload as UTF-8 text loses binary lengths and types. A decoder should preserve operation, response status, group, name, tag and value separately, and reject malformed data rather than guessing. RFC 8010 §3.4.4, RFC 8011 §4.3.2.
Scheduler, jobs, and authority#
A scheduler connection object represents one client connection and its request/response state. Authentication, authorization, printer policy and job ownership are evaluated under the configured service and operation. A job object can hold an identifier, owner, destination, state and references to spooled data. A client disconnect does not automatically erase an accepted job, and a numeric job ID does not identify the client connection that created it. Job state changes asynchronously as processing and device communication proceed.
A synthetic job can move from pending to processing and then completed, stopped, canceled or aborted depending on actual outcomes. A server that accepts a submission may later fail to run a filter or reach the device. Conversely, a canceled client request need not cancel a job already accepted by the scheduler. A meaningful report records the operation response, returned job identifier, later job state and relevant service logs, with authorization to inspect them. This lesson supplies no actual job state or service logs.
The cited local-printer creation branch illustrates a denial before the subsequent policy checks. If a remote client asks to create a local printer and fails the source's localhost test, the scheduler sends IPP_STATUS_ERROR_FORBIDDEN and returns. For a different operation or a different connection, other conditions apply. A localhost source address alone is not a complete authentication policy, and a network address does not identify the human who owns a job. Denial is a valid result; changing host policy or using elevated privileges to force success is outside this exercise.
- ClientScheduler IPP handler
Local-printer create request, request ID 1
- Scheduler IPP handlerAddress check
Test connection address with httpAddrLocalhost
- Address checkScheduler IPP handler
Return. Not localhost
- Scheduler IPP handlerClient
Return. IPP_STATUS_ERROR_FORBIDDEN, request ID 1
Time runs downward. Dashed arrows show returns.
Read the text version
- Client → Scheduler IPP handler: Local-printer create request, request ID 1
- Scheduler IPP handler → Address check: Test connection address with httpAddrLocalhost
- Address check → Scheduler IPP handler (return): Not localhost
- Scheduler IPP handler → Client (return): IPP_STATUS_ERROR_FORBIDDEN, request ID 1
Filters, backends, and data ownership#
A job can be spooled to files managed by the scheduler. A filter pipeline may convert the submitted format to another format expected by the selected backend or printer. Filter availability, executable versions, configuration and policy influence the path. A backend communicates with a destination such as a network printer or a locally attached device; it can fail after the scheduler accepted the job. The exact process chain and privileges are deployment-specific. CUPS, libcups, filters, Ghostscript and USB or network backends are distinct components with their own versions and licenses.
When a scheduler starts child processes connected by pipes, each process must close unused pipe ends. If a filter retains a write end after its intended output is complete, the downstream reader may not receive EOF. If a backend exits early, the writer can encounter a broken pipe. A child process must be waited for and its exit status interpreted; a successful fork or open does not prove successful printing. Descriptors can be duplicated and their numeric values are process-local. A spool file can contain private user data and must be protected and cleaned up according to service policy.
Driverless IPP Everywhere uses standardized IPP capabilities and supported formats, reducing the need for device-specific driver configuration in compatible deployments. Legacy paths can use PPD descriptions and filters. Neither label proves that every printer accepts every document format or that no conversion happens. Discovery mechanisms such as Avahi are separate projects and can reveal network devices; they are not needed for the source-only exercise and are not contacted here. A backend's access to a device is subject to host permissions and security policy, not granted merely by an IPP attribute.
Worked denied and malformed cases#
Assume an authored client sends a request with ID 1 to create a local printer from a nonlocal address. In the cited scheduler branch, the address test fails, a forbidden IPP status is sent and the handler returns. The client should correlate the response with ID 1 and not invent a created printer or job. If the connection fails before a response is received, the client has an unknown outcome until it obtains reliable state through an authorized mechanism; an absent response is not equivalent to a forbidden response.
For a different synthetic request, suppose the attribute name length says 18 but only three name bytes remain before the transport closes. The parser lacks the stated bytes. It must not read a complete 18-byte name from that fragment. The exact CUPS parser result depends on its callback and state, but the case illustrates why length validation and transport state must be considered together. If the name is complete but a mandatory attribute is absent, the message can be structurally parseable yet semantically invalid for its operation. Parsing and authorization are separate boundaries.
A useful job-state table contains the client request ID, scheduler job ID if one was returned, owner, spool reference, current state and last observed error. The request ID and job ID must not be substituted for one another. Add a process and fd ledger if a filter is launched: parent, filter and backend each own particular descriptors, and every unused duplicate can affect EOF. A synthetic table is not evidence that any local printer exists.
Security and interpretation#
IPP can carry user names, document names, job attributes and document bytes. Logs and spool files may contain personal or confidential information. A diagnostic should use self-authored data in an isolated environment and avoid copying real jobs or credentials. A printer, network service or filter is another component with its own attack surface and trust assumptions. Parsing a PDF or PostScript file with a filter is code execution in a complex interpreter and is not part of this lesson.
HTTP status and IPP status have different meanings. An HTTP transport can succeed while an IPP operation returns an error. A scheduler can accept a job while the backend later fails. Physical output can differ from the requested format even when transport succeeded. Distinguishing those stages prevents a client connection problem from being misreported as a printer defect, or a denied operation from being called a successful job. No one layer's success certifies the entire pipeline.
Interpreting a stalled pipeline#
Imagine a scheduler has accepted an authored job and started a filter and backend in a hypothetical isolated environment. The filter reports that it wrote its output, but the backend remains blocked waiting for more bytes. One hypothesis is that an extra copy of the pipe's write end remains open in the scheduler or another child. Another is that the backend is waiting on a network operation after receiving EOF. Those explanations predict different observations: an fd ownership ledger can establish whether any writer remains, whereas a backend state and authorized network trace can show whether the reader has already advanced beyond input. Neither follows solely from a pending job label.
A controlled test would use a tiny self-authored input and a disposable scheduler, replace the destination with an owned test backend, and compare normal closure with a deliberately retained duplicate descriptor. The owner would close the duplicate, terminate only the test processes and remove their private spool afterward. Such a test could show an EOF dependency in that setup, but it would not validate a physical printer, an arbitrary filter or a production service. This chapter does not start that environment. The source-only alternative records the same two hypotheses and identifies the missing observations, which is sufficient to practice the reasoning without accessing a printer or a real user's document.
Practice#
This pure-memory Python exercise encodes and decodes only an authored IPP-style header. It contacts no service and does not interpret a document:
python3 - <<'PY'
import struct
header = struct.pack('!BBHI', 1, 1, 2, 1)
print('header hex:', header.hex())
print('version, operation, request id:', struct.unpack('!BBHI', header))
try:
struct.unpack('!BBHI', header[:-1])
except struct.error as exc:
print('truncated header rejected:', type(exc).__name__)
PY
Expected hex is 0101000200000001; the tuple is version 1.1, operation 2 and request ID 1. Removing one byte raises a length error in this Python model. It does not test CUPS, HTTP framing, full IPP validation or printing. If Python is unavailable, parse the supplied bytes by hand.
For the source-only alternative, annotate every field of the supplied byte fragment and write a job table for the nonlocal create-printer denial. Add the truncated-name counterexample and distinguish transport failure, parser error and scheduler authorization. No lp, lpr, lpstat, printer discovery or service connection is needed.
Knowledge check#
- Why is a request ID different from a job ID?
- What does the
ippReadIOnull check establish? - Does the quoted localhost check forbid every remote print job?
- Why can an accepted job still fail before physical output?
- Why can an unused pipe descriptor delay pipeline completion?
Answers#
- The request ID correlates a protocol request and response; a job ID names a scheduler job under its own lifecycle. One connection can carry operations concerning different jobs.
- A missing source or IPP object returns a parser error before later parsing. It does not validate a complete message or authorize its operation.
- No. It is a branch for creating a local printer. Other operations and further policy checks have their own conditions.
- A filter, backend, device or later policy step can fail after acceptance. Acceptance is not evidence of physical output.
- A reader sees EOF only after all relevant write ends are closed. A leaked duplicate can keep the pipe open after the intended writer exits.
Checkpoint#
Submit the annotated IPP header, denied-operation timeline and filter/backend fd ledger. Pass when HTTP success, IPP status, job state and physical output are distinct, and malformed input is not presented as a valid job. Revisit the cited branches if a client request ID is treated as a printer job ID. This is not certification.