Backport PCP security CVE fixes and hardening for 6.3.7-11

Backport applicable private-pcp security fixes to PCP 6.3.7 for RHEL 9.9.
CVE-2026-16531 is not applicable because the pmproxy logger servlet is
absent in this release.

Resolves: RHEL-213747 CVE-2026-16530
Resolves: RHEL-213736 CVE-2026-16529
Resolves: RHEL-213711 CVE-2026-16527
Resolves: RHEL-213687 CVE-2026-16526
Resolves: RHEL-213658 CVE-2026-16524
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jan Kurik 2026-08-13 08:10:20 +02:00
parent 4f4f4d25c8
commit d6aade7578
No known key found for this signature in database
17 changed files with 4295 additions and 1 deletions

View File

@ -0,0 +1,41 @@
From b743fc5879 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp: fix OOB read in __pmDecodeInstance (CWE-125/195)
The __pmDecodeInstance() loop advances ip by the PDU alignment-padded
entry size after each instance. When namelen % 4 != 0, the padding
advance can push ip past pdu_end. The existing bounds check casts the
pointer difference to size_t: (size_t)(pdu_end - (char *)ip). When ip
is past pdu_end, this produces a negative ptrdiff_t that wraps to a
very large size_t, causing both bounds checks to silently pass.
Execution falls through to memcpy reading past the PDU buffer.
Fix: add an explicit signed pointer guard at the top of each loop
iteration — if ((char *)ip >= pdu_end) — before the size_t cast.
This ensures the subsequent unsigned comparison is always valid.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/src/libpcp/src/p_instance.c b/src/libpcp/src/p_instance.c
index 6881ff37ef..7d836e75a0 100644
--- a/src/libpcp/src/p_instance.c
+++ b/src/libpcp/src/p_instance.c
@@ -290,6 +290,13 @@ __pmDecodeInstance(__pmPDU *pdubuf, pmInResult **result)
pdu_used = (char *)&pp->rest[0];
for (i = j = 0; i < res->numinst; i++) {
ip = (instlist_t *)&pp->rest[j/sizeof(__pmPDU)];
+ if ((char *)ip >= pdu_end) {
+ if (pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: inst[%d] ip past pdu_end\n",
+ __FUNCTION__, i);
+ sts = PM_ERR_IPC;
+ goto badsts;
+ }
if (sizeof(instlist_t) - sizeof(ip->name) > (size_t)(pdu_end - (char *)ip)) {
if (pmDebugOptions.pdu) {
fprintf(stderr, "__pmDecodeInstance: PM_ERR_IPC: sizeof(instlist_t) %d - sizeof(name) %d > remainder %d\n",

View File

@ -0,0 +1,39 @@
From e512482e7d Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp: fix OOB read in __pmDecodeLabel via negative jsonoff (CWE-125)
The bounds check 'if (pdu_length < jsonoff + jsonlen)' uses signed
arithmetic. When jsonoff is negative (high bit set after ntohl) and
jsonlen is a small positive value, their sum wraps to a small positive
number, passing the check. The subsequent memcpy reads from
label_pdu + jsonoff, an address before the start of the PDU buffer.
Fix: reject negative jsonoff and jsonlen explicitly, then use unsigned
(size_t) arithmetic for the bounds check to prevent signed wraparound.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/src/libpcp/src/p_label.c b/src/libpcp/src/p_label.c
index 10a348c8f0..95dbc86455 100644
--- a/src/libpcp/src/p_label.c
+++ b/src/libpcp/src/p_label.c
@@ -446,10 +446,11 @@ __pmDecodeLabel(__pmPDU *pdubuf, int *ident, int *type, pmLabelSet **setsp, int
}
/* check JSON content fits within the PDU bounds */
- if (pdu_length < jsonoff + jsonlen) {
+ if (jsonoff < 0 || jsonlen < 0 ||
+ (size_t)jsonoff + (size_t)jsonlen > pdu_length) {
if (pmDebugOptions.pdu) {
- fprintf(stderr, "__pmDecodeLabel: PM_ERR_IPC: labelset[%d] pdu_length %d < jsonoff %d + jsonlen %d\n",
- i, (int)pdu_length, jsonoff, jsonlen);
+ fprintf(stderr, "%s: PM_ERR_IPC: labelset[%d] pdu_length %d < jsonoff %d + jsonlen %d\n",
+ __FUNCTION__, i, (int)pdu_length, jsonoff, jsonlen);
}
goto corrupt;
}

View File

@ -0,0 +1,247 @@
From 5366a546d6 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp: fix OOB read in __pmDecodeLogStatus (CWE-125)
For each of the six length-prefixed string fields in PDU_LOG_STATUS
(hostname, fqdn, timezone, zoneinfo for both pmcd and pmlogger),
strdup(p) was called before verifying that p+len falls within the
PDU buffer. strdup reads until a null byte, so a non-null-terminated
string causes reads past the PDU boundary into adjacent heap memory.
Fix: for all six fields, move the p+len > pduend bounds check before
the string copy, and replace strdup(p) with strndup(p, len) to
respect the declared length regardless of null terminator presence.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/src/libpcp/src/p_lstatus.c b/src/libpcp/src/p_lstatus.c
index d9053b5d1a..099ef7b5ff 100644
--- a/src/libpcp/src/p_lstatus.c
+++ b/src/libpcp/src/p_lstatus.c
@@ -265,157 +265,157 @@ __pmDecodeLogStatus(__pmPDU *pdubuf, __pmLoggerStatus **result)
if (len == 0)
lsp->pmcd.hostname = NULL;
else {
- if (len > PM_MAX_HOSTNAMELEN) {
- /* cannot be longer than hostname in archive label */
+ if (len < 0 || len > PM_MAX_HOSTNAMELEN) {
+ /* cannot be negative or longer than hostname in archive label */
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.hostname too long (%d)\n", len);
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.hostname (%d)\n", len);
__pmFreeLogStatus(lsp, 1);
return PM_ERR_IPC;
}
- if ((lsp->pmcd.hostname = strdup(p)) == NULL) {
+ if (p + len > pduend) {
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.hostname data[%ld] > PDU len (%d)\n",
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ if ((lsp->pmcd.hostname = strndup(p, len)) == NULL) {
sts = -oserror();
pmNoMem("__pmDecodeLogStatus: pmcd.hostname", len, PM_RECOV_ERR);
__pmFreeLogStatus(lsp, 1);
return sts;
}
p += len;
- if (p > pduend) {
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.hostname data[%ld] > PDU len (%d)\n",
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
- __pmFreeLogStatus(lsp, 1);
- return PM_ERR_IPC;
- }
}
len = ntohl(pp->pmcd_fqdn_len);
if (len == 0)
lsp->pmcd.fqdn = NULL;
else {
- if (len > PM_MAX_HOSTNAMELEN) {
- /* cannot be longer than hostname in archive label */
+ if (len < 0 || len > PM_MAX_HOSTNAMELEN) {
+ /* cannot be negative or longer than hostname in archive label */
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.fqdn too long (%d)\n", len);
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.fqdn (%d)\n", len);
__pmFreeLogStatus(lsp, 1);
return PM_ERR_IPC;
}
- if ((lsp->pmcd.fqdn = strdup(p)) == NULL) {
+ if (p + len > pduend) {
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.fqdn data[%ld] > PDU len (%d)\n",
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ if ((lsp->pmcd.fqdn = strndup(p, len)) == NULL) {
sts = -oserror();
pmNoMem("__pmDecodeLogStatus: pmcd.fqdn", len, PM_RECOV_ERR);
__pmFreeLogStatus(lsp, 1);
return sts;
}
p += len;
- if (p > pduend) {
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.fqdn data[%ld] > PDU len (%d)\n",
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
- __pmFreeLogStatus(lsp, 1);
- return PM_ERR_IPC;
- }
}
len = ntohl(pp->pmcd_timezone_len);
if (len == 0)
lsp->pmcd.timezone = NULL;
else {
- if (len > PM_MAX_TIMEZONELEN) {
- /* cannot be longer than timezone in archive label */
+ if (len < 0 || len > PM_MAX_TIMEZONELEN) {
+ /* cannot be negative or longer than timezone in archive label */
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.timezone too long (%d)\n", len);
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.timezone (%d)\n", len);
__pmFreeLogStatus(lsp, 1);
return PM_ERR_IPC;
}
- if ((lsp->pmcd.timezone = strdup(p)) == NULL) {
+ if (p + len > pduend) {
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.timezone data[%ld] > PDU len (%d)\n",
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ if ((lsp->pmcd.timezone = strndup(p, len)) == NULL) {
sts = -oserror();
pmNoMem("__pmDecodeLogStatus: pmcd.timezone", len, PM_RECOV_ERR);
__pmFreeLogStatus(lsp, 1);
return sts;
}
p += len;
- if (p > pduend) {
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.timezone data[%ld] > PDU len (%d)\n",
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
- __pmFreeLogStatus(lsp, 1);
- return PM_ERR_IPC;
- }
}
len = ntohl(pp->pmcd_zoneinfo_len);
if (len == 0)
lsp->pmcd.zoneinfo = NULL;
else {
- if (len > PM_MAX_ZONEINFOLEN) {
- /* cannot be longer than zoneinfo in archive label */
+ if (len < 0 || len > PM_MAX_ZONEINFOLEN) {
+ /* cannot be negative or longer than zoneinfo in archive label */
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.zoneinfo too long (%d)\n", len);
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.zoneinfo (%d)\n", len);
__pmFreeLogStatus(lsp, 1);
return PM_ERR_IPC;
}
- if ((lsp->pmcd.zoneinfo = strdup(p)) == NULL) {
+ if (p + len > pduend) {
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.zoneinfo data[%ld] > PDU len (%d)\n",
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ if ((lsp->pmcd.zoneinfo = strndup(p, len)) == NULL) {
sts = -oserror();
pmNoMem("__pmDecodeLogStatus: pmcd.zoneinfo", len, PM_RECOV_ERR);
__pmFreeLogStatus(lsp, 1);
return sts;
}
p += len;
- if (p > pduend) {
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.zoneinfo data[%ld] > PDU len (%d)\n",
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
- __pmFreeLogStatus(lsp, 1);
- return PM_ERR_IPC;
- }
}
len = ntohl(pp->pmlogger_timezone_len);
if (len == 0)
lsp->pmlogger.timezone = NULL;
else {
if (len > PM_MAX_TIMEZONELEN) {
- /* cannot be longer than timezone in archive label */
+ /* cannot be negative or longer than timezone in archive label */
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
fprintf(stderr, "__pmDecodeLogStatusPM_ERR_IPC: : pmlogger.timezone too long (%d)\n", len);
__pmFreeLogStatus(lsp, 1);
return PM_ERR_IPC;
}
- if ((lsp->pmlogger.timezone = strdup(p)) == NULL) {
+ if (p + len > pduend) {
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmlogger.timezone data[%ld] > PDU len (%d)\n",
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ if ((lsp->pmlogger.timezone = strndup(p, len)) == NULL) {
sts = -oserror();
pmNoMem("__pmDecodeLogStatus: pmlogger.timezone", len, PM_RECOV_ERR);
__pmFreeLogStatus(lsp, 1);
return sts;
}
p += len;
- if (p > pduend) {
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.timezone data[%ld] > PDU len (%d)\n",
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
- __pmFreeLogStatus(lsp, 1);
- return PM_ERR_IPC;
- }
}
len = ntohl(pp->pmlogger_zoneinfo_len);
if (len == 0)
lsp->pmlogger.zoneinfo = NULL;
else {
- if (len > PM_MAX_ZONEINFOLEN) {
- /* cannot be longer than zoneinfo in archive label */
+ if (len < 0 || len > PM_MAX_ZONEINFOLEN) {
+ /* cannot be negative or longer than zoneinfo in archive label */
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.zoneinfo too long (%d)\n", len);
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmlogger.zoneinfo (%d)\n", len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ if (p + len > pduend) {
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmlogger.zoneinfo data[%ld] > PDU len (%d)\n",
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
__pmFreeLogStatus(lsp, 1);
return PM_ERR_IPC;
}
- if ((lsp->pmlogger.zoneinfo = strdup(p)) == NULL) {
+ if ((lsp->pmlogger.zoneinfo = strndup(p, len)) == NULL) {
sts = -oserror();
pmNoMem("__pmDecodeLogStatus: pmlogger.zoneinfo", len, PM_RECOV_ERR);
__pmFreeLogStatus(lsp, 1);
return sts;
}
p += len;
- if (p > pduend) {
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.zoneinfo data[%ld] > PDU len (%d)\n",
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
- __pmFreeLogStatus(lsp, 1);
- return PM_ERR_IPC;
- }
}
}
else if (version == LOG_PDU_VERSION2) {

View File

@ -0,0 +1,38 @@
From 7f42013d33 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp_web: add numinst overflow check in pmDiscoverDecodeMetaInDom (CWE-125/190)
Defense-in-depth for the __pmLogLoadInDom streaming path fix (commit 1).
When __pmLogLoadInDom is called with acp=NULL from the pmproxy discover
code, a garbage numinst value read from a too-small buffer could be
passed to calloc(numinst, sizeof(char *)), causing an integer overflow
in the allocation size.
Add explicit validation that numinst > 0 and does not overflow SIZE_MAX
before the calloc in pmDiscoverDecodeMetaInDom(). The primary fix
(rlen and numinst validation in __pmLogLoadInDom itself) prevents this
value from being garbage in the first place.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/src/libpcp_web/src/discover.c b/src/libpcp_web/src/discover.c
index 0c7565340d..7363755746 100644
--- a/src/libpcp_web/src/discover.c
+++ b/src/libpcp_web/src/discover.c
@@ -1954,6 +1954,11 @@ pmDiscoverDecodeMetaInDom(__int32_t *buf, int len, int type, __pmTimestamp *tsp,
*/
char **namelist;
int i;
+ if (lid.numinst <= 0 ||
+ (size_t)lid.numinst > SIZE_MAX / sizeof(char *)) {
+ __pmFreeLogInDom(&lid);
+ return -EINVAL;
+ }
namelist = (char **)malloc(lid.numinst * sizeof(char *));
if (namelist == NULL) {
pmNoMem("pmDiscoverDecodeMetaInDom", lid.numinst * sizeof(char *), PM_FATAL_ERR);

View File

@ -0,0 +1,64 @@
From ccd1bb1679 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp: fix OOB read in __pmLogLoadLabelSet (CWE-125)
__pmLogLoadLabelSet() reads timestamp, type, ident, and nsets fields
from tbuf at sequential offsets without checking that rlen is large
enough to contain them. When the pmproxy logger servlet delivers a
TYPE_LABEL record with hdr.len=13 (minimum accepted by the dispatcher),
rlen=1 and the function reads 20-24 bytes from a 1-byte buffer.
Fix: add minimum-length guard at the top of __pmLogLoadLabelSet()
using LABELSET_V3_MINRLEN / LABELSET_V2_MINRLEN macros derived from
the on-disk __pmExtLabelSet_v3/v2 struct sizes (minus the len+type
header that rlen excludes).
Test coverage will be added in a consolidated pducrash.c extension
covering vulns 9-13.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/src/libpcp/src/e_labels.c b/src/libpcp/src/e_labels.c
index 8e94a112b0..169817266b 100644
--- a/src/libpcp/src/e_labels.c
+++ b/src/libpcp/src/e_labels.c
@@ -56,6 +56,10 @@ typedef struct {
/* will be expanded if nsets > 0 */
} __pmExtLabelSet_v2;
+/* Minimum rlen (record body without len+type header) to read fixed fields */
+#define LABELSET_V3_MINRLEN (sizeof(__pmExtLabelSet_v3) - 2 * sizeof(__int32_t))
+#define LABELSET_V2_MINRLEN (sizeof(__pmExtLabelSet_v2) - 2 * sizeof(__int32_t))
+
/*
* pack a set of labels into a physical metadata record
* - lcp required to provide archive version
@@ -215,6 +219,23 @@ __pmLogLoadLabelSet(char *tbuf, int rlen, int rtype, __pmTimestamp *stamp,
*nsetsp = 0;
*labelsetsp = NULL;
+ if (rtype == TYPE_LABEL_V2) {
+ if (rlen < (int)LABELSET_V2_MINRLEN) {
+ if (pmDebugOptions.logmeta)
+ fprintf(stderr, "%s: v2 rlen=%d too small (min=%d)\n",
+ __FUNCTION__, rlen, (int)LABELSET_V2_MINRLEN);
+ return PM_ERR_LOGREC;
+ }
+ }
+ else {
+ if (rlen < (int)LABELSET_V3_MINRLEN) {
+ if (pmDebugOptions.logmeta)
+ fprintf(stderr, "%s: v3 rlen=%d too small (min=%d)\n",
+ __FUNCTION__, rlen, (int)LABELSET_V3_MINRLEN);
+ return PM_ERR_LOGREC;
+ }
+ }
+
k = 0;
if (rtype == TYPE_LABEL_V2) {
__pmLoadTimeval((__int32_t *)&tbuf[k], stamp);

View File

@ -0,0 +1,158 @@
From 7465d7cbdb Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] qa: extend pducrash with tests for vulns 9, 12, 13
Add three new test functions to pducrash.c exercising the OOB read
fixes:
- decode_log_labelset: calls __pmLogLoadLabelSet with rlen=1 for both
V2 and V3 record types, verifying the minimum-length guard rejects
undersized records (vuln 9)
- decode_log_status_oob: crafts a PDU_LOG_STATUS V3 with hostname_len
extending past the PDU boundary, verifying the bounds check now runs
before strdup (vuln 12)
- decode_instance_overshoot: crafts a PDU_INSTANCE claiming 2 entries
but only containing 1, where the alignment-padded advance pushes ip
past pdu_end, verifying the signed pointer guard catches it (vuln 13)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/qa/513.out b/qa/513.out
index 7ccaa1f370..6ffe932b39 100644
--- a/qa/513.out
+++ b/qa/513.out
@@ -277,6 +277,14 @@ QA output created by 513
__pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
[log_indom] checking out-of-range stridx with acp==NULL
__pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
+[log_labelset] checking rlen too small for v3 header
+ __pmLogLoadLabelSet: sts = -12373 (Corrupted record in a PCP archive)
+[log_labelset] checking rlen too small for v2 header
+ __pmLogLoadLabelSet: sts = -12373 (Corrupted record in a PCP archive)
+[log_status_oob] checking hostname_len past PDU boundary
+ __pmDecodeLogStatus: sts = -12366 (IPC protocol failure)
+[instance_overshoot] checking alignment overshoot past pdu_end
+ __pmDecodeInstance: sts = -12366 (IPC protocol failure)
=== filtered valgrind report ===
Memcheck, a memory error detector
Command: src/pducrash
diff --git a/qa/src/pducrash.c b/qa/src/pducrash.c
index 76e1e0ce76..94a5f72d47 100644
--- a/qa/src/pducrash.c
+++ b/qa/src/pducrash.c
@@ -1703,6 +1703,102 @@ decode_log_indom(const char *name)
}
}
+/*
+ * Test __pmLogLoadLabelSet with undersized rlen (vuln 9).
+ */
+static void
+decode_log_labelset(const char *name)
+{
+ __pmTimestamp stamp;
+ pmLabelSet *sets = NULL;
+ int type, ident, nsets, sts;
+ char tiny[4];
+
+ fprintf(stderr, "[%s] checking rlen too small for v3 header\n", name);
+ memset(tiny, 0, sizeof(tiny));
+ sts = __pmLogLoadLabelSet(tiny, 1, TYPE_LABEL, &stamp, &type, &ident, &nsets, &sets);
+ fprintf(stderr, " __pmLogLoadLabelSet: sts = %d (%s)\n", sts, pmErrStr(sts));
+ if (sts >= 0 && sets) pmFreeLabelSets(sets, nsets);
+
+ fprintf(stderr, "[%s] checking rlen too small for v2 header\n", name);
+ memset(tiny, 0, sizeof(tiny));
+ sets = NULL;
+ sts = __pmLogLoadLabelSet(tiny, 1, TYPE_LABEL_V2, &stamp, &type, &ident, &nsets, &sets);
+ fprintf(stderr, " __pmLogLoadLabelSet: sts = %d (%s)\n", sts, pmErrStr(sts));
+ if (sts >= 0 && sets) pmFreeLabelSets(sets, nsets);
+}
+
+/*
+ * Test __pmDecodeLogStatus with hostname_len past PDU end (vuln 12).
+ */
+static void
+decode_log_status_oob(const char *name)
+{
+ __pmLoggerStatus *log;
+ int sts;
+ int save_ipc_version = __pmVersionIPC(0);
+ struct log_sts {
+ __pmPDUHdr hdr;
+ __int32_t buf[20+2*PM_LOG_MAXHOSTLEN+2*PM_TZ_MAXLEN];
+ } *log_sts;
+
+ __pmSetVersionIPC(0, LOG_PDU_VERSION3);
+ log_sts = (struct log_sts *)malloc(sizeof(*log_sts));
+
+ fprintf(stderr, "[%s] checking hostname_len past PDU boundary\n", name);
+ memset(log_sts, 0, sizeof(*log_sts));
+ log_sts->hdr.len = 100;
+ log_sts->hdr.type = PDU_LOG_STATUS;
+ /* buf[13] is pmcd_hostname_len in V3 — set to extend past the PDU */
+ log_sts->buf[13] = htonl(90);
+ sts = __pmDecodeLogStatus((__pmPDU *)log_sts, &log);
+ fprintf(stderr, " __pmDecodeLogStatus: sts = %d (%s)\n", sts, pmErrStr(sts));
+ if (sts == 0) __pmFreeLogStatus(log, 1);
+
+ free(log_sts);
+ __pmSetVersionIPC(0, save_ipc_version);
+}
+
+/*
+ * Test __pmDecodeInstance with alignment-padding overshoot (vuln 13).
+ * Craft a PDU_INSTANCE with numinst=2 where the first entry's namelen
+ * causes the alignment-padded advance to push ip past pdu_end.
+ */
+static void
+decode_instance_overshoot(const char *name)
+{
+ pmInResult *inresult;
+ int sts;
+ struct {
+ __pmPDUHdr hdr;
+ pmInDom indom;
+ int numinst;
+ /* entry 0: inst + namelen + name (padded) */
+ int inst0;
+ int namelen0;
+ char name0[4]; /* padded to 4 bytes */
+ /* entry 1 would start here — but PDU ends before it */
+ } *pdu;
+
+ pdu = (typeof(pdu))malloc(sizeof(*pdu));
+
+ fprintf(stderr, "[%s] checking alignment overshoot past pdu_end\n", name);
+ memset(pdu, 0, sizeof(*pdu));
+ pdu->hdr.len = sizeof(*pdu);
+ pdu->hdr.type = PDU_INSTANCE;
+ pdu->numinst = htonl(2); /* claim 2 entries but only room for 1 */
+ pdu->inst0 = htonl(0);
+ pdu->namelen0 = htonl(3); /* 3 bytes + pad to 4 = alignment overshoot */
+ pdu->name0[0] = 'a';
+ pdu->name0[1] = 'b';
+ pdu->name0[2] = 'c';
+ sts = __pmDecodeInstance((__pmPDU *)pdu, &inresult);
+ fprintf(stderr, " __pmDecodeInstance: sts = %d (%s)\n", sts, pmErrStr(sts));
+ if (sts >= 0) __pmFreeInResult(inresult);
+
+ free(pdu);
+}
+
typedef void (*decode_t)(const char *);
struct pdu {
@@ -1738,6 +1834,9 @@ struct pdu {
{ "desc_ids", decode_desc_ids },
{ "descs", decode_descs },
{ "log_indom", decode_log_indom },
+ { "log_labelset", decode_log_labelset },
+ { "log_status_oob", decode_log_status_oob },
+ { "instance_overshoot", decode_instance_overshoot },
};
int

View File

@ -0,0 +1,113 @@
From 2a4bd9f81a Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] pmdaroot: add peer credential verification on Unix socket (CWE-403)
Defense-in-depth for the FD_CLOEXEC fix: verify the UID of connecting
clients on the pmdaroot Unix socket using SO_PEERCRED (Linux) or
getpeereid (macOS/FreeBSD). Only root (UID 0) and the PCP service
user (typically 'pcp') are permitted to connect. Connections from
other UIDs are rejected with a log message.
This prevents exploitation even if the pmdaroot socket fd were to
leak to an unprivileged process through a path not covered by
FD_CLOEXEC (e.g., direct socket file access).
The PCP service UID is resolved once at startup via pmGetUsername()
and getpwnam(), cached in a static for use in the accept path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/src/pmdas/root/root.c b/src/pmdas/root/root.c
index 2e65a2ea6d..7ec2e5bf9c 100644
--- a/src/pmdas/root/root.c
+++ b/src/pmdas/root/root.c
@@ -23,6 +23,9 @@
#include "docker.h"
#include "podman.h"
#include "domain.h"
+#if defined(HAVE_PWD_H)
+#include <pwd.h>
+#endif
#ifndef S_IRWXU
/*
@@ -37,6 +40,7 @@ static char socket_path[MAXPATHLEN];
static __pmSockAddr *socket_addr;
static int socket_fd = -1;
static int pmcd_fd = -1;
+static uid_t pcp_uid;
static __pmFdSet connected_fds;
int root_maximum_fd;
@@ -461,6 +465,42 @@ root_accept_client(void)
exit(1);
}
}
+#if defined(HAVE_STRUCT_UCRED)
+ {
+ struct ucred cred;
+ __pmSockLen len = sizeof(cred);
+
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == 0) {
+ if (cred.uid != 0 && cred.uid != pcp_uid) {
+ pmNotifyErr(LOG_ERR,
+ "root_accept_client: rejected uid=%d (expected root or pcp[%d])\n",
+ cred.uid, pcp_uid);
+ close(fd);
+ root_client[i].fd = -1;
+ root_delete_client(&root_client[i]);
+ return NULL;
+ }
+ }
+ }
+#elif defined(HAVE_GETPEEREID)
+ {
+ uid_t uid;
+ gid_t gid;
+
+ if (getpeereid(fd, &uid, &gid) == 0) {
+ if (uid != 0 && uid != pcp_uid) {
+ pmNotifyErr(LOG_ERR,
+ "root_accept_client: rejected uid=%d (expected root or pcp[%d])\n",
+ uid, pcp_uid);
+ close(fd);
+ root_client[i].fd = -1;
+ root_delete_client(&root_client[i]);
+ return NULL;
+ }
+ }
+ }
+#endif
+
if (fd > root_maximum_fd)
root_maximum_fd = fd;
__pmFD_SET(fd, &connected_fds);
@@ -796,6 +836,19 @@ root_main(pmdaInterface *dp)
}
}
+static void
+root_get_pcp_uid(void)
+{
+#if defined(HAVE_PWD_H)
+ char *username;
+ struct passwd *pw;
+
+ pmGetUsername(&username);
+ if ((pw = getpwnam(username)) != NULL)
+ pcp_uid = pw->pw_uid;
+#endif
+}
+
static void
root_check_user(void)
{
@@ -815,6 +868,7 @@ static void
root_prep(void)
{
root_check_user();
+ root_get_pcp_uid();
root_setup_socket();
atexit(root_close_socket);
}

View File

@ -0,0 +1,142 @@
From cdc9676ab6 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] pmieconf: fix command injection via $HOME and -f (CWE-78)
The write_pmiefile() function constructed a shell command via
pmsprintf("/bin/mkdir -p %s", fname) and passed it to system().
The fname value derives from either $HOME or the -f command-line
argument without sanitization, enabling command injection through
shell metacharacters in the path.
Fix: replace system("/bin/mkdir -p ...") with __pmMakePath() which
creates directories recursively using mkdir() syscalls directly,
with no shell involvement.
Add qa/2103 verifying that legitimate directory creation works and
that shell metacharacters in -f and $HOME paths do not result in
command execution.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/qa/2103 b/qa/2103
new file mode 100755
index 0000000000..c068ae92f0
--- /dev/null
+++ b/qa/2103
@@ -0,0 +1,62 @@
+#!/bin/sh
+# PCP QA Test No. 2103
+# Verify pmieconf does not execute shell metacharacters in -f path
+# (CWE-78 fix verification)
+#
+# 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 pmieconf >/dev/null 2>&1 || _notrun "pmieconf not installed"
+
+_cleanup()
+{
+ cd $here
+ $sudo rm -rf $tmp $tmp.*
+}
+
+status=0 # success is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+# real QA test starts here
+
+echo "=== normal -f path should work ==="
+mkdir -p $tmp.dir
+pmieconf -f $tmp.dir/subdir/test.pmie modify global delta "2min" >$tmp.out 2>&1
+_sts=$?
+if [ -f $tmp.dir/subdir/test.pmie ]; then
+ echo "directory creation and file write succeeded"
+else
+ echo "FAIL: file not created (exit=$_sts)"
+ cat $tmp.out
+fi
+
+echo
+echo "=== -f path with semicolon should not execute commands ==="
+_bad="$tmp.dir/bad;touch $tmp.dir/pwned"
+pmieconf -f "$_bad" modify global delta "2min" >$tmp.out 2>&1
+if [ -f "$tmp.dir/pwned" ]; then
+ echo "FAIL: shell metacharacter was executed"
+else
+ echo "no command execution from semicolon in path"
+fi
+
+echo
+echo "=== verify no injected file from HOME variable ==="
+_badhome="$tmp.dir/home;touch $tmp.dir/pwned2"
+HOME="$_badhome" pmieconf modify global delta "2min" >$tmp.out 2>&1
+if [ -f "$tmp.dir/pwned2" ]; then
+ echo "FAIL: shell metacharacter in HOME was executed"
+else
+ echo "no command execution from HOME injection"
+fi
+
+# success, all done
+exit
diff --git a/qa/2103.out b/qa/2103.out
new file mode 100644
index 0000000000..a5054675e2
--- /dev/null
+++ b/qa/2103.out
@@ -0,0 +1,9 @@
+QA output created by 2103
+=== normal -f path should work ===
+directory creation and file write succeeded
+
+=== -f path with semicolon should not execute commands ===
+no command execution from semicolon in path
+
+=== verify no injected file from HOME variable ===
+no command execution from HOME injection
diff --git a/qa/group b/qa/group
index 9c158034fd..c096e80cc8 100644
--- a/qa/group
+++ b/qa/group
@@ -2221,4 +2221,5 @@ pmcd.pdu
2104 libpcp local security
2101 linux_sockets local security
2102 pmlogmv local security
+2103 pmieconf local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/src/pmieconf/rules.c b/src/pmieconf/rules.c
index 4bd173fcfe..f0b3ce68c6 100644
--- a/src/pmieconf/rules.c
+++ b/src/pmieconf/rules.c
@@ -1805,7 +1805,6 @@ write_pmiefile(char *program, int autocreate)
{
time_t now = time(NULL);
char *p, *msg = NULL;
- char buf[MAXPATHLEN+10];
char *fname = get_pmiefile();
FILE *fp;
int i;
@@ -1816,9 +1815,8 @@ write_pmiefile(char *program, int autocreate)
*p = '\0'; /* p is the dirname of fname */
if (stat(fname, &sbuf) < 0) {
- pmsprintf(buf, sizeof(buf), "/bin/mkdir -p %s", fname);
- if (system(buf) < 0) {
- pmsprintf(errmsg, sizeof(errmsg), "failed to create directory \"%s\"", p);
+ if (__pmMakePath(fname, 0755) < 0) {
+ pmsprintf(errmsg, sizeof(errmsg), "failed to create directory \"%s\"", fname);
return errmsg;
}
}

View File

@ -0,0 +1,338 @@
From dc73ec0f57 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] pmlogmv: fix command injection in pmlogcp/pmlogmv (CWE-78)
The do_link() function used system("cp src dst") to copy archive files
when link() fails with EXDEV. The source filename was not validated by
check_name() and was embedded directly into the shell command, enabling
command injection via crafted archive filenames. The do_checksum()
function similarly used system() for command detection and popen() for
checksum execution.
Fix:
- Replace system("cp ...") with copy_file() using open/read/write
syscalls directly, eliminating shell involvement entirely
- Replace system("if which ...") checksum detection with access() checks
- Replace popen("md5sum <file") with __pmProcessPipe() which uses
execvp() internally, passing filenames as argv not shell words
- Expand check_name() blocklist to include backtick, braces, backslash,
bang, newline and tab (defense-in-depth, no longer the security
boundary)
- Apply check_name() to source names as well as destination names
- Add qa/2102 verifying metacharacter rejection and normal copy
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/qa/2102 b/qa/2102
new file mode 100755
index 0000000000..5f8a4ef1cc
--- /dev/null
+++ b/qa/2102
@@ -0,0 +1,67 @@
+#!/bin/sh
+# PCP QA Test No. 2102
+# Verify pmlogmv rejects shell metacharacters in filenames
+# and that normal move operations still work (CWE-78 fix)
+#
+# 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
+
+_cleanup()
+{
+ cd $here
+ $sudo rm -rf $tmp $tmp.*
+}
+
+status=0 # success is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+_filter()
+{
+ sed \
+ -e "s,$tmp,TMP,g" \
+ -e "s/pmlogmv/TOOL/" \
+ # end
+}
+
+# real QA test starts here
+
+echo "=== normal pmlogmv should succeed ==="
+for ext in meta 0 index
+do
+ [ -f tmparch/foo.$ext ] && cp tmparch/foo.$ext $tmp.src.$ext
+done
+pmlogmv $tmp.src $tmp.mvdest 2>&1 | _filter
+if [ -f $tmp.mvdest.meta ] || [ -f $tmp.mvdest.0 ]; then
+ echo "move succeeded"
+else
+ echo "FAIL: move did not create files"
+fi
+
+echo
+echo "=== pmlogmv with backtick in destination should be rejected ==="
+pmlogmv tmparch/foo "$tmp.bad\`id\`" >$tmp.out 2>&1
+_sts=$?
+_filter <$tmp.out
+echo "exit status: $_sts"
+
+echo
+echo "=== pmlogmv with semicolon in destination should be rejected ==="
+pmlogmv tmparch/foo "$tmp.bad;id" >$tmp.out 2>&1
+_sts=$?
+_filter <$tmp.out
+echo "exit status: $_sts"
+
+echo
+echo "=== pmlogmv with dollar in destination should be rejected ==="
+pmlogmv tmparch/foo '$tmp.bad${IFS}' >$tmp.out 2>&1
+_sts=$?
+_filter <$tmp.out
+echo "exit status: $_sts"
+
+# success, all done
+exit
diff --git a/qa/2102.out b/qa/2102.out
new file mode 100644
index 0000000000..5a3fec6ca8
--- /dev/null
+++ b/qa/2102.out
@@ -0,0 +1,15 @@
+QA output created by 2102
+=== normal pmlogmv should succeed ===
+move succeeded
+
+=== pmlogmv with backtick in destination should be rejected ===
+TOOL: name (TMP.bad`id`) unsafe [shell metacharacter '`']
+exit status: 1
+
+=== pmlogmv with semicolon in destination should be rejected ===
+TOOL: name (TMP.bad;id) unsafe [shell metacharacter ';']
+exit status: 1
+
+=== pmlogmv with dollar in destination should be rejected ===
+TOOL: name ($tmp.bad${IFS}) unsafe [shell metacharacter '$']
+exit status: 1
diff --git a/qa/group b/qa/group
index 0d9ef9ffb6..9c158034fd 100644
--- a/qa/group
+++ b/qa/group
@@ -2220,4 +2220,5 @@ pmcd.pdu
2100 pmproxy local security
2104 libpcp local security
2101 linux_sockets local security
+2102 pmlogmv local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/src/pmlogmv/pmlogmv.c b/src/pmlogmv/pmlogmv.c
index ee59e07b7e..742fa8d740 100644
--- a/src/pmlogmv/pmlogmv.c
+++ b/src/pmlogmv/pmlogmv.c
@@ -17,6 +17,8 @@
*/
#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>
@@ -73,12 +75,12 @@ myoverrides(int opt, pmOptions *optsp)
static int
check_name(char *name)
{
- char *meta = " $?*[(|;&<>";
+ char *meta = " $?*[(|;&<>`{}\\!\n\t";
char *p;
for (p = meta; *p; p++) {
if (index(name, *p) != NULL) {
- fprintf(stderr, "pmlogmv: newname (%s) unsafe [shell metacharacter '%c']\n", name, *p);
+ fprintf(stderr, "pmlogmv: name (%s) unsafe [shell metacharacter '%c']\n", name, *p);
return -1;
}
}
@@ -144,54 +146,54 @@ setup_sufftab(void)
void
do_checksum(const char *file, char *sum)
{
- char cmd[MAXPATHLEN+40];
static char *executable = NULL;
FILE *fp;
static int trunc_warn = 0;
if (executable == NULL) {
- /*
- * one-trip to initialize the "checksum" command ...
- * prefer md5sum, then sha256sum, then sha1sum, then sum,
- * else do nothing
- */
- snprintf(cmd, sizeof(cmd), "if which md5sum >/dev/null 2>&1; then exit 0; fi; exit 1");
- if (system(cmd) == 0)
- executable = "md5sum";
- else {
- snprintf(cmd, sizeof(cmd), "if which sha256sum >/dev/null 2>&1; then exit 0; fi; exit 1");
- if (system(cmd) == 0)
- executable = "sha256sum";
- else {
- snprintf(cmd, sizeof(cmd), "if which sha1sum >/dev/null 2>&1; then exit 0; fi; exit 1");
- if (system(cmd) == 0)
- executable = "sha1sum";
- else {
- snprintf(cmd, sizeof(cmd), "if which sum >/dev/null 2>&1; then exit 0; fi; exit 1");
- if (system(cmd) == 0)
- executable = "sum";
- else {
- executable = "none";
- fprintf(stderr, "pmlogmv: warning: no checksum command found, checksums skipped\n");
- }
- }
+ static const char *candidates[] = {
+ "md5sum", "sha256sum", "sha1sum", "sum", NULL
+ };
+ const char **cp;
+ char path[MAXPATHLEN];
+
+ executable = "none";
+ for (cp = candidates; *cp != NULL; cp++) {
+ snprintf(path, sizeof(path), "/usr/bin/%s", *cp);
+ if (access(path, X_OK) == 0) {
+ executable = (char *)*cp;
+ break;
+ }
+ snprintf(path, sizeof(path), "/usr/sbin/%s", *cp);
+ if (access(path, X_OK) == 0) {
+ executable = (char *)*cp;
+ break;
}
}
+ if (strcmp(executable, "none") == 0)
+ fprintf(stderr, "pmlogmv: warning: no checksum command found, checksums skipped\n");
if (verbose && strcmp(executable, "none") != 0)
printf("checksum cmd: %s\n", executable);
}
sum[0] = '\0';
if (strcmp(executable, "none") == 0)
return;
- snprintf(cmd, sizeof(cmd), "%s <%s", executable, file);
- if ((fp = popen(cmd, "r")) == NULL) {
- /*
- * abandon checksuming ...
- */
- fprintf(stderr, "pmlogmv: pipe(\"%s\") failed: %s\n", cmd, strerror(errno));
- executable = "none";
+ {
+ __pmExecCtl_t *argp = NULL;
+ int sts;
+
+ if ((sts = __pmProcessAddArg(&argp, executable)) < 0 ||
+ (sts = __pmProcessAddArg(&argp, file)) < 0) {
+ executable = "none";
+ return;
+ }
+ if ((sts = __pmProcessPipe(&argp, "r", PM_EXEC_TOSS_NONE, &fp)) < 0) {
+ fprintf(stderr, "pmlogmv: __pmProcessPipe(\"%s\") failed: %s\n", executable, pmErrStr(sts));
+ executable = "none";
+ return;
+ }
}
- else {
+ {
char *p = sum;
int c;
while ((c = fgetc(fp)) != EOF) {
@@ -200,9 +202,6 @@ do_checksum(const char *file, char *sum)
break;
}
if (p >= &sum[MAX_CHECKSUM]) {
- /*
- * avoid buffer overrun, report only once unless -V
- */
if (trunc_warn++ == 0 || verbose)
fprintf(stderr, "pmlogmv: warning: checksum truncated after %d characters\n", MAX_CHECKSUM);
*p = '\0';
@@ -210,10 +209,54 @@ do_checksum(const char *file, char *sum)
}
*p++ = c;
}
- pclose(fp);
+ __pmProcessPipeClose(fp);
+ }
+}
+
+/*
+ * copy a file using read/write - no shell involvement
+ */
+static int
+copy_file(const char *src, const char *dst)
+{
+ int sfd, dfd;
+ struct stat sbuf;
+ ssize_t nread, nwritten;
+ char buf[BUFSIZ];
+
+ if ((sfd = open(src, O_RDONLY)) < 0)
+ return -1;
+ if (fstat(sfd, &sbuf) < 0) {
+ close(sfd);
+ return -1;
+ }
+ if ((dfd = open(dst, O_WRONLY|O_CREAT|O_EXCL, sbuf.st_mode & 0777)) < 0) {
+ close(sfd);
+ return -1;
+ }
+ while ((nread = read(sfd, buf, sizeof(buf))) > 0) {
+ char *p = buf;
+ while (nread > 0) {
+ nwritten = write(dfd, p, nread);
+ if (nwritten < 0) {
+ close(sfd);
+ close(dfd);
+ unlink(dst);
+ return -1;
+ }
+ nread -= nwritten;
+ p += nwritten;
+ }
}
+ close(sfd);
+ if (nread < 0 || close(dfd) < 0) {
+ unlink(dst);
+ return -1;
+ }
+ return 0;
}
+
/*
* make link for one physical file
* return codes:
@@ -267,7 +310,6 @@ do_link(int vol)
#endif
/* link() failed cross-device, need to copy ... */
int sts;
- char cmd[2*MAXPATHLEN+4];
char sum_src[MAX_CHECKSUM+1];
char sum_dst[MAX_CHECKSUM+1];
if (checksum) {
@@ -280,8 +322,7 @@ do_link(int vol)
printf("source checksum: %s\n", sum_src);
}
- snprintf(cmd, sizeof(cmd), "cp %s %s", src, dst);
- if ((sts = system(cmd)) != 0) {
+ if ((sts = copy_file(src, dst)) != 0) {
fprintf(stderr, "pmlogmv: copy %s -> %s failed: %s\n", src, dst, strerror(errno));
return -1;
}

View File

@ -0,0 +1,43 @@
From 81a9efe96d Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] pmproxy: enforce -Q (CERT_REQD) for REST API connections
The -Q flag (PM_SERVER_FEATURE_CERT_REQD) was only enforced in the
legacy PCP wire protocol path (deprecated.c). The modern HTTP/REST
API path had no check, allowing unauthenticated plain-HTTP clients
to access all endpoints even when -Q was specified.
Add enforcement in on_headers_complete() alongside the existing -S
(CREDS_REQD) check: when CERT_REQD is active, reject requests where
the connection is not TLS or no client certificate was presented.
Returns HTTP 403 Forbidden. If OpenSSL is not compiled in, all
connections are rejected when -Q is set since TLS is unavailable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/src/pmproxy/src/http.c b/src/pmproxy/src/http.c
index cfbc8aec0c..e0d28ffc28 100644
--- a/src/pmproxy/src/http.c
+++ b/src/pmproxy/src/http.c
@@ -1094,6 +1094,20 @@ on_headers_complete(http_parser *request)
}
}
+ /* client certificate required for all servlets */
+ if (__pmServerHasFeature(PM_SERVER_FEATURE_CERT_REQD)) {
+#ifdef HAVE_OPENSSL
+ if (!client->stream.secure ||
+ !client->secure.ssl ||
+ SSL_get_peer_certificate(client->secure.ssl) == NULL) {
+ client->u.http.parser.status_code = HTTP_STATUS_FORBIDDEN;
+ }
+#else
+ /* no TLS support compiled in, reject all connections */
+ client->u.http.parser.status_code = HTTP_STATUS_FORBIDDEN;
+#endif
+ }
+
return sts;
}

View File

@ -0,0 +1,44 @@
From be9ade9950 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] qa/src/scanmeta.c: fix call to __pmLogLoadInDom()
scanmeta was *using* the acp == NULL guard to dodge the rlen test and
calling with rlen == 0 (this QA app was simply assuming the record
was valid, at least to the point that the buffer could be correctly
decoded).
Fix involves re-extracting the correct record length and calling
__pmLogLoadInDom() with rlen != 0.
---
diff --git a/qa/src/scanmeta.c b/qa/src/scanmeta.c
index 1f884ecbb6..3e65fb5028 100644
--- a/qa/src/scanmeta.c
+++ b/qa/src/scanmeta.c
@@ -143,7 +143,7 @@ free_elt_fields(elt_t *ep)
}
void
-do_indom(__int32_t *buf, int type)
+do_indom(__int32_t *buf, int type, int len)
{
int sts;
static __pmTimestamp prior_stamp = { 0, 0 };
@@ -156,7 +156,7 @@ do_indom(__int32_t *buf, int type)
elt_t *tp;
elt_t *dp = &dup;
- if ((sts = __pmLogLoadInDom(NULL, 0, type, &lid, &buf)) < 0) {
+ if ((sts = __pmLogLoadInDom(NULL, len, type, &lid, &buf)) < 0) {
fprintf(stderr, "__pmLoadLoadInDom: failed: %s\n", pmErrStr(sts));
return;
}
@@ -689,7 +689,7 @@ main(int argc, char *argv[])
case TYPE_INDOM_V2:
if (!iflag)
break;
- do_indom(buf, hdr.type);
+ do_indom(buf, hdr.type, hdr.len);
break;
case TYPE_LABEL:

View File

@ -0,0 +1,243 @@
From f86c0f4cda Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp, libpcp_web: validate timezone and zoneinfo strings
The timezone and zoneinfo fields from archive labels and PDU_LOG_STATUS
are used via pmNewZone() -> setenv("TZ", ...), causing glibc to resolve
Olson timezone paths against /usr/share/zoneinfo/. A crafted value
like "../../etc/passwd" would cause glibc to open arbitrary files.
Fix at two layers:
- Front door: add check_tz() check in pmLogGroupLabel() alongside the
existing check_hostname() check, rejecting unsafe timezone/zoneinfo
before any data is written to disk
- Consumption: add check_tz() check in pmNewZone() as defense-in-depth,
protecting against malicious archives created by other means
The allowlist permits alphanumeric characters plus /_+-.:" which covers
both Olson paths (America/New_York) and POSIX TZ strings (EST5EDT).
Leading slashes and ".." path components are rejected.
Add qa/src/check_tz.c and qa/2107 exercising pmNewZone() with valid
and malicious timezone strings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/qa/2107 b/qa/2107
new file mode 100755
index 0000000000..d4017c02fe
--- /dev/null
+++ b/qa/2107
@@ -0,0 +1,60 @@
+#!/bin/sh
+# PCP QA Test No. 2107
+# Verify pmNewZone rejects unsafe timezone strings containing
+# path traversal or invalid characters
+#
+# 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
+
+_cleanup()
+{
+ cd $here
+ $sudo rm -rf $tmp $tmp.*
+}
+
+status=0 # success is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+# real QA test starts here
+
+echo "=== valid Olson timezone ==="
+src/check_tz "America/New_York"
+
+echo
+echo "=== valid POSIX timezone ==="
+src/check_tz "EST5EDT"
+
+echo
+echo "=== valid simple timezone ==="
+src/check_tz "UTC"
+
+echo
+echo "=== path traversal should be rejected ==="
+src/check_tz "../../etc/passwd"
+
+echo
+echo "=== leading slash should be rejected ==="
+src/check_tz "/etc/localtime"
+
+echo
+echo "=== semicolon should be rejected ==="
+src/check_tz "UTC;id"
+
+echo
+echo "=== backtick should be rejected ==="
+src/check_tz 'UTC`id`'
+
+echo
+echo "=== empty string should be accepted ==="
+src/check_tz ""
+
+# success, all done
+exit
diff --git a/qa/2107.out b/qa/2107.out
new file mode 100644
index 0000000000..42cf4d6adb
--- /dev/null
+++ b/qa/2107.out
@@ -0,0 +1,24 @@
+QA output created by 2107
+=== valid Olson timezone ===
+pmNewZone("America/New_York") -> 0 (accepted)
+
+=== valid POSIX timezone ===
+pmNewZone("EST5EDT") -> 0 (accepted)
+
+=== valid simple timezone ===
+pmNewZone("UTC") -> 0 (accepted)
+
+=== path traversal should be rejected ===
+pmNewZone("../../etc/passwd") -> Invalid argument (rejected)
+
+=== leading slash should be rejected ===
+pmNewZone("/etc/localtime") -> Invalid argument (rejected)
+
+=== semicolon should be rejected ===
+pmNewZone("UTC;id") -> Invalid argument (rejected)
+
+=== backtick should be rejected ===
+pmNewZone("UTC`id`") -> Invalid argument (rejected)
+
+=== empty string should be accepted ===
+pmNewZone("") - skipped (empty string)
diff --git a/qa/group b/qa/group
index c096e80cc8..a80d1039dc 100644
--- a/qa/group
+++ b/qa/group
@@ -2222,4 +2222,5 @@ pmcd.pdu
2101 linux_sockets local security
2102 pmlogmv local security
2103 pmieconf local security
+2107 libpcp local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/qa/src/GNUlocaldefs b/qa/src/GNUlocaldefs
index 6ee74c7f16..c0b158a1a6 100644
--- a/qa/src/GNUlocaldefs
+++ b/qa/src/GNUlocaldefs
@@ -55,7 +55,7 @@ CFILES = disk_test.c exercise.c context_test.c chkoptfetch.c \
dumpstack.c usergroup.c derived_help.c ready-or-not.c cleanmapdir.c \
throttle.c throttle_timeout.c y2038.c bigpmcdpmids.c pdu-gadget.c \
newcontext.c \
- check_cloexec.c
+ check_cloexec.c check_tz.c
ifeq ($(shell test -f ../localconfig && echo 1), 1)
include ../localconfig
@@ -580,6 +580,11 @@ check_cloexec: check_cloexec.c
$(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS)
$(LINKER_MAKERULE)
+check_tz: check_tz.c
+ rm -f $@
+ $(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS)
+ $(LINKER_MAKERULE)
+
check_import: check_import.c
rm -f $@
$(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS) -lpcp_import
diff --git a/qa/src/check_tz.c b/qa/src/check_tz.c
new file mode 100644
index 0000000000..ad96444f9c
--- /dev/null
+++ b/qa/src/check_tz.c
@@ -0,0 +1,31 @@
+/*
+ * Verify pmNewZone accepts/rejects timezone strings correctly.
+ */
+
+#include <pcp/pmapi.h>
+
+int
+main(int argc, char **argv)
+{
+ int sts;
+
+ pmSetProgname(argv[0]);
+
+ if (argc != 2) {
+ fprintf(stderr, "Usage: %s timezone\n", pmGetProgname());
+ return 1;
+ }
+
+ if (argv[1][0] == '\0') {
+ printf("pmNewZone(\"\") - skipped (empty string)\n");
+ return 0;
+ }
+
+ sts = pmNewZone(argv[1]);
+ if (sts >= 0)
+ printf("pmNewZone(\"%s\") -> %d (accepted)\n", argv[1], sts);
+ else
+ printf("pmNewZone(\"%s\") -> %s (rejected)\n", argv[1], pmErrStr(sts));
+
+ return 0;
+}
diff --git a/src/libpcp/src/tz.c b/src/libpcp/src/tz.c
index 0aab541776..169846acc9 100644
--- a/src/libpcp/src/tz.c
+++ b/src/libpcp/src/tz.c
@@ -25,6 +25,7 @@
* lock initialization in pmNewContext().
*/
+#include <ctype.h>
#include "pmapi.h"
#include "libpcp.h"
#include "sha256.h"
@@ -573,6 +574,22 @@ pmUseZone(const int tz_handle)
return 0;
}
+static int
+valid_tz(const char *tz)
+{
+ const char *p;
+
+ if (tz == NULL || tz[0] == '\0' || tz[0] == '/')
+ return 0;
+ for (p = tz; *p; p++) {
+ if (!isalnum((unsigned char)*p) && strchr("/_+-.:,\"'", *p) == NULL)
+ return 0;
+ }
+ if (strstr(tz, "..") != NULL)
+ return 0;
+ return 1;
+}
+
int
pmNewZone(const char *tz)
{
@@ -580,6 +597,13 @@ pmNewZone(const char *tz)
int hack = 0;
int sts;
+ if (!valid_tz(tz)) {
+ if (pmDebugOptions.context)
+ fprintf(stderr, "%s: rejecting unsafe timezone: %s\n",
+ __FUNCTION__, tz ? tz : "(null)");
+ return -EINVAL;
+ }
+
PM_LOCK(__pmLock_extcall);
len = (int)strlen(tz);

256
pcp-RHEL-213658.patch Normal file
View File

@ -0,0 +1,256 @@
From c5cbeceb7d Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] linux_sockets pmda: fix command injection via network.persocket.filter (CWE-78)
The sockets_check_filter() validation helper returns 1 for safe input
and 0 for unsafe input. The guard in sockets_store() tested
if (sockets_check_filter(av.cp)) — rejecting safe input and accepting
malicious input containing shell metacharacters. The accepted filter
was later passed to popen() via shell interpretation, enabling arbitrary
command execution as the PMDA process user.
Fix:
- Invert the guard: if (!sockets_check_filter(av.cp))
- Replace popen()/pclose() in ss_open_stream() with the libpcp
__pmProcessAddArg()/__pmProcessPipe()/__pmProcessPipeClose() API
which uses execvp() internally, eliminating shell interpretation
of the filter string entirely
- Add qa/2101 verifying that valid filters are accepted and shell
metacharacters (semicolons, backticks, pipes) are rejected
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves: RHEL-213658 CVE-2026-16524
---
diff --git a/qa/2101 b/qa/2101
new file mode 100755
index 0000000000..733b7f70c9
--- /dev/null
+++ b/qa/2101
@@ -0,0 +1,70 @@
+#!/bin/sh
+# PCP QA Test No. 2101
+# Verify linux_sockets PMDA filter validation rejects shell metacharacters
+# and accepts valid filter expressions (CWE-78 fix verification)
+#
+# 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
+
+[ $PCP_PLATFORM = linux ] || _notrun "Linux-specific sockets testing"
+[ -f $PCP_PMDAS_DIR/sockets/pmdasockets ] || _notrun "sockets PMDA not installed"
+
+_cleanup()
+{
+ _cleanup_pmda sockets
+ cd $here
+ $sudo rm -rf $tmp $tmp.*
+}
+
+status=0 # success is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+_prepare_pmda sockets
+_stop_auto_restart pmcd
+
+# install the sockets PMDA
+cd $PCP_PMDAS_DIR/sockets
+$sudo ./Remove >/dev/null 2>&1
+$sudo ./Install </dev/null >$tmp.out 2>&1
+cat $tmp.out >>$seq_full
+
+# check the PMDA is alive
+pmprobe -v network.persocket.filter >$tmp.probe 2>&1
+grep -q 'No PMCD agent' $tmp.probe && _notrun "sockets PMDA failed to install"
+
+# real QA test starts here
+
+echo "=== valid filter should be accepted ==="
+pmstore network.persocket.filter "sport == 22" 2>&1 \
+| grep -q 'Bad input' && echo "FAIL: valid filter rejected" || echo "valid filter accepted"
+
+echo
+echo "=== shell metacharacter semicolon should be rejected ==="
+pmstore network.persocket.filter ';id' 2>&1 \
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
+
+echo
+echo "=== shell metacharacter backtick should be rejected ==="
+pmstore network.persocket.filter '`id`' 2>&1 \
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
+
+echo
+echo "=== shell metacharacter pipe should be rejected ==="
+pmstore network.persocket.filter '|cat /etc/passwd' 2>&1 \
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
+
+echo
+echo "=== shell metacharacter dollar should be rejected ==="
+pmstore network.persocket.filter '${IFS}id' 2>&1 \
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
+
+# success, all done
+exit
diff --git a/qa/2101.out b/qa/2101.out
new file mode 100644
index 0000000000..02f9ac5655
--- /dev/null
+++ b/qa/2101.out
@@ -0,0 +1,15 @@
+QA output created by 2101
+=== valid filter should be accepted ===
+valid filter accepted
+
+=== shell metacharacter semicolon should be rejected ===
+metacharacter rejected
+
+=== shell metacharacter backtick should be rejected ===
+metacharacter rejected
+
+=== shell metacharacter pipe should be rejected ===
+metacharacter rejected
+
+=== shell metacharacter dollar should be rejected ===
+metacharacter rejected
diff --git a/qa/group b/qa/group
index c7db6290b2..0d9ef9ffb6 100644
--- a/qa/group
+++ b/qa/group
@@ -2219,4 +2219,5 @@ pmcd.pdu
2105 libpcp local security
2100 pmproxy local security
2104 libpcp local security
+2101 linux_sockets local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/src/pmdas/linux_sockets/pmda.c b/src/pmdas/linux_sockets/pmda.c
index 5a3018d8aa..59e69a6756 100644
--- a/src/pmdas/linux_sockets/pmda.c
+++ b/src/pmdas/linux_sockets/pmda.c
@@ -162,11 +162,9 @@ sockets_check_filter(const char *string)
const char *p;
for (p = string; *p; p++) {
- if (isspace(*p))
+ if (isspace(*p) || isalnum(*p))
continue;
- if (isalnum(*p))
- continue;
- if (*p == '(' || *p == ')')
+ if (strchr("()=!<>:.*/-,", *p) != NULL)
continue;
return 0; /* disallow */
}
@@ -191,7 +189,7 @@ sockets_store(pmResult *result, pmdaExt *pmda)
case 0: /* network.persocket.filter */
if ((sts = pmExtractValue(vsp->valfmt, &vsp->vlist[0],
PM_TYPE_STRING, &av, PM_TYPE_STRING)) >= 0) {
- if (sockets_check_filter(av.cp)) {
+ if (!sockets_check_filter(av.cp)) {
sts = PM_ERR_BADSTORE;
free(av.cp);
break;
diff --git a/src/pmdas/linux_sockets/ss_stream.c b/src/pmdas/linux_sockets/ss_stream.c
index 421c65fd16..833fc275a0 100644
--- a/src/pmdas/linux_sockets/ss_stream.c
+++ b/src/pmdas/linux_sockets/ss_stream.c
@@ -14,18 +14,19 @@
#include <pcp/pmapi.h>
#include <pcp/pmda.h>
+#include <pcp/libpcp.h>
#include "ss_stats.h"
#define SS_OPTIONS "-noemitauO"
-char *ss_filter = NULL; /* storable: network.persocket.filter */
+char *ss_filter; /* storable: network.persocket.filter */
+static int using_pipe; /* pipe is normal operation, QA uses files */
FILE *
ss_open_stream()
{
- FILE *fp;
+ FILE *fp = NULL;
char *path;
- char cmd[MAXPATHLEN];
if (ss_filter == NULL) {
/* pmstore to network.persocket.filter frees this if changing */
@@ -38,17 +39,51 @@ ss_open_stream()
fp = fopen(path, "r");
if (pmDebugOptions.appl0)
fprintf(stderr, "ss_open_stream: open PCPQA_PMDA_SOCKETS=%s\n", path);
+ using_pipe = 0;
} else {
+ __pmExecCtl_t *argp = NULL;
+ int sts;
+
if (access((path = "/usr/sbin/ss"), X_OK) != 0) {
if (access((path = "/usr/bin/ss"), X_OK) != 0) {
fprintf(stderr, "Error: no \"ss\" binary found\n");
return NULL;
}
}
- pmsprintf(cmd, sizeof(cmd), "%s %s %s", path, SS_OPTIONS, ss_filter);
- fp = popen(cmd, "r");
+ if ((sts = __pmProcessAddArg(&argp, path)) < 0 ||
+ (sts = __pmProcessAddArg(&argp, SS_OPTIONS)) < 0) {
+ if (pmDebugOptions.appl0)
+ fprintf(stderr, "ss_open_stream: __pmProcessAddArg failed: %s\n",
+ pmErrStr(sts));
+ return NULL;
+ }
+ if (ss_filter[0] != '\0') {
+ char *s, *tok, *saveptr;
+
+ if ((s = strdup(ss_filter)) == NULL)
+ return NULL;
+ for (tok = strtok_r(s, " \t", &saveptr); tok != NULL;
+ tok = strtok_r(NULL, " \t", &saveptr)) {
+ if ((sts = __pmProcessAddArg(&argp, tok)) < 0) {
+ free(s);
+ if (pmDebugOptions.appl0)
+ fprintf(stderr, "ss_open_stream: __pmProcessAddArg failed: %s\n",
+ pmErrStr(sts));
+ return NULL;
+ }
+ }
+ free(s);
+ }
+ if ((sts = __pmProcessPipe(&argp, "r", PM_EXEC_TOSS_NONE, &fp)) < 0) {
+ if (pmDebugOptions.appl0)
+ fprintf(stderr, "ss_open_stream: __pmProcessPipe failed: %s\n",
+ pmErrStr(sts));
+ return NULL;
+ }
if (pmDebugOptions.appl0)
- fprintf(stderr, "ss_open_stream: popen %s\n", cmd);
+ fprintf(stderr, "ss_open_stream: exec %s %s %s\n",
+ path, SS_OPTIONS, ss_filter);
+ using_pipe = 1;
}
return fp;
@@ -57,8 +92,8 @@ ss_open_stream()
void
ss_close_stream(FILE *fp)
{
- if (getenv("PCPQA_PMDA_SOCKETS") != NULL)
- fclose(fp);
+ if (using_pipe)
+ __pmProcessPipeClose(fp);
else
- pclose(fp);
+ fclose(fp);
}

207
pcp-RHEL-213687.patch Normal file
View File

@ -0,0 +1,207 @@
From 7e27614006 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp, libpcp_pmda: set FD_CLOEXEC on AF_UNIX sockets (CWE-403)
The __pmInitSocket() function returns early for AF_UNIX sockets,
skipping all subsequent socket hardening including FD_CLOEXEC.
This causes the pmdaroot Unix socket fd to be inherited by child
processes spawned via popen()/fork(), enabling privilege escalation
when combined with the linux_sockets command injection (vuln 3):
an attacker's popen() child inherits the pmdaroot fd and can send
a PDUROOT_STARTPMDA_REQ to execute commands as root.
Fix:
- Set FD_CLOEXEC on AF_UNIX sockets in __pmInitSocket() before the
early return, matching the behavior TCP sockets get via
__pmConnectRestoreFlags()
- Set FD_CLOEXEC on pmdarootfd in pmdaRootConnect() after connect()
succeeds, as belt-and-suspenders for this critical fd
- Add qa/src/check_cloexec.c and qa/2104 verifying FD_CLOEXEC is set
on sockets created by __pmCreateUnixSocket()
Note: SO_PEERCRED peer credential verification on the pmdaroot server
side is a separate hardening measure to be addressed as a follow-up.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves: RHEL-213687 CVE-2026-16526
---
diff --git a/qa/2104 b/qa/2104
new file mode 100755
index 0000000000..0b2b0b602e
--- /dev/null
+++ b/qa/2104
@@ -0,0 +1,31 @@
+#!/bin/sh
+# PCP QA Test No. 2104
+# Verify AF_UNIX sockets have FD_CLOEXEC set (CWE-403 fix)
+#
+# 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
+
+[ -f src/check_cloexec ] || _notrun "check_cloexec not built"
+
+_cleanup()
+{
+ cd $here
+ $sudo rm -rf $tmp $tmp.*
+}
+
+status=0 # success is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+# real QA test starts here
+src/check_cloexec
+
+# success, all done
+exit
diff --git a/qa/2104.out b/qa/2104.out
new file mode 100644
index 0000000000..900a95ec75
--- /dev/null
+++ b/qa/2104.out
@@ -0,0 +1,2 @@
+QA output created by 2104
+FD_CLOEXEC is set
diff --git a/qa/group b/qa/group
index e538db58e6..c7db6290b2 100644
--- a/qa/group
+++ b/qa/group
@@ -2218,4 +2218,5 @@ pmcd.pdu
1992 pmda.uwsgi local
2105 libpcp local security
2100 pmproxy local security
+2104 libpcp local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/qa/src/GNUlocaldefs b/qa/src/GNUlocaldefs
index f838260680..6ee74c7f16 100644
--- a/qa/src/GNUlocaldefs
+++ b/qa/src/GNUlocaldefs
@@ -54,7 +54,8 @@ CFILES = disk_test.c exercise.c context_test.c chkoptfetch.c \
stampconv.c time_stamp.c archend.c scandata.c wait_for_values.c \
dumpstack.c usergroup.c derived_help.c ready-or-not.c cleanmapdir.c \
throttle.c throttle_timeout.c y2038.c bigpmcdpmids.c pdu-gadget.c \
- newcontext.c
+ newcontext.c \
+ check_cloexec.c
ifeq ($(shell test -f ../localconfig && echo 1), 1)
include ../localconfig
@@ -574,6 +575,11 @@ sortinst: sortinst.c
# --- need libpcp_import
#
+check_cloexec: check_cloexec.c
+ rm -f $@
+ $(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS)
+ $(LINKER_MAKERULE)
+
check_import: check_import.c
rm -f $@
$(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS) -lpcp_import
@@ -880,6 +886,8 @@ xmktime.o: libpcp.h
xxx.o: libpcp.h
y2038.o: libpcp.h
+check_cloexec.o: libpcp.h
+
bozo:
@echo CFILES_TARGETS=$(CFILES_TARGETS)
@echo "patsubst ->" $(patsubst %.c,%,$(CFILES_TARGETS))
diff --git a/qa/src/check_cloexec.c b/qa/src/check_cloexec.c
new file mode 100644
index 0000000000..ebc438d301
--- /dev/null
+++ b/qa/src/check_cloexec.c
@@ -0,0 +1,38 @@
+/*
+ * Verify that AF_UNIX sockets created by __pmCreateUnixSocket()
+ * have FD_CLOEXEC set.
+ */
+
+#include <pcp/pmapi.h>
+#include "libpcp.h"
+#include <fcntl.h>
+
+int
+main(int argc, char **argv)
+{
+ int fd, flags;
+
+ pmSetProgname(argv[0]);
+
+ fd = __pmCreateUnixSocket();
+ if (fd < 0) {
+ fprintf(stderr, "Error: __pmCreateUnixSocket failed: %s\n",
+ pmErrStr(fd));
+ return 1;
+ }
+
+ flags = fcntl(fd, F_GETFD);
+ if (flags < 0) {
+ fprintf(stderr, "Error: fcntl F_GETFD failed\n");
+ close(fd);
+ return 1;
+ }
+
+ if (flags & FD_CLOEXEC)
+ printf("FD_CLOEXEC is set\n");
+ else
+ printf("FAIL: FD_CLOEXEC is NOT set\n");
+
+ close(fd);
+ return 0;
+}
diff --git a/src/libpcp/src/auxconnect.c b/src/libpcp/src/auxconnect.c
index 62f6f54517..67227e3c6b 100644
--- a/src/libpcp/src/auxconnect.c
+++ b/src/libpcp/src/auxconnect.c
@@ -516,8 +516,12 @@ __pmInitSocket(int fd, int family)
}
#if defined(HAVE_STRUCT_SOCKADDR_UN)
- if (family == AF_UNIX)
+ if (family == AF_UNIX) {
+ int fdFlags;
+ if ((fdFlags = __pmGetFileDescriptorFlags(fd)) >= 0)
+ __pmSetFileDescriptorFlags(fd, fdFlags | FD_CLOEXEC);
return fd;
+ }
#endif
/* Avoid 200 ms delay. This option is not supported for unix domain sockets. */
diff --git a/src/libpcp_pmda/src/root.c b/src/libpcp_pmda/src/root.c
index 1d3223b572..3950e4f1ec 100644
--- a/src/libpcp_pmda/src/root.c
+++ b/src/libpcp_pmda/src/root.c
@@ -32,7 +32,7 @@ pmdaRootConnect(const char *path)
char *tmpdir;
char socketpath[MAXPATHLEN];
char errmsg[PM_MAXERRMSGLEN];
- int fd, sts, version, features;
+ int fd, sts, version, features, fdFlags;
/* Initialize the socket address. */
if ((addr = __pmSockAddrAlloc()) == NULL)
@@ -72,6 +72,9 @@ pmdaRootConnect(const char *path)
return sts;
}
+ if ((fdFlags = __pmGetFileDescriptorFlags(fd)) >= 0)
+ __pmSetFileDescriptorFlags(fd, fdFlags | FD_CLOEXEC);
+
/* Check server connection information */
if ((sts = __pmdaRecvRootPDUInfo(fd, &version, &features)) < 0) {
pmNotifyErr(LOG_ERR,

196
pcp-RHEL-213711.patch Normal file
View File

@ -0,0 +1,196 @@
From d96ba5a716 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] pmproxy: fix missing -Q and -S authentication flags (CWE-306)
The pmproxy -Q (require client certificate) and -S (require
authenticated clients) flags existed as case blocks in the option
parser but were absent from the short_options string and the longopts
table, making them permanently unreachable. An unauthenticated HTTP
client could access all REST API endpoints including /store and /derive.
Fix:
- Add Q and S to short_options so pmgetopt_r() delivers them
- Add --certreqd and --reqauth entries to the longopts table
- Document both flags in the pmproxy(1) man page
- Add qa/2100 verifying the flags are accepted and that -S correctly
rejects unauthenticated REST API requests with HTTP 403
Note: -S enforcement in the REST API path already exists in http.c and
webapi.c. -Q (CERT_REQD) enforcement is only implemented for the
legacy PCP wire protocol path, not the REST API; this is a pre-existing
limitation to be addressed separately.
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
Reported-by: Massimiliano Brolli, TIM Security Red Team
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves: RHEL-213711 CVE-2026-16527
---
diff --git a/man/man1/pmproxy.1 b/man/man1/pmproxy.1
index f2f831b6ec..65d66c5d21 100644
--- a/man/man1/pmproxy.1
+++ b/man/man1/pmproxy.1
@@ -229,6 +229,9 @@ Specify an alternate
number to listen on for client connections.
The default value is 44322.
.TP
+\fB\-Q\f1, \fB\-\-certreqd\f1
+Require that all client connections provide a trusted client certificate.
+.TP
\f3\-r\f1 \f2port\f1, \f3\-\-keyport\f1=\f2port\f1
Specify an alternate key-value server
.I port
@@ -243,6 +246,9 @@ The default value is
.IR $PCP_RUN_DIR/pmproxy.socket .
This option implies \f3pmproxy\f1 is running in \f3timeseries\f1 mode.
.TP
+\fB\-S\f1, \fB\-\-reqauth\f1
+Require that all client connections be authenticated.
+.TP
\fB\-t\f1, \fB\-\-timeseries\f1
Operate in automatic archive timeseries discovery mode.
This mode of operation will enable the
diff --git a/qa/2100 b/qa/2100
new file mode 100755
index 0000000000..1de957a2f1
--- /dev/null
+++ b/qa/2100
@@ -0,0 +1,85 @@
+#!/bin/sh
+# PCP QA Test No. 2100
+# Verify pmproxy -Q and -S authentication flags are accepted
+# and that -S (reqauth) enforces authentication on REST API
+#
+# 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"
+
+_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
+
+# real QA test starts here
+
+echo "=== checking -Q and -S appear in usage ==="
+pmproxy --help 2>&1 | grep -E '\-[QS]' | sed -e 's/^ *//'
+
+echo
+echo "=== checking -S enforces authentication on REST API ==="
+__port=`_find_free_port`
+$PCP_BINADM_DIR/pmproxy -S -f -p $__port -l $tmp.log &
+__pid=$!
+sleep 1
+if kill -0 $__pid 2>/dev/null; then
+ echo "pmproxy with -S started"
+
+ # unauthenticated request should be rejected
+ __code=`curl -s -o /dev/null -w '%{http_code}' "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null`
+ if [ "$__code" = "403" ]; then
+ echo "unauthenticated request correctly rejected (HTTP $__code)"
+ else
+ echo "FAIL: expected HTTP 403, got HTTP $__code"
+ fi
+
+ kill $__pid
+ wait $__pid 2>/dev/null
+ __pid=""
+else
+ echo "FAIL: pmproxy with -S did not start"
+fi
+
+echo
+echo "=== checking without -S allows unauthenticated access ==="
+__port=`_find_free_port`
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log2 &
+__pid=$!
+sleep 1
+if kill -0 $__pid 2>/dev/null; then
+ echo "pmproxy without -S started"
+
+ # unauthenticated request should succeed
+ __code=`curl -s -o /dev/null -w '%{http_code}' "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null`
+ if [ "$__code" = "200" ]; then
+ echo "unauthenticated request correctly allowed (HTTP $__code)"
+ else
+ echo "FAIL: expected HTTP 200, got HTTP $__code"
+ fi
+
+ kill $__pid
+ wait $__pid 2>/dev/null
+ __pid=""
+else
+ echo "FAIL: pmproxy without -S did not start"
+fi
+
+# success, all done
+exit
diff --git a/qa/2100.out b/qa/2100.out
new file mode 100644
index 0000000000..5302aea77a
--- /dev/null
+++ b/qa/2100.out
@@ -0,0 +1,12 @@
+QA output created by 2100
+=== checking -Q and -S appear in usage ===
+-Q, --certreqd require client certificate authentication
+-S, --reqauth require all client connections to be authenticated
+
+=== checking -S enforces authentication on REST API ===
+pmproxy with -S started
+unauthenticated request correctly rejected (HTTP 403)
+
+=== checking without -S allows unauthenticated access ===
+pmproxy without -S started
+unauthenticated request correctly allowed (HTTP 200)
diff --git a/qa/group b/qa/group
index 80a861c5f6..e538db58e6 100644
--- a/qa/group
+++ b/qa/group
@@ -2217,4 +2217,5 @@ pmcd.pdu
1991 pcp netstat python local
1992 pmda.uwsgi local
2105 libpcp local security
+2100 pmproxy local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/src/pmproxy/src/pmproxy.c b/src/pmproxy/src/pmproxy.c
index 845479c467..ff18396bfa 100644
--- a/src/pmproxy/src/pmproxy.c
+++ b/src/pmproxy/src/pmproxy.c
@@ -82,7 +82,9 @@ static pmLongOptions longopts[] = {
PMAPI_OPTIONS_HEADER("Connection options"),
{ "interface", 1, 'i', "ADDR", "accept connections on this IP address" },
{ "port", 1, 'p', "PORT", "accept connections on this port" },
+ { "certreqd", 0, 'Q', 0, "require client certificate authentication" },
{ "socket", 1, 's', "PATH", "Unix domain socket file [default $PCP_RUN_DIR/pmproxy.socket]" },
+ { "reqauth", 0, 'S', 0, "require all client connections to be authenticated" },
{ "keyport", 1, 'r', "PORT", "Connect to key server on this TCP/IP port (implies --timeseries)" },
{ "keyhost", 1, 'h', "HOST", "Connect to key server on this host name (implies --timeseries)" },
{ "redisport", 1, 'r', "PORT", "Backwards-compatibility option, do not use" },
@@ -95,7 +97,7 @@ static pmLongOptions longopts[] = {
};
static pmOptions opts = {
- .short_options = "Ac:dD:Ffh:i:l:L:p:r:s:tT:U:x:?",
+ .short_options = "Ac:dD:Ffh:i:l:L:p:Qr:s:StT:U:x:?",
.long_options = longopts,
};

2092
pcp-RHEL-213736.patch Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
Name: pcp
Version: 6.3.7
Release: 10%{?dist}
Release: 11%{?dist}
Summary: System-level performance monitoring and performance management
License: GPL-2.0-or-later AND LGPL-2.1-or-later AND CC-BY-3.0
URL: https://pcp.io
@ -31,6 +31,30 @@ Patch17: ds389.patch
# https://github.com/performancecopilot/pcp/commit/b72da5135df7fb08ca432db1e2db24478f43aea2
# https://github.com/performancecopilot/pcp/commit/edfd909edac8ad4762aa150e330f92211df98c1c
Patch18: pcp-RHEL-213747.patch
# https://issues.redhat.com/browse/RHEL-213736
# https://github.com/performancecopilot/pcp/commit/ef848fb978c8b2f0bf8c6777bb478635aba6c6dc5
Patch19: pcp-RHEL-213736.patch
# https://issues.redhat.com/browse/RHEL-213711
# https://github.com/performancecopilot/pcp/commit/d96ba5a716c8b2f0bf8c6777bb478635aba6c6dc5
Patch20: pcp-RHEL-213711.patch
# https://issues.redhat.com/browse/RHEL-213687
# https://github.com/performancecopilot/pcp/commit/7e27614006ff6fc4925991edbedaf1eab6b14731
Patch21: pcp-RHEL-213687.patch
# https://issues.redhat.com/browse/RHEL-213658
# https://github.com/performancecopilot/pcp/commit/c5cbeceb7d8b2f0bf8c6777bb478635aba6c6dc5
Patch22: pcp-RHEL-213658.patch
Patch23: pcp-6.3.7-pmlogmv-command-injection.patch
Patch24: pcp-6.3.7-pmieconf-command-injection.patch
Patch25: pcp-6.3.7-OOB-pmLogLoadLabelSet.patch
Patch26: pcp-6.3.7-OOB-pmDiscoverDecodeMetaInDom.patch
Patch27: pcp-6.3.7-OOB-pmDecodeLabel.patch
Patch28: pcp-6.3.7-OOB-pmDecodeLogStatus.patch
Patch29: pcp-6.3.7-OOB-pmDecodeInstance.patch
Patch30: pcp-6.3.7-pducrash-oob-tests.patch
Patch31: pcp-6.3.7-timezone-zoneinfo-validation.patch
Patch32: pcp-6.3.7-pmdaroot-peer-credentials.patch
Patch33: pcp-6.3.7-pmproxy-rest-certreqd.patch
Patch34: pcp-6.3.7-scanmeta-LogLoadInDom-caller.patch
%if 0%{?fedora} >= 40 || 0%{?rhel} >= 10
ExcludeArch: %{ix86}
@ -3641,6 +3665,15 @@ fi
%files zeroconf -f pcp-zeroconf-files.rpm
%changelog
* Thu Aug 13 2026 Jan Kurik <jkurik@redhat.com> - 6.3.7-11
- Backport PCP security CVE fixes for RHEL 9.9
- Fix integer overflow in __pmGetPDU() (CVE-2026-16529)
- Fix missing pmproxy -Q/-S authentication flags (CVE-2026-16527)
- Set FD_CLOEXEC on AF_UNIX sockets (CVE-2026-16526)
- Fix command injection in linux_sockets PMDA (CVE-2026-16524)
- Backport applicable private-pcp security hardening fixes
- CVE-2026-16531 is not applicable (pmproxy logger servlet absent in 6.3.7)
* Thu Jul 30 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 6.3.7-10
- Fix arbitrary pointer dereference in __pmLogLoadInDom (CVE-2026-16530)