pcp/pcp-7.1.5-pmproxy-logger-meta-network.patch
Jan Kurik 4d55c1806b
Backport remaining private-pcp security hardening fixes for 7.1.5-6
Add patches for pmieconf/pmlogmv command injection, libpcp PDU decode
OOB guards, timezone validation, pmdaroot peer credentials, and pmproxy
REST CERT_REQD and logger authentication hardening.

Resolves: RHEL-213756 CVE-2026-16531
Resolves: RHEL-213746 CVE-2026-16530
Resolves: RHEL-213726 CVE-2026-16529
Resolves: RHEL-213717 CVE-2026-16527
Resolves: RHEL-213692 CVE-2026-16526
Resolves: RHEL-213661 CVE-2026-16524
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 12:10:43 +02:00

184 lines
5.6 KiB
Diff

From bf898b50ef Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] qa: add network-layer regression tests for pmproxy /logger/meta
Adapted from PoC exploits provided by TIM Security Red Team to test
the pmproxy HTTP streaming path that pducrash.c does not exercise.
qa/2109 starts pmproxy, creates a valid archive via POST /logger/label,
then POSTs three malformed metadata records to /logger/meta:
- Vuln 1: TYPE_INDOM with stridx=0x7FFFFFFF (OOB pointer deref)
- Vuln 9: TYPE_LABEL with hdr.len=13 (rlen=1, undersized for header)
- Vuln 10: TYPE_INDOM_DELTA with hdr.len=13 (OOB numinst read)
Verifies pmproxy handles each gracefully and remains responsive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/qa/2109 b/qa/2109
new file mode 100755
index 0000000000..6423286d97
--- /dev/null
+++ b/qa/2109
@@ -0,0 +1,145 @@
+#!/bin/sh
+# PCP QA Test No. 2109
+# Verify pmproxy /logger/meta endpoint rejects malformed metadata
+# records without crashing (network-layer regression for vulns 1, 9, 10)
+#
+# Adapted from PoC exploits provided by TIM Security Red Team.
+#
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
+#
+
+seq=`basename $0`
+echo "QA output created by $seq"
+
+# get standard environment, filters and checks
+. ./common.product
+. ./common.filter
+. ./common.check
+
+which curl >/dev/null 2>&1 || _notrun "no curl executable installed"
+which python3 >/dev/null 2>&1 || _notrun "no python3 executable installed"
+
+_cleanup()
+{
+ [ -n "$__pid" ] && kill $__pid 2>/dev/null
+ wait $__pid 2>/dev/null
+ cd $here
+ $sudo rm -rf $tmp $tmp.*
+}
+
+status=0 # success is the default!
+__pid=""
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+__port=`_find_free_port`
+mkdir -p $tmp.archdir
+cat >$tmp.conf <<EOF
+[pmproxy]
+pcp.enabled = true
+http.enabled = true
+[discover]
+enabled = false
+EOF
+PCP_REMOTE_ARCHIVE_DIR=$tmp.archdir $PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log -c $tmp.conf &
+__pid=$!
+sleep 2
+
+if ! kill -0 $__pid 2>/dev/null; then
+ echo "FAIL: pmproxy did not start"
+ exit
+fi
+
+_filter_archive_id()
+{
+ sed -e 's/archive created: [0-9]*/archive created: ARCHIVE_ID/'
+}
+
+# real QA test starts here
+python3 - $__port <<'PYEOF' | _filter_archive_id
+import http.client, json, struct, sys, time
+
+PORT = int(sys.argv[1])
+
+def post(path, body):
+ c = http.client.HTTPConnection("localhost", PORT, timeout=10)
+ c.request("POST", path, body=body,
+ headers={"Content-Type": "application/octet-stream"})
+ try:
+ r = c.getresponse()
+ data = r.read()
+ c.close()
+ return r.status, data
+ except http.client.RemoteDisconnected:
+ return 0, b"connection lost"
+
+# create a valid archive via POST /logger/label
+PM_LOG_MAGIC = 0x50052600
+LABEL_MAGIC = PM_LOG_MAGIC | 0x02
+LABEL_V2_SIZE = 4 + 4 + 8 + 4 + 64 + 40
+TOTAL_SIZE = LABEL_V2_SIZE + 8
+
+hostname = b'testhost\x00' + b'\x00' * (64 - 9)
+timezone = b'UTC\x00' + b'\x00' * (40 - 4)
+
+label = struct.pack(">I", LABEL_MAGIC)
+label += struct.pack(">i", 1337)
+label += struct.pack(">ii", int(time.time()), 0)
+label += struct.pack(">i", 0)
+label += hostname + timezone
+
+label_body = struct.pack(">i", TOTAL_SIZE) + label + struct.pack(">i", TOTAL_SIZE)
+
+status, resp = post("/logger/label", label_body)
+if status != 200:
+ print(f"FAIL: /logger/label returned HTTP {status}")
+ sys.exit(0)
+archive_id = json.loads(resp)["archive"]
+print(f"archive created: {archive_id}")
+
+# --- Vuln 1: TYPE_INDOM with OOB stridx ---
+print("=== vuln 1: TYPE_INDOM with OOB stridx ===")
+TYPE_INDOM = 5
+body = (
+ struct.pack(">II", 0, 0) + # sec[2]
+ struct.pack(">I", 0) + # nsec
+ struct.pack(">I", 1) + # indom
+ struct.pack(">I", 1) + # numinst=1
+ struct.pack(">I", 0) + # instlist[0]
+ struct.pack(">I", 0x7FFFFFFF) + # stridx[0] OOB
+ b'\x00'
+)
+total = 8 + len(body) + 4
+record = struct.pack(">II", total, TYPE_INDOM) + body + struct.pack(">I", total)
+status, resp = post(f"/logger/meta/{archive_id}", record)
+print(f"pmproxy responded (not crashed)")
+
+# --- Vuln 9: TYPE_LABEL with rlen=1 (undersized) ---
+print("=== vuln 9: TYPE_LABEL with rlen=1 ===")
+TYPE_LABEL = 7
+hdr_len = 13
+record = struct.pack(">ii", hdr_len, TYPE_LABEL) + b'\x00' + struct.pack(">i", hdr_len)
+status, resp = post(f"/logger/meta/{archive_id}", record)
+print(f"pmproxy responded (not crashed)")
+
+# --- Vuln 10: TYPE_INDOM_DELTA with rlen=1 (OOB numinst) ---
+print("=== vuln 10: TYPE_INDOM_DELTA with rlen=1 ===")
+TYPE_INDOM_DELTA = 6
+hdr_len = 13
+record = struct.pack(">ii", hdr_len, TYPE_INDOM_DELTA) + b'\x00' + struct.pack(">i", hdr_len)
+status, resp = post(f"/logger/meta/{archive_id}", record)
+print(f"pmproxy responded (not crashed)")
+
+# verify pmproxy is still alive after all malformed records
+try:
+ c = http.client.HTTPConnection("localhost", PORT, timeout=5)
+ c.request("GET", "/pmapi/ping")
+ r = c.getresponse()
+ r.read()
+ c.close()
+ print("=== pmproxy still responding after all tests ===")
+except Exception:
+ print("FAIL: pmproxy is not responding")
+PYEOF
+
+# success, all done
+exit
diff --git a/qa/2109.out b/qa/2109.out
new file mode 100644
index 0000000000..9918d5f025
--- /dev/null
+++ b/qa/2109.out
@@ -0,0 +1,9 @@
+QA output created by 2109
+archive created: ARCHIVE_ID
+=== vuln 1: TYPE_INDOM with OOB stridx ===
+pmproxy responded (not crashed)
+=== vuln 9: TYPE_LABEL with rlen=1 ===
+pmproxy responded (not crashed)
+=== vuln 10: TYPE_INDOM_DELTA with rlen=1 ===
+pmproxy responded (not crashed)
+=== pmproxy still responding after all tests ===