import CS pcp-6.3.7-13.el9

This commit is contained in:
AlmaLinux RelEng Bot 2026-09-03 07:52:18 -04:00
parent 6c2a4da866
commit 66f7d33737
18 changed files with 2662 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);

View File

@ -0,0 +1,255 @@
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,69 @@
+#!/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
+
+# 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);
}

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,

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,
};

View File

@ -0,0 +1,128 @@
From ef848fb978 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] libpcp: fix integer overflow in __pmGetPDU() (CWE-190)
When php->len is near INT_MAX (e.g. 0x7FFFFFFF), the buffer size
computation PDU_CHUNK * (1 + php->len / PDU_CHUNK) overflows signed
int, producing a negative value that permanently corrupts the static
maxsize variable. Every subsequent __pmFindPDUBuf() call returns NULL,
rendering the affected daemon (pmlogger, pmcd) unable to process any
further PDUs for the remainder of its lifetime — a persistent denial
of service requiring a restart.
Fix: add an overflow guard (php->len > INT_MAX - PDU_CHUNK) before
the multiplication, returning PM_ERR_TOOBIG for absurdly large PDU
lengths. This protects the NO_LIMIT code path used by pmcd and
pmlogger that is not covered by the existing ceiling check.
Also add _filter_pmcd() to qa/common.pmcd.pdu to normalize fd=N in
pmcd log output, and qa/2105 with a crafted PDU exercising the
overflow.
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-213736 CVE-2026-16529
---
diff --git a/qa/2105 b/qa/2105
new file mode 100755
index 0000000000..44b849ad39
--- /dev/null
+++ b/qa/2105
@@ -0,0 +1,20 @@
+#!/bin/sh
+# PCP QA Test No. 2105
+# Verify __pmGetPDU rejects PDU with len near INT_MAX
+# (integer overflow in buffer size computation, CWE-190)
+#
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
+#
+
+seq=`basename $0`
+echo "QA output created by $seq"
+
+mkdir -p pdudata
+printf '\177\377\377\377\000\000\200\000' >pdudata/pdu-getpdu-overflow
+
+pdu_data=pdudata/pdu-getpdu-overflow
+grep_pattern="bad PDU len=.*exceeds maximum|PDU len=.*too large"
+
+# this is one of the generic pmcd PDU exerciser tests ...
+#
+. ./common.pmcd.pdu
diff --git a/qa/2105.out b/qa/2105.out
new file mode 100644
index 0000000000..a304f9d913
--- /dev/null
+++ b/qa/2105.out
@@ -0,0 +1,10 @@
+QA output created by 2105
+expect error(s) to be logged ...
+__pmGetPDU: fd=N type=0x8000 bad PDU len=2147483647 in hdr exceeds maximum client PDU size (65536)
+
+and no valgrind badness ...
+Memcheck, a memory error detector
+LEAK SUMMARY:
+definitely lost: 0 bytes in 0 blocks
+indirectly lost: 0 bytes in 0 blocks
+ERROR SUMMARY: 0 errors from 0 contexts ...
diff --git a/qa/common.pmcd.pdu b/qa/common.pmcd.pdu
index d9e3c6c5c9..9882284b5c 100644
--- a/qa/common.pmcd.pdu
+++ b/qa/common.pmcd.pdu
@@ -55,6 +55,14 @@ _filter()
# end
}
+_filter_pmcd()
+{
+ sed \
+ -e 's/fd=[0-9][0-9]*/fd=N/g' \
+ -e 's/^\[.*\] pmcd([0-9]*) [A-Za-z]*: //' \
+ # end
+}
+
mkdir $tmp || exit 1
cd $tmp
grep sampledso $PCP_PMCDCONF_PATH >pmcd.conf
@@ -91,7 +99,7 @@ wait
[ -s $tmp.err ] && cat $tmp.err
echo "expect error(s) to be logged ..."
-grep -E "$grep_pattern" pmcd.log
+grep -E "$grep_pattern" pmcd.log | _filter_pmcd
echo
echo "and no valgrind badness ..."
diff --git a/qa/group b/qa/group
index d4da513ce2..80a861c5f6 100644
--- a/qa/group
+++ b/qa/group
@@ -2216,4 +2216,5 @@ pmcd.pdu
1990 pcp buddyinfo python local
1991 pcp netstat python local
1992 pmda.uwsgi local
+2105 libpcp local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/src/libpcp/src/pdu.c b/src/libpcp/src/pdu.c
index 5845932be1..0a4ae75b25 100644
--- a/src/libpcp/src/pdu.c
+++ b/src/libpcp/src/pdu.c
@@ -658,6 +658,14 @@ check_read_len:
PM_LOCK(pdu_lock);
if (php->len > maxsize) {
+ if (php->len > INT_MAX - PDU_CHUNK) {
+ PM_UNLOCK(pdu_lock);
+ if (pmDebugOptions.pdu)
+ pmNotifyErr(LOG_ERR, "%s: fd=%d PDU len=%d too large",
+ __FUNCTION__, fd, php->len);
+ __pmUnpinPDUBuf(pdubuf);
+ return PM_ERR_TOOBIG;
+ }
tmpsize = PDU_CHUNK * ( 1 + php->len / PDU_CHUNK);
maxsize = tmpsize;
}

View File

@ -0,0 +1,317 @@
From 1f232730588b2f0bf8c6777bb478635aba6c6dc5 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Date: Thu, 2 Jul 2026 15:10:16 +1000
Subject: [PATCH 1/3] libpcp: fix arbitrary pointer deref in __pmLogLoadInDom
(CWE-125/822)
The bounds check on string indices (idx > max_idx) in __pmLogLoadInDom()
was guarded by if (acp != NULL), making it unreachable from the streaming
path used by pmproxy (which passes acp=NULL). An attacker could submit
a TYPE_INDOM record with an out-of-range stridx value via POST
/logger/meta, causing namelist[i] to point to arbitrary heap memory.
Fix:
- Add minimum rlen checks before reading fixed fields, using macros
derived from the on-disk struct sizes (INDOM_V3_MINRLEN, INDOM_V2_MINRLEN)
- Validate numinst against rlen before using it in arithmetic, preventing
integer overflow in the max_idx computation
- Make max_idx computation and idx bounds check unconditional (remove the
acp != NULL guard) so they protect both archive and streaming paths
- Extend qa/src/pducrash.c with decode_log_indom() exercising all four
failure modes via __pmLogLoadInDom(NULL, ...)
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>
---
qa/513.out | 8 +++++
qa/src/pducrash.c | 77 ++++++++++++++++++++++++++++++++++++++++
src/libpcp/src/e_indom.c | 63 ++++++++++++++++++++------------
3 files changed, 126 insertions(+), 22 deletions(-)
diff --git a/qa/513.out b/qa/513.out
index 5894ca159..7ccaa1f37 100644
--- a/qa/513.out
+++ b/qa/513.out
@@ -269,6 +269,14 @@ QA output created by 513
__pmDecodeDescs: sts = -12366 (IPC protocol failure)
[descs] checking access beyond extended buffer
__pmDecodeDescs: sts = -12366 (IPC protocol failure)
+[log_indom] checking rlen too small for v3 header
+ __pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
+[log_indom] checking rlen too small for v2 header
+ __pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
+[log_indom] checking numinst larger than rlen allows
+ __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)
=== filtered valgrind report ===
Memcheck, a memory error detector
Command: src/pducrash
diff --git a/qa/src/pducrash.c b/qa/src/pducrash.c
index 26dbd2414..76e1e0ce7 100644
--- a/qa/src/pducrash.c
+++ b/qa/src/pducrash.c
@@ -1627,6 +1627,82 @@ decode_trace_data(const char *name)
free(trace_data);
}
+/*
+ * Test __pmLogLoadInDom with acp==NULL (streaming path used by pmproxy).
+ * The on-disk v3 record layout (after len+type header) is:
+ * sec[2], nsec, indom, numinst, instlist[numinst],
+ * stridx[numinst], name_strings
+ * rlen is the body length excluding the 2-word header.
+ */
+static void
+decode_log_indom(const char *name)
+{
+ int sts;
+ __pmLogInDom lid;
+ __int32_t *buf;
+
+ /* TYPE_INDOM (v3): rlen too small for fixed header fields */
+ fprintf(stderr, "[%s] checking rlen too small for v3 header\n", name);
+ {
+ __int32_t tiny[1];
+ memset(&lid, 0, sizeof(lid));
+ memset(tiny, 0, sizeof(tiny));
+ buf = tiny;
+ sts = __pmLogLoadInDom(NULL, 4, TYPE_INDOM, &lid, &buf);
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
+ }
+
+ /* TYPE_INDOM_V2: rlen too small for fixed header fields */
+ fprintf(stderr, "[%s] checking rlen too small for v2 header\n", name);
+ {
+ __int32_t tiny[1];
+ memset(&lid, 0, sizeof(lid));
+ memset(tiny, 0, sizeof(tiny));
+ buf = tiny;
+ sts = __pmLogLoadInDom(NULL, 4, TYPE_INDOM_V2, &lid, &buf);
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
+ }
+
+ /* TYPE_INDOM (v3): numinst too large for rlen */
+ fprintf(stderr, "[%s] checking numinst larger than rlen allows\n", name);
+ {
+ /* v3 fixed fields: sec[2]+nsec+indom+numinst = 5 words (20 bytes) */
+ __int32_t rec[7]; /* room for header + fixed fields */
+ memset(&lid, 0, sizeof(lid));
+ memset(rec, 0, sizeof(rec));
+ rec[0] = htonl(sizeof(rec)); /* len (not used but for completeness) */
+ rec[1] = htonl(TYPE_INDOM); /* type */
+ /* sec[2], nsec, indom are zero */
+ rec[6] = htonl(999999); /* numinst - way too large */
+ buf = &rec[2]; /* skip len+type, as __pmLogLoadInDom expects */
+ sts = __pmLogLoadInDom(NULL, 20, TYPE_INDOM, &lid, &buf);
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
+ }
+
+ /* TYPE_INDOM (v3): valid numinst=1 but stridx out of range */
+ fprintf(stderr, "[%s] checking out-of-range stridx with acp==NULL\n", name);
+ {
+ /*
+ * Layout after len+type: sec[2], nsec, indom, numinst,
+ * instlist[1], stridx[1], (no string data)
+ * That's 5 + 1 + 1 = 7 words = 28 bytes of body.
+ */
+ __int32_t rec[9]; /* 2 (header) + 7 (body) */
+ memset(&lid, 0, sizeof(lid));
+ memset(rec, 0, sizeof(rec));
+ rec[0] = htonl(sizeof(rec)); /* len */
+ rec[1] = htonl(TYPE_INDOM); /* type */
+ /* sec[2], nsec, indom are zero */
+ rec[6] = htonl(1); /* numinst */
+ rec[7] = htonl(0); /* instlist[0] = 0 */
+ rec[8] = htonl(0x7FFFFFFF); /* stridx[0] = huge OOB index */
+ buf = &rec[2];
+ sts = __pmLogLoadInDom(NULL, 28, TYPE_INDOM, &lid, &buf);
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
+ if (sts >= 0) __pmFreeLogInDom(&lid);
+ }
+}
+
typedef void (*decode_t)(const char *);
struct pdu {
@@ -1661,6 +1737,7 @@ struct pdu {
{ "highres_result", decode_highres_result },
{ "desc_ids", decode_desc_ids },
{ "descs", decode_descs },
+ { "log_indom", decode_log_indom },
};
int
diff --git a/src/libpcp/src/e_indom.c b/src/libpcp/src/e_indom.c
index 8036fc6cc..de44751f0 100644
--- a/src/libpcp/src/e_indom.c
+++ b/src/libpcp/src/e_indom.c
@@ -51,6 +51,10 @@ typedef struct {
/* will be expanded if numinst > 0 */
} __pmInDom_v2;
+/* Minimum rlen (record body without len+type header) to read fixed fields */
+#define INDOM_V3_MINRLEN (sizeof(__pmInDom_v3) - 2 * sizeof(__int32_t))
+#define INDOM_V2_MINRLEN (sizeof(__pmInDom_v2) - 2 * sizeof(__int32_t))
+
/*
* pack an indom into a physical metadata record
* - lcp required to provide archive version (else NULL)
@@ -258,33 +262,55 @@ PM_FAULT_POINT("libpcp/" __FILE__ ":3", PM_FAULT_ALLOC);
if (type == TYPE_INDOM || type == TYPE_INDOM_DELTA) {
__pmInDom_v3 *v3;
+ if (rlen < (int)INDOM_V3_MINRLEN) {
+ if (pmDebugOptions.logmeta)
+ fprintf(stderr, "__pmLogLoadInDom: v3 rlen=%d too small (min=%d)\n",
+ rlen, (int)INDOM_V3_MINRLEN);
+ goto bad;
+ }
v3 = (__pmInDom_v3 *)&lbuf[-2]; /* len+type not in buf */
__pmLoadTimestamp(&v3->sec[0], &lidp->stamp);
k = (sizeof(v3->sec)+sizeof(v3->nsec))/sizeof(__int32_t);
lidp->indom = __ntohpmInDom(v3->indom);
k++;
lidp->numinst = ntohl(v3->numinst);
+ if (lidp->numinst < 0 ||
+ lidp->numinst > (rlen - (int)INDOM_V3_MINRLEN) / (2 * (int)sizeof(__int32_t))) {
+ if (pmDebugOptions.logmeta)
+ fprintf(stderr, "__pmLogLoadInDom: v3 numinst=%d not consistent with rlen=%d\n",
+ lidp->numinst, rlen);
+ goto bad;
+ }
k++;
lidp->instlist = (int *)&v3->data;
- if (acp != NULL) {
- /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
- max_idx = rlen - 5*sizeof(__int32_t) - 2*lidp->numinst*sizeof(__int32_t);
- }
+ /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
+ max_idx = rlen - (int)INDOM_V3_MINRLEN - 2 * lidp->numinst * (int)sizeof(__int32_t);
}
else if (type == TYPE_INDOM_V2) {
__pmInDom_v2 *v2;
+ if (rlen < (int)INDOM_V2_MINRLEN) {
+ if (pmDebugOptions.logmeta)
+ fprintf(stderr, "__pmLogLoadInDom: v2 rlen=%d too small (min=%d)\n",
+ rlen, (int)INDOM_V2_MINRLEN);
+ goto bad;
+ }
v2 = (__pmInDom_v2 *)&lbuf[-2]; /* len+type not in lbuf */
__pmLoadTimeval(&v2->sec, &lidp->stamp);
k = (sizeof(v2->sec)+sizeof(v2->usec))/sizeof(__int32_t);
lidp->indom = __ntohpmInDom(v2->indom);
k++;
lidp->numinst = ntohl(v2->numinst);
+ if (lidp->numinst < 0 ||
+ lidp->numinst > (rlen - (int)INDOM_V2_MINRLEN) / (2 * (int)sizeof(__int32_t))) {
+ if (pmDebugOptions.logmeta)
+ fprintf(stderr, "__pmLogLoadInDom: v2 numinst=%d not consistent with rlen=%d\n",
+ lidp->numinst, rlen);
+ goto bad;
+ }
k++;
lidp->instlist = (int *)&v2->data;
- if (acp != NULL) {
- /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
- max_idx = rlen - 4*sizeof(__int32_t) - 2*lidp->numinst*sizeof(__int32_t);
- }
+ /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
+ max_idx = rlen - (int)INDOM_V2_MINRLEN - 2 * lidp->numinst * (int)sizeof(__int32_t);
}
else {
if (pmDebugOptions.logmeta)
@@ -327,21 +353,14 @@ PM_FAULT_POINT("libpcp/" __FILE__ ":4", PM_FAULT_ALLOC);
}
idx = ntohl(stridx[i]);
if (idx >= 0) {
- if (acp != NULL) {
- /*
- * crude sanity check ... if the index points to the
- * start of the name that is past the end of the input
- * record, the record is corrupted
- */
- if (idx > max_idx) {
- if (pmDebugOptions.logmeta) {
- char strbuf[20];
- fprintf(stderr, "__pmLogLoadInDom: InDom: %s instance[%d]: bad string index (%d) > max index based on record length (%d)\n",
- pmInDomStr_r(lidp->indom, strbuf, sizeof(strbuf)),
- i, idx, max_idx);
- }
- goto bad;
+ if (idx > max_idx) {
+ if (pmDebugOptions.logmeta) {
+ char strbuf[20];
+ fprintf(stderr, "__pmLogLoadInDom: InDom: %s instance[%d]: bad string index (%d) > max index based on record length (%d)\n",
+ pmInDomStr_r(lidp->indom, strbuf, sizeof(strbuf)),
+ i, idx, max_idx);
}
+ goto bad;
}
lidp->namelist[i] = &namebase[idx];
if (pmDebugOptions.logmeta && pmDebugOptions.desperate)
From d16c0cbdc680a539a64433716d6888003e086c81 Mon Sep 17 00:00:00 2001
From: Ken McDonell <kenj@kenj.id.au>
Date: Sat, 11 Jul 2026 07:29:38 +1000
Subject: [PATCH 2/3] src/pmlogrewrite/indom.c: fix call to __pmLogLoadInDom()
Turns out pmlogrewrite was *using* the acp == NULL guard to dodge the
rlen test and calling with rlen == 0 (this was correct as the *same* record
had previously been processed elsewhere with the correct rlen so the
buffer was know to be good).
Fix involves re-extracting the correct record length and calling
__pmLogLoadInDom() with rlen != 0.
---
src/pmlogrewrite/indom.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/pmlogrewrite/indom.c b/src/pmlogrewrite/indom.c
index eeef646fc..0300aa9a2 100644
--- a/src/pmlogrewrite/indom.c
+++ b/src/pmlogrewrite/indom.c
@@ -223,9 +223,10 @@ _pmUnpackInDom(__int32_t *recbuf, __pmLogInDom *lidp)
}
else {
__int32_t *buf;
+ int len = htonl(hdr->len);
/* buffer for __pmLogLoadInDom has to start AFTER the header */
buf = &recbuf[2];
- sts = __pmLogLoadInDom(NULL, 0, type, lidp, &buf);
+ sts = __pmLogLoadInDom(NULL, len, type, lidp, &buf);
if (sts < 0) {
fprintf(stderr, "_pmUnpackInDom: __pmLogLoadInDom(type=%d): failed: %s\n", type, pmErrStr(sts));
abandon();
From b07e33d85a93a66b63e2c4e579100d0ae999a622 Mon Sep 17 00:00:00 2001
From: Ken McDonell <kenj@kenj.id.au>
Date: Sat, 11 Jul 2026 08:12:58 +1000
Subject: [PATCH 3/3] src/pmlogextract/pmlogextract.c: fix call to
__pmLogLoadInDom()
pmlogextract was also *using* the acp == NULL guard to dodge the rlen
test and calling with rlen == 0.
Fix involves using the correct record length and calling
__pmLogLoadInDom() with rlen != 0.
---
src/pmlogextract/pmlogextract.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pmlogextract/pmlogextract.c b/src/pmlogextract/pmlogextract.c
index 93a3d3988..eac7bf610 100644
--- a/src/pmlogextract/pmlogextract.c
+++ b/src/pmlogextract/pmlogextract.c
@@ -1300,7 +1300,7 @@ write_rec(reclist_t *rec)
memcpy(buf, rec->pdu, rlen);
ibuf = &buf[2];
- sts = __pmLogLoadInDom(NULL, 0, type, &lid, &ibuf);
+ sts = __pmLogLoadInDom(NULL, rlen, type, &lid, &ibuf);
if (sts < 0) {
fprintf(stderr, "write_rec: __pmLogLoadInDom(type=%s (%d)): failed: %s\n", __pmLogMetaTypeStr(type), type, pmErrStr(sts));
}

View File

@ -1,6 +1,6 @@
Name: pcp
Version: 6.3.7
Release: 9%{?dist}
Release: 13%{?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
@ -26,6 +26,35 @@ Patch14: pmda-openmetrics-performance.patch
Patch15: pcp-RHEL-133548.patch
Patch16: memory-leaks.patch
Patch17: ds389.patch
# https://issues.redhat.com/browse/RHEL-213747
# https://github.com/performancecopilot/pcp/commit/ec81e2b35c7dc19a69406d2712d1b9904ac22112
# 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}
@ -3636,6 +3665,25 @@ fi
%files zeroconf -f pcp-zeroconf-files.rpm
%changelog
* Thu Aug 13 2026 Jan Kurik <jkurik@redhat.com> - 6.3.7-13
- Fix qa/2101 to stop using undefined $seq_full on PCP 6.3.7
- Fix qa/2105 to ship malformed PDU test data for CVE-2026-16529
* Thu Aug 13 2026 Jan Kurik <jkurik@redhat.com> - 6.3.7-12
- Fix pcp-RHEL-213736.patch to stop deleting stray .orig backup files
* 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)
* Thu Apr 2 2026 Jan Kurik <jkurik@redhat.com> - 6.3.7-9
- Backported ds389 patch from the upstream (pcp-7.1.1)