import Oracle_OSS pcp-7.0.3-5.0.1.el10_2

This commit is contained in:
AlmaLinux RelEng Bot 2026-08-19 04:34:07 -04:00
parent f5949a2578
commit 5f0a3da4bf
34 changed files with 8947 additions and 465 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
pcp-6.3.7.src.tar.gz
pcp-7.0.3.src.tar.gz
pdu-getpdu-overflow

View File

@ -0,0 +1,38 @@
From c80dc758eb0eb27fafa0b594d6c2aa1f4c3803fa Mon Sep 17 00:00:00 2001
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
Date: Wed, 3 Dec 2025 20:40:52 +0530
Subject: [PATCH] pmdanfsclient: fix regex to correctly parse NFS op stats
The regex used to match NFS operation statistics in /proc/self/mountstats was
missing a capture group, causing parsing failures when an additional field
was present in newer kernel formats. Updated the regex to include the extra
numeric field so that all opstats lines are parsed correctly.
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/85671f8874d7d4b4e57eb45bfe295de92b95415c
Orabug: 38724847
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
---
src/pmdas/nfsclient/pmdanfsclient.python | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/pmdas/nfsclient/pmdanfsclient.python b/src/pmdas/nfsclient/pmdanfsclient.python
index 786d958..f08b93a 100644
--- a/src/pmdas/nfsclient/pmdanfsclient.python
+++ b/src/pmdas/nfsclient/pmdanfsclient.python
@@ -636,7 +636,7 @@ class NFSCLIENTPMDA(PMDA):
line = STATS.readline()
if line == '':
break
- m = re.match(r'\s*([A-Z_]*): (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*)$', line)
+ m = re.match(r'\s*([A-Z_]*): (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*)$', line)
if not m:
break
opname = m.group(1).lower()
--
2.43.7

View File

@ -0,0 +1,575 @@
From ca8fbd2a827e3e7caa72331e003f93523c9f5241 Mon Sep 17 00:00:00 2001
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
Date: Tue, 6 Jan 2026 05:53:08 +0000
Subject: [PATCH] pcp-nfsiostat: Introduce PCP implementation of nfsiostat
parser
Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/c0a0c53b57774dfac0b727e62472fb1ae6065540
Orabug: 38817053
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
---
src/pcp/GNUmakefile | 1 +
src/pcp/nfsiostat/GNUmakefile | 43 ++++
src/pcp/nfsiostat/pcp-nfsiostat.1 | 132 +++++++++++
src/pcp/nfsiostat/pcp-nfsiostat.py | 346 +++++++++++++++++++++++++++++
4 files changed, 522 insertions(+)
create mode 100644 src/pcp/nfsiostat/GNUmakefile
create mode 100644 src/pcp/nfsiostat/pcp-nfsiostat.1
create mode 100644 src/pcp/nfsiostat/pcp-nfsiostat.py
diff --git a/src/pcp/GNUmakefile b/src/pcp/GNUmakefile
index da29269..2b74fc0 100644
--- a/src/pcp/GNUmakefile
+++ b/src/pcp/GNUmakefile
@@ -30,6 +30,7 @@ SUBDIRS = \
mpstat \
netstat \
numastat \
+ nfsiostat \
pidstat \
ps \
python \
diff --git a/src/pcp/nfsiostat/GNUmakefile b/src/pcp/nfsiostat/GNUmakefile
new file mode 100644
index 0000000..d8c5356
--- /dev/null
+++ b/src/pcp/nfsiostat/GNUmakefile
@@ -0,0 +1,43 @@
+#
+# Copyright (c) 2023 Oracle and/or its affiliates.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+
+TOPDIR = ../../..
+include $(TOPDIR)/src/include/builddefs
+
+TARGET = pcp-nfsiostat
+SCRIPT = $(TARGET).py
+MAN_SECTION = 1
+MAN_PAGES = $(TARGET).$(MAN_SECTION)
+MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTION)
+
+default: $(SCRIPT) $(MAN_PAGES)
+
+include $(BUILDRULES)
+
+install: default
+ifeq "$(HAVE_PYTHON)" "true"
+ $(INSTALL) -m 755 $(SCRIPT) $(PCP_BINADM_DIR)/$(TARGET)
+ @$(INSTALL_MAN)
+endif
+
+default_pcp : default
+
+install_pcp : install
+
+check:: $(SCRIPT)
+ $(PYLINT) $^
+
+check :: $(MAN_PAGES)
+ $(MANLINT) $^
diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.1 b/src/pcp/nfsiostat/pcp-nfsiostat.1
new file mode 100644
index 0000000..b297285
--- /dev/null
+++ b/src/pcp/nfsiostat/pcp-nfsiostat.1
@@ -0,0 +1,132 @@
+'\"! tbl | mmdoc
+'\"macro stdmacro
+.\"
+.\" Man page for pcp-nfsiostat
+.\" Copyright (c) 2023 Oracle and/or its affiliates.
+.\" DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+.\"
+.\" This program is free software; you can redistribute it and/or modify it
+.\" under the terms of the GNU General Public License as published by the
+.\" Free Software Foundation; either version 2 of the License, or (at your
+.\" option) any later version.
+.\"
+.\" This program is distributed in the hope that it will be useful, but
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+.\" or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+.\" for more details.
+.\"
+
+.TH PCP-NFSIOSTAT 1 "PCP" "Performance Co-Pilot"
+
+.SH NAME
+\f3pcp-nfsiostat\f1 \- Emulate iostat for NFS mount points using /proc/self/mountstats
+
+.SH SYNOPSIS
+\fBpcp\fP [\fBpcp options\fP] \fBnfsiostat\fP [\fB-s\fP \fBsamples\fP] [\fB-a\fP \fBarchive\fP] [\fB-Z\fP \fB--timezone\fP] [\fB-z\fP \fB--hostzone\fP] [\fB-V\fP \fBVersion\fP]
+
+.SH DESCRIPTION
+The\fB pcp-nfsiostat \fPcommand reports client-side\fB Network File System (NFS) \fPI/O statistics for each mounted NFS filesystem. For every NFS mountpoint, the output consists of a summary section followed by detailed read and write statistics blocks. These statistics include operation rates, data throughput, latency, retransmissions, queueing delays, and error counts. By default,\fB pcp-nfsiostat \fPmonitors NFS mountpoints on the local host and reports live metrics collected via the \fB PCP NFS PMDA\fP. When an archive is specified, historical metrics are reported instead.
+
+.SH OUTPUT FORMAT
+Statistics are reported per mounted NFS filesystem using the following layout:
+
+.nf
+server:/export mounted on /mount/point:
+
+ ops/s rpc bklog
+ 0.017 0.000
+
+read: ops/s kB/s kB/op retrans avg RTT (ms) avg exe (ms) avg queue (ms) errors
+ ...
+
+write: ops/s kB/s kB/op retrans avg RTT (ms) avg exe (ms) avg queue (ms) errors
+ ...
+.fi
+
+.SH FIELD DESCRIPTIONS
+.TP
+.B ops/s
+Number of NFS operations performed per second.
+
+.TP
+.B rpc bklog
+Average number of RPC requests waiting to be transmitted.
+
+.TP
+.B kB/s
+Kilobytes transferred per second.
+
+.TP
+.B kB/op
+Average number of kilobytes transferred per operation.
+
+.TP
+.B retrans
+Number of RPC retransmissions and the retransmission percentage.
+
+.TP
+.B avg RTT (ms)
+Average round-trip time in milliseconds for RPC requests.
+
+.TP
+.B avg exe (ms)
+Average execution time in milliseconds spent servicing requests on the server.
+
+.TP
+.B avg queue (ms)
+Average time in milliseconds spent waiting in the RPC transmission queue.
+
+.TP
+.B errors
+Number of failed operations and failure percentage.
+
+.SH OPTIONS
+.TP
+.BR \-a ", " \-\-archive " " I archive
+Fetch NFS I/O statistics from the specified PCP archive.
+
+.TP
+.BR \-s ", " \-\-samples " " I samples
+Number of samples to collect before exiting.
+
+.TP
+.BR \-z ", " \-\-hostzone
+Set the reporting timezone to the local timezone of the metrics source.
+
+.TP
+.BR \-Z ", " \-\-timezone " " I tz
+Set the reporting timezone.
+
+.TP
+.BR \-V ", " \-\-version
+Display version information and exit.
+
+.TP
+.BR \-? ", " \-\-help
+Display usage information and exit.
+
+.SH NOTES
+.B pcp-nfsiostat
+reports client-side NFS statistics collected from the kernel and exposed
+via the PCP NFS PMDA. The output format and metrics are similar to those
+reported by the
+.BR nfsiostat (1)
+tool from the nfs-utils package.
+
+.SH PCP ENVIRONMENT
+Environment variables with the prefix \fBPCP_\fP are used to parameterize
+the file and directory names used by PCP.
+On each installation, the
+file \fI/etc/pcp.conf\fP contains the local values for these variables.
+The \fB$PCP_CONF\fP variable may be used to specify an alternative
+configuration file, as described in
+.BR pcp.conf (5).
+
+For environment variables affecting PCP tools, see
+.BR pmGetOptions (3).
+
+.SH SEE ALSO
+.BR PCPIntro (1),
+.BR pcp (1),
+.BR nfsiostat (1),
+.BR environ (7).
diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.py b/src/pcp/nfsiostat/pcp-nfsiostat.py
new file mode 100644
index 0000000..0b0376d
--- /dev/null
+++ b/src/pcp/nfsiostat/pcp-nfsiostat.py
@@ -0,0 +1,346 @@
+#!/usr/bin/pmpython
+#
+# Copyright (c) 2023 Oracle and/or its affiliates.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# pylint: disable=bad-whitespace,too-many-lines,bad-continuation
+# pylint: disable=too-many-arguments,too-many-positional-arguments
+# pylint: disable=redefined-outer-name,unnecessary-lambda
+#
+
+import signal
+import sys
+import time
+from pcp import pmapi, pmcc
+from cpmapi import PM_CONTEXT_ARCHIVE
+
+SYS_METRICS= ["kernel.uname.sysname","kernel.uname.release",
+ "kernel.uname.nodename","kernel.uname.machine","hinv.ncpu"]
+NFSIOSTAT_METRICS = ["nfsclient.mountpoint","nfsclient.export","nfsclient.age",
+ "nfsclient.xprt.sends","nfsclient.xprt.backlog_u","nfsclient.ops.read.ops",
+ "nfsclient.ops.read.errors","nfsclient.ops.read.execute","nfsclient.ops.read.rtt",
+ "nfsclient.ops.read.queue","nfsclient.ops.read.bytes_recv","nfsclient.ops.read.bytes_sent",
+ "nfsclient.ops.read.ntrans","nfsclient.ops.write.ops","nfsclient.ops.write.errors",
+ "nfsclient.ops.write.execute","nfsclient.ops.write.rtt","nfsclient.ops.write.queue",
+ "nfsclient.ops.write.bytes_recv","nfsclient.ops.write.bytes_sent","nfsclient.ops.write.ntrans"]
+ALL_METRICS = NFSIOSTAT_METRICS + SYS_METRICS
+
+def adjust_length(name):
+ return name.ljust(25)
+class ReportingMetricRepository:
+
+ def __init__(self,group):
+ self.group=group
+ self.current_cached_values = {}
+
+ def __sorted(self,data):
+ return dict(sorted(data.items(), key=lambda item: item[0].lower()))
+
+ def __fetch_current_value(self,metric):
+ val=dict(map(lambda x: (x[1], x[2]), self.group[metric].netValues))
+ val=self.__sorted(val)
+ return dict(val)
+
+ def current_value(self,metric):
+ if not metric in self.group:
+ return None
+ if self.current_cached_values.get(metric) is None:
+ first_value=self.__fetch_current_value(metric)
+ self.current_cached_values[metric]=first_value
+ return self.current_cached_values[metric]
+
+class NfsioStatUtil:
+ def __init__(self,metrics_repository):
+ self.__metric_repository=metrics_repository
+ self.report=ReportingMetricRepository(self.__metric_repository)
+
+ def mount_point(self):
+ return self.report.current_value('nfsclient.mountpoint')
+
+ def mount_share(self):
+ return self.report.current_value('nfsclient.export')
+
+ def mount_share_keys(self):
+ data = self.report.current_value('nfsclient.export')
+ return data.keys()
+
+ def sample_time(self):
+ return self.report.current_value('nfsclient.age')
+
+ def xprt_sends(self):
+ return self.report.current_value('nfsclient.xprt.sends')
+
+ def xprt_backlog(self):
+ return self.report.current_value('nfsclient.xprt.backlog_u')
+
+ def readops(self):
+ return self.report.current_value('nfsclient.ops.read.ops')
+
+ def readerrors(self):
+ return self.report.current_value('nfsclient.ops.read.errors')
+
+ def readexecute(self):
+ return self.report.current_value('nfsclient.ops.read.execute')
+
+ def readrtt(self):
+ return self.report.current_value('nfsclient.ops.read.rtt')
+
+ def readqueue(self):
+ return self.report.current_value('nfsclient.ops.read.queue')
+
+ def readbytesrecv(self):
+ return self.report.current_value('nfsclient.ops.read.bytes_recv')
+
+ def readbytessent(self):
+ return self.report.current_value('nfsclient.ops.read.bytes_sent')
+
+ def readntrans(self):
+ return self.report.current_value('nfsclient.ops.read.ntrans')
+
+ def writeops(self):
+ return self.report.current_value('nfsclient.ops.write.ops')
+
+ def writeerrors(self):
+ return self.report.current_value('nfsclient.ops.write.errors')
+
+ def writeexecute(self):
+ return self.report.current_value('nfsclient.ops.write.execute')
+
+ def writertt(self):
+ return self.report.current_value('nfsclient.ops.write.rtt')
+
+ def writequeue(self):
+ return self.report.current_value('nfsclient.ops.write.queue')
+
+ def writebytesrecv(self):
+ return self.report.current_value('nfsclient.ops.write.bytes_recv')
+
+ def writebytessent(self):
+ return self.report.current_value('nfsclient.ops.write.bytes_sent')
+
+ def writentrans(self):
+ return self.report.current_value('nfsclient.ops.write.ntrans')
+
+class NfsiostatReport(pmcc.MetricGroupPrinter):
+ def __init__(self,opts,group):
+ self.opts = opts
+ self.group = group
+ self.samples = opts.samples
+ self.context = opts.context
+
+ def __get_ncpu(self, group):
+ return group['hinv.ncpu'].netValues[0][2]
+
+ def __print_machine_info(self, context):
+ timestamp = self.group.pmLocaltime(context.timestamp.tv_sec)
+ # Please check strftime(3) for different formatting options.
+ # Also check TZ and LC_TIME environment variables for more
+ # information on how to override the default formatting of
+ # the date display in the header
+ time_string = time.strftime("%m/%d/%Y %H:%M:%S", timestamp.struct_time())
+ header_string = ''
+ header_string += context['kernel.uname.sysname'].netValues[0][2] + ' '
+ header_string += context['kernel.uname.release'].netValues[0][2] + ' '
+ header_string += '(' + context['kernel.uname.nodename'].netValues[0][2] + ') '
+ header_string += time_string + ' '
+ header_string += context['kernel.uname.machine'].netValues[0][2] + ' '
+ print("%s (%s CPU)" % (header_string, self.__get_ncpu(context)))
+
+ def __print_values(self,timestamp, nfsstatus):
+ n_shares = nfsstatus.mount_share_keys()
+ mountshare = nfsstatus.mount_share()
+ mountpoint = nfsstatus.mount_point()
+ sampletime = nfsstatus.sample_time()
+ sends = nfsstatus.xprt_sends()
+ backlog = nfsstatus.xprt_backlog()
+ readops = nfsstatus.readops()
+ readerrors = nfsstatus.readerrors()
+ readexecute = nfsstatus.readexecute()
+ readrtt = nfsstatus.readrtt()
+ readqueue = nfsstatus.readqueue()
+ readbytesrecv = nfsstatus.readbytesrecv()
+ readbytessent = nfsstatus.readbytessent()
+ readntrans = nfsstatus.readntrans()
+ writeops = nfsstatus.writeops()
+ writeerrors = nfsstatus.writeerrors()
+ writeexecute = nfsstatus.writeexecute()
+ writertt = nfsstatus.writertt()
+ writequeue = nfsstatus.writequeue()
+ writebytesrecv = nfsstatus.writebytesrecv()
+ writebytessent = nfsstatus.writebytessent()
+ writentrans = nfsstatus.writentrans()
+
+ print("%-18s:%s"%("Timestamp", timestamp))
+ print()
+
+ for name in n_shares:
+ # read
+ r_kilobytes = (readbytessent[name] + readbytesrecv[name]) / 1024
+ if sampletime[name] > 0:
+ ops_per_sample = sends[name] / sampletime[name]
+ ops_per_sample_read = readops[name] / sampletime[name]
+ r_kilobytes_per_sample = r_kilobytes / sampletime[name]
+ else:
+ ops_per_sample = 0.0
+ ops_per_sample_read = 0.0
+ r_kilobytes_per_sample = 0.0
+
+ r_retrans = readntrans[name] - readops[name]
+ if readops[name] > 0:
+ r_kilobytes_per_op = r_kilobytes / readops[name]
+ r_retrans_percent = (r_retrans * 100) / readops[name]
+ r_rtt_per_op = readrtt[name] / readops[name]
+ r_exe_per_op = readexecute[name] / readops[name]
+ r_queued_for_per_op = readqueue[name] / readops[name]
+ r_errs_percent = (readerrors[name] * 100) / readops[name]
+ else:
+ r_kilobytes_per_op = 0.0
+ r_retrans_percent = 0.0
+ r_rtt_per_op = 0.0
+ r_exe_per_op = 0.0
+ r_queued_for_per_op = 0.0
+ r_errs_percent = 0.0
+
+ # write
+ w_kilobytes = (writebytessent[name] + writebytesrecv[name]) / 1024
+ if sampletime[name] > 0:
+ ops_per_sample_write = writeops[name] / sampletime[name]
+ w_kilobytes_per_sample = w_kilobytes / sampletime[name]
+ else:
+ ops_per_sample_write = 0.0
+ w_kilobytes_per_sample = 0.0
+
+ w_retrans = writentrans[name] - writeops[name]
+ if writeops[name] > 0:
+ w_kilobytes_per_op = w_kilobytes / writeops[name]
+ w_retrans_percent = (w_retrans * 100) / writeops[name]
+ w_rtt_per_op = writertt[name] / writeops[name]
+ w_exe_per_op = writeexecute[name] / writeops[name]
+ w_queued_for_per_op = writequeue[name] / writeops[name]
+ w_errs_percent = (writeerrors[name] * 100) / writeops[name]
+ else:
+ w_kilobytes_per_op = 0.0
+ w_retrans_percent = 0.0
+ w_rtt_per_op = 0.0
+ w_exe_per_op = 0.0
+ w_queued_for_per_op = 0.0
+ w_errs_percent = 0.0
+
+ print(f"{mountshare[name]} mounted on {mountpoint[name]}:")
+
+ print(f"{'':14}ops/s{'':7}rpc bklog")
+ print(f"{ops_per_sample:19.3f}{backlog[name]:16.3f}")
+ print()
+ print(
+ "read: "
+ "ops/s kB/s kB/op retrans "
+ "avg RTT (ms) avg exe (ms) avg queue (ms) errors"
+ )
+ print(
+ f"{'':19}"
+ f"{ops_per_sample_read:5.3f}"
+ f"{r_kilobytes_per_sample:12.3f}"
+ f"{r_kilobytes_per_op:13.3f} "
+ f"{int(r_retrans):2d} ({r_retrans_percent:2.1f}%)"
+ f"{r_rtt_per_op:15.3f}"
+ f"{r_exe_per_op:15.3f}"
+ f"{r_queued_for_per_op:17.3f} "
+ f"{int(readerrors[name]):4d} ({r_errs_percent:2.1f}%)"
+ )
+
+ print(
+ "write: "
+ "ops/s kB/s kB/op retrans "
+ "avg RTT (ms) avg exe (ms) avg queue (ms) errors"
+ )
+
+ print(
+ f"{'':19}"
+ f"{ops_per_sample_write:5.3f}"
+ f"{w_kilobytes_per_sample:12.3f}"
+ f"{w_kilobytes_per_op:13.3f} "
+ f"{int(w_retrans):2d} ({w_retrans_percent:2.1f}%)"
+ f"{w_rtt_per_op:15.3f}"
+ f"{w_exe_per_op:15.3f}"
+ f"{w_queued_for_per_op:17.3f} "
+ f"{int(writeerrors[name]):4d} ({w_errs_percent:2.1f}%)"
+ )
+ print()
+
+ def print_report(self,group,timestamp, manager_nfsiostat):
+ def __print_nfs_status():
+ nfsstatus = NfsioStatUtil(manager_nfsiostat)
+ if nfsstatus.mount_share():
+ try:
+ self.__print_machine_info(group)
+ self.__print_values(timestamp, nfsstatus)
+ except IndexError:
+ print("Incorrect machine info due to some missing metrics")
+ return
+ else:
+ pass
+
+ if self.context != PM_CONTEXT_ARCHIVE and self.samples is None:
+ __print_nfs_status()
+ sys.exit(0)
+ elif self.context == PM_CONTEXT_ARCHIVE and self.samples is None:
+ __print_nfs_status()
+ elif self.samples >=1:
+ __print_nfs_status()
+ self.samples-=1
+ else:
+ pass
+
+ def report(self, manager):
+ group = manager["sysinfo"]
+ self.samples = self.opts.pmGetOptionSamples()
+ t_s = group.contextCache.pmLocaltime(int(group.timestamp))
+ timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time())
+ self.print_report(group,timestamp,manager['nfsiostat'])
+
+class NfsiostatOptions(pmapi.pmOptions):
+ timefmt = "%m/%d/%Y %H:%M:%S"
+ def __init__(self):
+ pmapi.pmOptions.__init__(self, "a:s:Z:zV?")
+ self.pmSetLongOptionHeader("General options")
+ self.pmSetLongOptionHostZone()
+ self.pmSetLongOptionTimeZone()
+ self.pmSetLongOptionHelp()
+ self.pmSetLongOptionSamples()
+ self.pmSetLongOptionVersion()
+ self.samples=None
+ self.context=None
+
+if __name__ == '__main__':
+ try:
+ opts = NfsiostatOptions()
+ mngr = pmcc.MetricGroupManager.builder(opts,sys.argv)
+ opts.context=mngr.type
+ missing = mngr.checkMissingMetrics(ALL_METRICS)
+ if missing is not None:
+ sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing)
+ sys.exit(1)
+ mngr["nfsiostat"] = ALL_METRICS
+ mngr["sysinfo"] = SYS_METRICS
+ mngr.printer = NfsiostatReport(opts,mngr)
+ sts = mngr.run()
+ sys.exit(sts)
+ except pmapi.pmErr as error:
+ sys.stderr.write('%s\n' % (error.message()))
+ except pmapi.pmUsageErr as usage:
+ usage.message()
+ sys.exit(1)
+ except IOError:
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
+ except KeyboardInterrupt:
+ pass
--
2.43.7

View File

@ -0,0 +1,178 @@
From 7bc4a59bfe06f9b988ea21ad6a2decc86c942097 Mon Sep 17 00:00:00 2001
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
Date: Thu, 26 Mar 2026 14:16:48 +0000
Subject: [PATCH OL9 1013/1016] pmlogger_janitor fix not to terminate
unauthorized process
- Fix pmlogger_janitor.sh to verify pid for active pmlogger before kill/removing map files
- Update process type detection to support *BSD ps output syntax
- Refactor pmlogger_check.sh to load janitor env vars more robustly
upstream :- 358b684619d786acc93092824bc24ba69c5d5dbc
f305a22730a614535098c4f85c05de37eadc790e
[Orabug:38598244]
Signed-off-by: sagar sagar <sagar.sagar@oracle.com>
---
src/pmlogger/pmlogger_check.sh | 27 +++++++++++---
src/pmlogger/pmlogger_farm.defaults | 8 ----
src/pmlogger/pmlogger_janitor.sh | 57 ++++++++++++++++++++++++++++-
3 files changed, 76 insertions(+), 16 deletions(-)
diff --git a/src/pmlogger/pmlogger_check.sh b/src/pmlogger/pmlogger_check.sh
index c941df0..27a0a1e 100755
--- a/src/pmlogger/pmlogger_check.sh
+++ b/src/pmlogger/pmlogger_check.sh
@@ -23,9 +23,6 @@
PMLOGGER="$PCP_BINADM_DIR/pmlogger"
PMLOGCONF="$PCP_BINADM_DIR/pmlogconf"
-PMLOGGERENVS="$PCP_SYSCONFIG_DIR/pmlogger"
-PMLOGGERFARMENVS="$PCP_SYSCONFIG_DIR/pmlogger_farm"
-PMLOGGERZEROCONFENVS="$PCP_SHARE_DIR/zeroconf/pmlogger"
# error messages should go to stderr, not the GUI notifiers
#
@@ -703,7 +700,7 @@ _callback_log_control()
if [ "X$primary" = Xy ]
then
# User configuration takes precedence over pcp-zeroconf
- envs=`grep -h ^PMLOGGER "$PMLOGGERZEROCONFENVS" "$PMLOGGERENVS" 2>/dev/null`
+ envs=`grep -h ^PMLOGGER "$PCP_SHARE_DIR/zeroconf/pmlogger" "$PCP_SYSCONFIG_DIR/pmlogger" 2>/dev/null`
args="-P $args"
iam=" primary"
# clean up port-map, just in case
@@ -721,7 +718,7 @@ _callback_log_control()
return
fi
else
- envs=`grep -h ^PMLOGGER "$PMLOGGERFARMENVS" 2>/dev/null`
+ envs=`grep -h ^PMLOGGER "$PCP_SYSCONFIG_DIR/pmlogger_farm" 2>/dev/null`
args="-h $host $args"
iam=""
fi
@@ -896,10 +893,28 @@ fi
# because the legitimate pmloggers, like the primary pmlogger, may
# not be included in the "test" control file(s).
#
+if [ -z "${PMLOGGER_CHECK_SKIP_JANITOR+is_set}" ]
+then
+ check=`grep '^PMLOGGER_CHECK_SKIP_JANITOR=' $PCP_SYSCONFIG_DIR/pmlogger`
+ if [ -n "$check" ]
+ then
+ eval $check
+ fi
+fi
+if [ -z "${PMLOGGER_JANITOR_ARGS+is_set}" ]
+then
+ check=`grep '^PMLOGGER_JANITOR_ARGS=' $PCP_SYSCONFIG_DIR/pmlogger`
+ if [ -n "$check" ]
+ then
+ eval $check
+ fi
+fi
+
if [ "$CONTROL" = "$PCP_PMLOGGERCONTROL_PATH" -a "$PMLOGGER_CHECK_SKIP_JANITOR" != "yes" ]
then
$VERY_VERBOSE && echo "Running: pmlogger_janitor $daily_args"
- $PCP_BINADM_DIR/pmlogger_janitor $daily_args
+ args="$daily_args"
+ $PCP_BINADM_DIR/pmlogger_janitor $args
fi
if [ -f $tmp/err ]
diff --git a/src/pmlogger/pmlogger_farm.defaults b/src/pmlogger/pmlogger_farm.defaults
index 10a0132..af06446 100644
--- a/src/pmlogger/pmlogger_farm.defaults
+++ b/src/pmlogger/pmlogger_farm.defaults
@@ -25,11 +25,3 @@
# setting PMLOGGER_CHECK_SKIP_LOGCONF to yes disables the regeneration
# and checking.
# PMLOGGER_CHECK_SKIP_LOGCONF=yes
-
-# By default pmlogger_check(1) will run pmlogger_janitor to check for
-# pmlogger(1) badness caused by processes and/or files that were once
-# managed from the control files but have become detached from those
-# control files.
-# Setting PMLOGGER_CHECK_SKIP_JANITOR to yes disables pmlogger_janitor
-# and maybe useful for QA or special testing
-# PMLOGGER_CHECK_SKIP_JANITOR=yes
diff --git a/src/pmlogger/pmlogger_janitor.sh b/src/pmlogger/pmlogger_janitor.sh
index bc0360a..7c8ec0c 100755
--- a/src/pmlogger/pmlogger_janitor.sh
+++ b/src/pmlogger/pmlogger_janitor.sh
@@ -348,8 +348,15 @@ _callback_log_control()
if [ -n "$pid" ]
then
- # found matching pmlogger ... cull this one from
- $VERY_VERBOSE && echo "[$filename:$line] match PID $pid, nothing to be done"
+ # found matching pmlogger ... cull this one from $tmp/loggers
+ #
+ if $VERY_VERBOSE
+ then
+ echo "[$filename:$line] match PID $pid, nothing to be done"
+ elif $VERBOSE
+ then
+ echo "Pass 3: PID $pid matches control [$filename:$line], nothing to be done"
+ fi
sed <$tmp/loggers >$tmp/tmp -e "/^$pid /d"
mv $tmp/tmp $tmp/loggers
fi
@@ -365,6 +372,52 @@ then
| while read file
do
pid=`echo "$file" | sed -e "s@$PCP_TMP_DIR/pmlogger/@@"`
+ # sanity checks
+ # 1. does this process exist?
+ # 2. is it really pmlogger?
+ # if "no" to either case, remove this (stale) mapfile
+ # and move on ...
+ #
+ if $PCP_PS_PROG -p "$pid" >$tmp/tmp 2>&1
+ then
+ # ps(1) -p output should be something like this ...
+ # PID TTY TIME CMD
+ # 14298 ? 00:00:00 pmlogger
+ # or this (for *BSD)
+ # PID TT STAT TIME COMMAND
+ # 22839 1 S 0:00.04 /usr/libexec/pcp/bin/pmlogger -N -P ...
+ #
+ if sed -n -e 2p <$tmp/tmp | grep -E -q '( pmlogger$)|(/bin/pmlogger )'
+ then
+ : OK
+ else
+ if $VERBOSE
+ then
+ cat $tmp/tmp
+ echo "Warning: PID $pid is not a pmlogger process, removing $file"
+ fi
+ if $SHOWME
+ then
+ echo "+ rm $file"
+ else
+ rm -f "$file"
+ fi
+ continue
+ fi
+ else
+ if $VERBOSE
+ then
+ echo "Warning: PID $pid has vanished, removing $file"
+ fi
+ if $SHOWME
+ then
+ echo "+ rm $file"
+ else
+ rm -f "$file"
+ fi
+ continue
+ fi
+
# timing window here, file may have gone away between
# find(1) and awk(1), so just ignore any errors ...
#
--
2.43.7

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,604 @@
From 09f39fb28a3171300afa5ed15e7f75ac0fbfdabc Mon Sep 17 00:00:00 2001
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
Date: Thu, 26 Mar 2026 16:10:37 +0000
Subject: [PATCH OL9 1015/1016] pmlogger_daily -d/--disk option for archive
space limit
Added -d/--disk option in pmlogger_daily for archive size limit
Doc: Explain $PCP_SPACELIMIT env var for disk usage in PCP archives
Clarify archive retention/purging with space limits and env/CLI opts
utilproc.sh: Refactor _convert_to_kb(), unit checks, env
upstream ref:- 7b7c38c16c6904960ede400efa5225732935b830
[orabug:38757778]
Signed-off-by: sagar sagar <sagar.sagar@oracle.com>
---
man/man1/pmlogger_daily.1 | 85 +++++++++++++++-
src/pmlogger/pmlogger_daily.sh | 176 +++++++++++++++++++++++++++++++--
src/pmlogger/utilproc.sh | 142 +++++++++++++++++++++++++-
3 files changed, 391 insertions(+), 12 deletions(-)
diff --git a/man/man1/pmlogger_daily.1 b/man/man1/pmlogger_daily.1
index c7b30ae..0371f63 100644
--- a/man/man1/pmlogger_daily.1
+++ b/man/man1/pmlogger_daily.1
@@ -20,6 +20,7 @@
.B $PCP_BINADM_DIR/pmlogger_daily
[\f3\-DEfKMNnoPpQrRVzZ?\f1]
[\f3\-c\f1 \f2control\f1]
+[\f3\-d\f1 \f2fssize\f1]
[\f3\-k\f1 \f2time\f1]
[\f3\-l\f1 \f2logfile\f1]
[\f3\-m\f1 \f2addresses\f1]
@@ -120,6 +121,72 @@ Do not perform the conditional
.BR pmlogger_daily_report (1)
processing as described below.
.TP 5
+\fB\-d\fR \fIfssize\fR, \fB\-\-disk\fR=\fIfssize\fR
+This option enforces a maximum total file system space usage per host directory for PCP archive files stored under each
+.IR $PCP_ARCHIVE_DIR/<hostname>
+and
+.IR $PCP_REMOTE_ARCHIVE_DIR/<hostname>
+location. The
+.I fssize
+must be specified as an integer with a suffix
+.B K
+or
+.B k
+for kilobytes,
+.B M
+or
+.B m
+for megabytes,
+.B G
+or
+.B g
+for gigabytes (e.g. 500m or 10G).
+As a special case,
+.I fssize
+may be the keyword
+.B unlimited
+to prevent any file system space limit enforcement (this is the default
+behaviour).
+.RS
+.PP
+Alternatively, the file system space limit may be set by exporting the
+.B $PCP_SPACELIMIT
+environment variable, which is handled analogously to
+.BR $PCP_CULLAFTER .
+If both
+.B $PCP_SPACELIMIT
+and
+.B \-d
+are provided and specify different values then the value from
+the environment variable is used and a warning is issued.
+.PP
+After normal daily log processing (compression, merging, culling by age, etc.)
+completes,
+if the total file system space consumed by any
+.BR pmlogger (1)
+instance exceeds
+.IR fssize ,
+then additional archive files will be purged (i.e. removed),
+oldest first, until
+the file system usage is reduced to be not more than
+.IR fssize .
+This purging operation never deletes archive files with today's date,
+and so always preserves the most recent archives required by active
+.BR pmlogger (1)
+processes.
+.PP
+Enforcement is only performed when
+.B pmlogger_daily
+runs (typically once a day), not continuously.
+As a result, the total file system usage for any
+.BR pmlogger (1)
+instance may exceed a specified maximum between
+.B pmlogger_daily
+executions (for example, as new archives are created
+or when archives are uncompressed for merging or
+prior to asynchronous archive compression).
+.RE
+.TP 5
\fB\-E\fR, \fB\-\-expunge\fR
This option causes
.B pmlogger_daily
@@ -524,7 +591,7 @@ maximizes the diagnostic capabilities for debugging.
.TP 5
\fB\-x\fR \fItime\fR, \fB\-\-compress\-after\fR=\fItime\fR
Archive data files can optionally be compressed after some period
-to conserve disk space.
+to conserve file system space.
This is particularly useful for large numbers of
.BR pmlogger (1)
processes under the control of
@@ -796,7 +863,7 @@ may appear literally in
and will be substituted at execution time to generate the destination
directory name. For example:
.ft CR
-.in +6n
+.in +2n
$PCP_AUTOSAVE_DIR=/gpfs/LOCALHOSTNAME/DATEYYYY/DATEMM-DATEDD
.br
.PP
@@ -957,6 +1024,20 @@ if this file exists, then this is treated as equivalent to using
on the command line and the file will be removed once all rewriting
has been done.
.SH PCP ENVIRONMENT
+.TP 5
+.B $PCP_SPACELIMIT
+If set, specifies a maximum allowed total file system space (in kilobytes, or with optional K/M/G suffix as for the \-d option) for each per-host archive directory under
+.I $PCP_ARCHIVE_DIR
+or
+.I $PCP_REMOTE_ARCHIVE_DIR .
+This value is used by
+.B pmlogger_daily
+when enforcing archive retention limits for file system space, unless the
+.B \-d
+option is provided in which case the command line flag overrides the environment variable and a warning is issued. Enforcement occurs after each run of
+.B pmlogger_daily
+and only applies at that time—not continuously.
+.PP
Environment variables with the prefix \fBPCP_\fP are used to parameterize
the file and directory names used by PCP.
On each installation, the
diff --git a/src/pmlogger/pmlogger_daily.sh b/src/pmlogger/pmlogger_daily.sh
index 3ebd17c..be915d6 100755
--- a/src/pmlogger/pmlogger_daily.sh
+++ b/src/pmlogger/pmlogger_daily.sh
@@ -37,6 +37,7 @@ prog=`basename $0`
PROGLOG=$PCP_LOG_DIR/pmlogger/$prog.log
MYPROGLOG=$PROGLOG.$$
USE_SYSLOG=true
+localhost=`hostname || echo localhost`
# optional begin logging to $PCP_LOG_DIR/NOTICES
#
@@ -353,6 +354,7 @@ Options:
-E,--expunge expunge metrics with metadata inconsistencies when merging archives
-f,--force force actions (intended for QA, not production)
-k=TIME,--discard=TIME remove archives after TIME (format DD[:HH[:MM]])
+ -d=fssize,--disk=fssize set maximum disk usage for archives for each pmlogger instance
-K compress, but no other changes
-l=FILE,--logfile=FILE send important diagnostic messages to FILE
-m=ADDRs,--mail=ADDRs send daily NOTICES entries to email addresses
@@ -404,6 +406,8 @@ DO_DAILY_REPORT=true
NOPROXY=false
PROXYONLY=false
NOERROR=false
+SPACELIMIT_CMDLINE=""
+SPACELIMIT_DEFAULT="unlimited"
ARGS=`pmgetopt --progname=$prog --config=$tmp/usage -- "$@"`
[ $? != 0 ] && exit 1
@@ -419,6 +423,24 @@ do
;;
-D) DO_DAILY_REPORT=false
;;
+ -d) SPACELIMIT_CMDLINE="$2"
+ shift
+ if [ -n "$PCP_SPACELIMIT" -a "$PCP_SPACELIMIT" != "$SPACELIMIT_CMDLINE" ]
+ then
+ echo "Warning: -d value ($SPACELIMIT_CMDLINE) ignored because \$PCP_SPACELIMIT ($PCP_SPACELIMIT) set in environment"
+ SPACELIMIT_CMDLINE=""
+ continue
+ fi
+ if [ "$SPACELIMIT_CMDLINE" != unlimited ]
+ then
+ if ! _convert_to_kb "$SPACELIMIT_CMDLINE" >/dev/null
+ then
+ echo "Error: -d value ($SPACELIMIT_CMDLINE) not valid"
+ $NOERROR || status=1
+ exit
+ fi
+ fi
+ ;;
-E) EXPUNGE="-E"
;;
-f) FORCE=true
@@ -781,10 +803,49 @@ fi
if [ ! -f "$CONTROL" ]
then
echo "$prog: Error: cannot find control file ($CONTROL)"
+ echo "... I am here ... `pwd` ... and these files are here ..."
+ ls -l
$NOERROR || status=1
exit
fi
+# Given a list of candidate directories, calculate the total
+# size below them in Kbytes
+#
+_calculate_total_size()
+{
+ du -sk "$@" 2>$tmp/cts_err \
+ | awk >$tmp/cts_out '
+BEGIN { kb = 0 }
+ { kb += $1 }
+END { print kb }' >$tmp/cts_out
+ if [ -s $tmp/cts_err ] && $VERY_VERBOSE
+ then
+ echo >&2 "Warning: _calculate_total_size: du -sk $@ produced errors ..."
+ cat >&2 $tmp/cts_err
+ fi
+ $VERY_VERBOSE && echo >&2 "Info: _calculate_total_size -> `cat $tmp/cts_out` Kbytes"
+ cat $tmp/cts_out
+}
+
+# Calculate the total size of the archive with basename $1
+#
+_calculate_archive_size()
+{
+ du -sk "$1".* 2>$tmp/cts_err \
+ | awk >$tmp/cts_out '
+BEGIN { kb = 0 }
+ { kb += $1 }
+END { print kb }' >$tmp/cts_out
+ if [ -s $tmp/cts_err ] && $VERY_VERBOSE
+ then
+ echo >&2 "Warning: _calculate_archive_size: du -sk $1.* produced errors ..."
+ cat >&2 $tmp/cts_err
+ fi
+ $VERY_VERBOSE && echo >&2 "Info: _calculate_archive_size $1 -> `cat $tmp/cts_out` Kbytes"
+ cat $tmp/cts_out
+}
+
_skipping()
{
echo "$prog: Warning: $@"
@@ -1062,15 +1123,93 @@ BEGIN { seenslash = 0; inshell = 0; out = "" }
END { print out }'
}
+
+
+# Check disk space allocation for one pmlogger instance, and if more
+# than $SPACELIMIT Kbytes, purge files to try and get the space
+# allocated to be not more than $SPACELIMIT Kbytes.
+#
+# Oldest files are purged first and the most recent archives for today
+# (including those required by active pmlogger (1) processes are
+# never deleted.
+#
+# On entry, $find_dirs is a list of one or more directories holding
+# archives for a pmlogger instance, set in _callback_log_control(),
+# and $host is the host name field from the control line, set in
+# _parse_log_control().
+#
+_do_purge()
+{
+ SPACELIMIT="$PCP_SPACELIMIT"
+ [ -z "$SPACELIMIT" ] && SPACELIMIT="$SPACELIMIT_CMDLINE"
+ [ -z "$SPACELIMIT" ] && SPACELIMIT="$SPACELIMIT_DEFAULT"
+ [ "$SPACELIMIT" = unlimited ] && return
+ if ! _convert_to_kb "$SPACELIMIT" >$tmp/tmp
+ then
+ _warning "skipping purging because of invalid space limit"
+ return
+ fi
+ SPACELIMIT=`cat $tmp/tmp`
+ $VERY_VERBOSE && echo >&2 "SPACELIMIT=$SPACELIMIT"
+ __total_size=`_calculate_total_size $find_dirs`
+ if [ "$__total_size" -le "$SPACELIMIT" ]
+ then
+ $VERBOSE && echo "Info: No purging required for $host, archives ($__total_size Kbytes) <= size limit ($SPACELIMIT Kbytes)"
+ return
+ fi
+ $VERBOSE && echo "Info: archives for $host ($__total_size Kbytes) >= size limit ($SPACELIMIT Kbytes)"
+
+ # algorithm to find archive basenames borrowed from _do_merge()
+ # output is in this format ...
+ # <directory>|<archive>
+ #
+ TODAY=`date +%Y%m%d`
+ find $find_dirs -maxdepth 1 -type f \
+ | sed -n \
+ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.meta.*\)/s//\1|\2/p' \
+ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
+ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]-[0-9][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
+ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.meta.*\)/s//\1|\2/p' \
+ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
+ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]-[0-9][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
+ | sort -t'|' -n -k2,2 \
+ | $PCP_AWK_PROG -F'|' '
+$2 == "'$TODAY'" { next }
+$2 ~ /^'$TODAY'/ { next }
+ { print }' >$tmp/purge_candidates
+ if [ ! -s $tmp/purge_candidates ]
+ then
+ $VERBOSE && echo "Info: No candidates to purge."
+ return
+ fi
+
+ sed -e 's/|/ /' <$tmp/purge_candidates \
+ | while read __dir __file
+ do
+ [ "$__dir" != "." ] && __file="$__dir/$__file"
+ __arch_size=`_calculate_archive_size "$__file"`
+ if $SHOWME
+ then
+ echo "+ rm `echo $__file.**`"
+ __total_size=`expr $__total_size - $__arch_size`
+ else
+ if rm "$__file".*
+ then
+ $VERBOSE && echo "Info: purge $__file, reclaims $__arch_size Kbytes"
+ __total_size=`expr $__total_size - $__arch_size`
+ else
+ $VERBOSE && echo "Warning: rm $__file.* failed"
+ fi
+ fi
+ [ "$__total_size" -le "$SPACELIMIT" ] && break
+ done
+}
+
# come here from _parse_log_control() once per valid line in a control
# file ... see utilproc.sh for interface definitions
#
_callback_log_control()
{
- # nothing to do for pmlogger pushing to a remote pmproxy
- #
- $logpush && return
-
if $VERBOSE
then
echo
@@ -1130,6 +1269,10 @@ _callback_log_control()
find_dirs=`echo $orig_dir | _unbackquote`
$VERBOSE && echo "Embedded \`...\`: find_dirs=$find_dirs"
fi
+ # and rewrite LOCALHOSTNAME if it is included
+ #
+ find_dirs=`echo "$find_dirs" | sed -e "s;LOCALHOSTNAME;$localhost;g"`
+ $VERY_VERBOSE && echo "Info: find_dirs=$find_dirs"
# For archive rewriting (to make metadata consistent across
# archives) find the rules as follows:
@@ -1424,6 +1567,14 @@ _callback_log_control()
$VERY_VERBOSE && echo >&2 "Warning: no trace files found to cull"
fi
fi
+
+ # if space limit specified, potentially purge old archives if
+ # space limit exceeded
+ #
+ if [ -n "$PCP_SPACELIMIT" -o -n "$SPACELIMIT_CMDLINE" ]
+ then
+ _do_purge
+ fi
}
# Paranoid archive saving
@@ -1779,7 +1930,10 @@ _do_compress()
do
# pmlc may race with pmlogger starting up here - a timeout is required
# to avoid pmlc blocking forever and hanging pmlogger_daily. RHBZ#1892326
- [ -z "$PMLOGGER_REQUEST_TIMEOUT" ] && export PMLOGGER_REQUEST_TIMEOUT=2
+ if [ -z "$PMLOGGER_REQUEST_TIMEOUT" ]
+ then
+ PMLOGGER_REQUEST_TIMEOUT=2; export PMLOGGER_REQUEST_TIMEOUT
+ fi
if pmlc "$pid" </dev/null 2>&1 | tee $tmp/out \
| grep "^Connected to .*pmlogger" >/dev/null
then
@@ -2017,6 +2171,7 @@ else
#
if cd "$PCP_REMOTE_ARCHIVE_DIR"
then
+ $VERY_VERBOSE && echo "Info: do_compress: cd to $PCP_REMOTE_ARCHIVE_DIR OK"
# one-trip guard if there is something to be done
#
rm -f $tmp/proxy_sighup
@@ -2077,7 +2232,16 @@ else
_parse_log_control $tmp/control
fi
done
- cd $here
+ if cd $here
+ then
+ $VERY_VERBOSE && echo "Info: do_compress: cd back to $here OK"
+ else
+ echo >&2 "Error: do_compress: failed to cd back to $here"
+ $NOERROR || status=1
+ fi
+ else
+ echo >&2 "Error: do_compress: faild to cd to $PCP_REMOTE_ARCHIVE_DIR"
+ $NOERROR || status=1
fi
fi
diff --git a/src/pmlogger/utilproc.sh b/src/pmlogger/utilproc.sh
index ae9c72f..9e5582c 100644
--- a/src/pmlogger/utilproc.sh
+++ b/src/pmlogger/utilproc.sh
@@ -256,6 +256,7 @@ BEGIN { i = 0 }
[ $? -eq 0 ] && _PWDCMD="$_PWDCMD -P"
fi
_here=`$_PWDCMD`
+ $VERY_VERBOSE && echo >&2 "_parse_log_control: initial pwd=$_here"
if echo "$1" | grep -q -e '\.rpmsave$' -e '\.rpmnew$' -e '\.rpmorig$' -e '\.dpkg-dist$' -e '\.dpkg-old$' -e '\.dpkg-new$'
then
@@ -270,7 +271,10 @@ BEGIN { i = 0 }
| while read host primary socks dir args
do
# start in one place for each iteration (beware of relative paths)
- cd "$_here"
+ if ! cd "$_here"
+ then
+ $VERY_VERBOSE && echo >&2 "_parse_log_control: failed to cd back to $_here"
+ fi
line=`expr $line + 1`
if $VERY_VERBOSE
@@ -350,6 +354,51 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p
fi
;;
+ 'export PCP_SPACELIMIT;'*)
+ _old_value="$PCP_SPACELIMIT"
+ _check=`echo "$_cmd" | sed -e 's/.*=//' -e 's/ *$//'`
+ if [ -n "$_check" ]
+ then
+ if [ "$_check" = unlimited ]
+ then
+ # no conversion
+ :
+ else
+ # check syntax & convert to canonical Kbytes
+ #
+ _kb=`_convert_to_kb "$_check"`
+ if [ $? != 0 ]
+ then
+ _warning "\$PCP_SPACELIMIT value ($_check) is invalid. Must be a positive integer and a unit (e.g. 100M)"
+ _cmd=''
+ else
+ $SHOWME && echo "+ $_cmd (normalized to $_kb Kbytes)"
+ # need to put back the "K" units here
+ # # because it will get re-processed by
+ # _convert_to_kb() later
+ #
+ _cmd=`echo "$_cmd" | sed -e "s/=.*/=${_kb}K/"`
+ fi
+ fi
+ if [ -n "$_cmd" ]
+ then
+ echo eval $_cmd >>$tmp/_cmd
+ eval $_cmd
+ if [ -n "$_old_value" -a "$_old_value" != "$PCP_SPACELIMIT" ]
+ then
+ _warning "\$PCP_SPACELIMIT ($PCP_SPACELIMIT) reset from control file, previous value ($_old_value) ignored"
+ fi
+ if [ -n "$PCP_SPACELIMIT" -a -n "$SPACELIMIT_CMDLINE" -a "$PCP_SPACELIMIT" != "$SPACELIMIT_CMDLINE" ]
+ then
+ _warning "\$PCP_SPACELIMIT ($PCP_SPACELIMIT) reset from control file, -d value ($SPACELIMIT_CMDLINE) ignored"
+ SPACELIMIT_CMDLINE=""
+ fi
+ fi
+ else
+ _warning "\$PCP_SPACELIMIT from control file missing a value, will be ignored"
+ fi
+ ;;
+
'export PCP_COMPRESS;'*)
_old_value="$PCP_COMPRESS"
$SHOWME && echo "+ $_cmd"
@@ -537,10 +586,8 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p
# check $dir, cd there, acquire lock
#
$SHOWME && echo "+ cd $dir"
- if cd "$dir"
+ if ! cd "$dir"
then
- :
- else
if $SHOWME
then
echo "+ ... cannot show any more for this control line"
@@ -659,6 +706,10 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p
fi
done
+ if ! cd "$_here"
+ then
+ $VERY_VERBOSE && echo >&2 "_parse_log_control: failed to cd back to $_here at return"
+ fi
}
# Called from _callback_log_control() [in pmlogger_check and pmlogger_janitor]
@@ -859,6 +910,89 @@ END { exit sts }'
fi
}
+# Converts a size string like 10G, 100M, 100K to integer KBytes for limit
+# checks.
+# Usage: _convert_to_kb size_string
+# Outputs: integer KBytes on stdout, returns 0 if OK, 1 if error
+_convert_to_kb()
+{
+ __input="$1"
+ __num=
+ __unit=
+ __kb=
+ if [ -z "$__input" ]
+ then
+ echo "Error: _convert_to_kb(): missing argument" >&2
+ return 1
+ fi
+ __num=`echo "$__input" | sed -E 's/^([0-9]+)\s*([a-zA-Z]*)$/\1/'`
+ case "$__num"
+ in
+ "" )
+ echo "Error: _convert_to_kb(): argument '$__input' does not start with a number" >&2
+ return 1
+ ;;
+ *[!0-9]* )
+ echo "Error: _convert_to_kb(): argument '$__input' has a non-numeric value" >&2
+ return 1
+ ;;
+ 0 )
+ echo "Error: _convert_to_kb(): argument '$__input' resolves to zero (not allowed)" >&2
+ return 1
+ ;;
+ esac
+ __unit=`echo "$__input" | sed -E 's/^([0-9]+)\s*([a-zA-Z]*)$/\2/'`
+ case "$__unit"
+ in
+ G|g|M|m|K|k)
+ ;;
+ '')
+ echo "Error: _convert_to_kb(): missing unit after '$__num'" >&2
+ return 1
+ ;;
+ *)
+ echo "Error: _convert_to_kb(): invalid unit '$__unit'" >&2
+ return 1
+ ;;
+ esac
+
+ MAX_INT32=2147483647
+
+ case "$__unit"
+ in
+ G|g)
+ # Check before multiplying
+ __max=`expr $MAX_INT32 / \( 1024 \* 1024 \)`
+ if [ $__num -gt $__max ]
+ then
+ echo "Error: overflow, $__num Gbytes too large for Kbytes in a 32-bit signed int" >&2
+ return 1
+ fi
+ __kb=`expr $__num \* 1024 \* 1024`
+ ;;
+ M|m)
+ # Check before multiplying
+ __max=`expr $MAX_INT32 / 1024`
+ if [ $__num -gt $__max ]
+ then
+ echo "Error: overflow, $__num Mbytes too large for Kbytes in a 32-bit signed int" >&2
+ return 1
+ fi
+ __kb=`expr $__num \* 1024`
+ ;;
+ K|k)
+ if [ $__num -gt $MAX_INT32 ]
+ then
+ echo "Error: overflow, $__num Kbytes too large for 32-bit signed int" >&2
+ return 1
+ fi
+ __kb=$__num
+ ;;
+ esac
+ echo "$__kb"
+ return 0
+}
+
# current time to the highest precision available from date(1) and
# strftime(3)
#
--
2.43.7

View File

@ -0,0 +1,332 @@
From a6bb22bf407522ef95557de0cebde717de347bde Mon Sep 17 00:00:00 2001
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
Date: Thu, 26 Mar 2026 16:31:22 +0000
Subject: [PATCH OL9 1016/1016] pcp-system-tools: restore backward
compatibility with older pmapi versions
Commit 0a37ed0 introduced support for pmapi version 4, but upgrading to the latest pmapi is not currently desired.
This change updates pcp-iostat, mpstat, pidstat, and ps to fall back to tv_usec when tv_nsec is unavailable
during timestamp delta calculation, ensuring compatibility with older pmapi versions and collectors that only expose tv_usec
- optimized code in pcp-ps for dynamic sorting
- fixed sorting colum indexes in -u case
[Orabug: 38719615]
Signed-off-by: Sagar Sagar <sagar.sagar@oracle.com>
---
src/pcp/iostat/pcp-iostat.py | 15 +++-
src/pcp/mpstat/pcp-mpstat.py | 13 +++-
src/pcp/pidstat/pcp-pidstat.py | 13 +++-
src/pcp/ps/pcp-ps.py | 125 +++++++++++++------------------
src/pcp/tapestat/pcp-tapestat.py | 16 +++-
5 files changed, 97 insertions(+), 85 deletions(-)
diff --git a/src/pcp/iostat/pcp-iostat.py b/src/pcp/iostat/pcp-iostat.py
index fd049a6..79d8c9b 100755
--- a/src/pcp/iostat/pcp-iostat.py
+++ b/src/pcp/iostat/pcp-iostat.py
@@ -56,9 +56,18 @@ class IostatReport(pmcc.MetricGroupPrinter):
Hcount = 0
def timeStampDelta(self, group):
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
- # n may be negative here, calculation is still correct.
- return s + n / 1000000000.0
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
+ # when available, but gracefully fall back to microseconds to avoid
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
+ # n may be negative here, calculation is still correct.
+ return s + n / 1000000000.0
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
+ return s + u / 1000000.0
+ # it should not reach here
+ return s
def instlist(self, group, name):
return dict(map(lambda x: (x[1], x[2]), group[name].netValues)).keys()
diff --git a/src/pcp/mpstat/pcp-mpstat.py b/src/pcp/mpstat/pcp-mpstat.py
index 2b0bd59..4e0cdf6 100755
--- a/src/pcp/mpstat/pcp-mpstat.py
+++ b/src/pcp/mpstat/pcp-mpstat.py
@@ -499,8 +499,17 @@ class MpstatReport(pmcc.MetricGroupPrinter):
def timeStampDelta(self, group):
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
- return s + n / 1000000000.0
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
+ # when available, but gracefully fall back to microseconds to avoid
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
+ return s + n / 1000000000.0
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
+ return s + u / 1000000.0
+ # it should not reach here
+ return s
def print_machine_info(self,group, context):
self.get_summary_metrics(group)
diff --git a/src/pcp/pidstat/pcp-pidstat.py b/src/pcp/pidstat/pcp-pidstat.py
index 32bf925..b7613e1 100755
--- a/src/pcp/pidstat/pcp-pidstat.py
+++ b/src/pcp/pidstat/pcp-pidstat.py
@@ -925,8 +925,17 @@ class PidstatReport(pmcc.MetricGroupPrinter):
def timeStampDelta(self, group):
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
- return s + n / 1000000000.0
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
+ # when available, but gracefully fall back to microseconds to avoid
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
+ return s + n / 1000000000.0
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
+ return s + u / 1000000.0
+ # it should not reach here
+ return s
def print_machine_info(self,group, context):
timestamp = context.pmLocaltime(group.timestamp.tv_sec)
diff --git a/src/pcp/ps/pcp-ps.py b/src/pcp/ps/pcp-ps.py
index e2992ea..e5abf34 100755
--- a/src/pcp/ps/pcp-ps.py
+++ b/src/pcp/ps/pcp-ps.py
@@ -252,7 +252,7 @@ class ProcessStatusUtil:
if self.user_percent() is not None and self.guest_percent() is not None and self.system_percent() is not None:
return float("%.2f" % (self.user_percent() + self.guest_percent() + self.system_percent()))
else:
- return None
+ return 0.0
def stime(self):
c_systime = self.__get_value('proc.psinfo.stime', self.instance)
@@ -366,6 +366,20 @@ class DynamicProcessReporter:
self.printer = printer
self.processStatOptions = processStatOptions
+ def __sort_by_idx(self, output_list, sorting_idx, reverse=True):
+ # Rows are tab-delimited; splitting on whitespace breaks when
+ # command/args contain spaces and shifts sortable column indexes.
+ return sorted(
+ output_list,
+ key=lambda row: (
+ float(row.split('\t')[sorting_idx].strip())
+ if sorting_idx < len(row.split('\t'))
+ and row.split('\t')[sorting_idx].strip().replace('.', '', 1).replace('-', '', 1).isdigit()
+ else float('-inf')
+ ),
+ reverse=reverse
+
+
def _is_last_and_args(self, key):
return (key == "args") and \
self.processStatOptions.colum_list.index(key) == len(self.processStatOptions.colum_list) - 1
@@ -378,19 +392,15 @@ class DynamicProcessReporter:
# Sorting validations
sorting_idx = None
if self.processStatOptions.sorting_flag:
- if self.processStatOptions.filterstate == "ALL":
- if self.processStatOptions.sorting_order == '%mem':
- sorting_idx = 7
- elif self.processStatOptions.sorting_order == '%cpu':
- sorting_idx = 8
- else:
- sorting_idx = next((idx for idx, key in enumerate(self.processStatOptions.colum_list)
- if key == self.processStatOptions.sorting_order),
- None)
- if sorting_idx is None:
- raise ValueError("Sorting order not found in output columns")
- # Adjust for timestamp column
- sorting_idx += 1
+ # For dynamic output, sorting key must be present in selected columns.
+ if self.processStatOptions.sorting_order not in self.processStatOptions.colum_list:
+ raise ValueError("Sorting order not found in output columns")
+
+ # Find sorting column index and adjust for Timestamp at position 0.
+ sorting_idx = next((idx for idx, key in enumerate(self.processStatOptions.colum_list)
+ if key == self.processStatOptions.sorting_order), None)
+ # to account for Timestamp colum
+ sorting_idx += 1
# Always compute process list ONCE
processes = self.process_filter.filter_processes(
@@ -400,48 +410,8 @@ class DynamicProcessReporter:
output_list = []
header = None
- # -------- PATH 1: With filterstate -------- #
- if self.processStatOptions.filterstate == "ALL":
- header = (
- "Timestamp\tUSER\t\tPID\t\tPPID\t\tPRI\t%CPU\t%MEM\tVSZ"
- "\tRSS\tS\tSTARTED\t\tTIME\t\t"
- "WCHAN\t\t\t\tCommand"
- )
-
- # Precompute format string
- # fmt = (
- # "{ts}{indent}{user}\t{pid}\t{ppid}\t{pri}\t{cpu}\t{mem}\t"
- # "{vsz}\t{rss}\t{s}\t{started}\t{time}\t{wchan}\t{cmd}"
- # )
-
- for process in processes:
- # Maintain state info
- key = (process.s_name(), process.pid())
- process_state_info[key] = process_state_info.get(key, 0) + self.delta_time
- row = [timestamp]
- row.extend([
- process.user_name(),
- process.pid(),
- process.ppid(),
- process.priority(),
- process.total_percent(),
- process.system_percent(),
- process.vsize(),
- process.rss(),
- process.s_name(),
- process.start(),
- process.total_time(),
- process.wchan_s(),
- process.process_name_with_args_last()[:45]
- ])
- output_list.append(
- "\t".join(
- str(x) if x is not None else '' for x in row
- )
- )
-
- # -------- PATH 2: Customized column list -------- #
- elif self.processStatOptions.colum_list is not None:
+ # -------- Dynamic column list path -------- #
+ if self.processStatOptions.colum_list is not None:
header = "Timestamp\t"
for key in self.processStatOptions.colum_list:
@@ -457,23 +427,14 @@ class DynamicProcessReporter:
# print(row)
output_list.append("\t".join(str(x) if x is not None else '' for x in row))
- # -------- PATH 3: Invalid filterstate or column list -------- #
+ # -------- Invalid column list -------- #
# This should never happen, but just in case
else:
raise ValueError("No valid filterstate or column list provided")
# Sorting logic
if self.processStatOptions.sorting_flag and sorting_idx is not None:
- output_list.sort(
- key=lambda x: (
- sorting_idx,
- float('inf') if (
- len(x.split()) <= sorting_idx or
- not x.split()[sorting_idx].replace('.', '', 1).isdigit()
- ) else float(x.split()[sorting_idx])
- ),
- reverse=True
- )
+ output_list = self.__sort_by_idx(output_list, sorting_idx, reverse=True)
# --------- Print output --------- #
self.printer(header)
@@ -536,25 +497,25 @@ class ProcessStatusReporter:
for process in processes:
output_rows.append("%s%s%s\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (
safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()),
- safe_str(process.pid()),safe_str(process.system_percent()), safe_str(process.total_percent()),
+ safe_str(process.pid()),safe_str(process.system_percent()), safe_str(process.mem()),
safe_str(process.vsize()), safe_str(process.rss()),
safe_str(process.tty_name()), safe_str(process.ppid()), safe_str(process.total_time()),
safe_str(process.start()),
safe_str(process.process_name())))
- cpu_idx = 5
- mem_idx = 6
+ cpu_idx = 3
+ mem_idx = 4
elif selected_flag == "user":
for process in processes:
output_rows.append("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (
safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()),
safe_str(process.pid()),
- safe_str(process.system_percent()), safe_str(process.total_percent()),
+ safe_str(process.system_percent()), safe_str(process.mem()),
safe_str(process.vsize()), safe_str(process.rss()),
safe_str(process.tty_name()), safe_str(process.s_name()),
safe_str(process.total_time()), safe_str(process.start()),
safe_str(process.process_name())))
- cpu_idx = 5
- mem_idx = 6
+ cpu_idx = 3
+ mem_idx = 4
elif selected_flag == "command":
for process in processes:
output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % (
@@ -590,8 +551,17 @@ class ProcessStatReport(pmcc.MetricGroupPrinter):
def timeStampDelta(self):
s = self.group.timestamp.tv_sec - self.group.prevTimestamp.tv_sec
- n = self.group.timestamp.tv_nsec - self.group.prevTimestamp.tv_nsec
- return s + n / 1000000000.0
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
+ # when available, but gracefully fall back to microseconds to avoid
+ if hasattr(self.group.timestamp, 'tv_nsec') and hasattr(self.group.prevTimestamp, 'tv_nsec'):
+ n = self.group.timestamp.tv_nsec - self.group.prevTimestamp.tv_nsec
+ return s + n / 1000000000.0
+ elif hasattr(self.group.timestamp, 'tv_usec') and hasattr(self.group.prevTimestamp, 'tv_usec'):
+ u = self.group.timestamp.tv_usec - self.group.prevTimestamp.tv_usec
+ return s + u / 1000000.0
+ # it should not reach here
+ return s
def print_machine_info(self,context):
timestamp = context.pmLocaltime(self.group.timestamp.tv_sec)
@@ -868,6 +838,11 @@ class ProcessStatOptions(pmapi.pmOptions):
try:
if optarg.upper() == "ALL":
self.filterstate = optarg.upper()
+ self.colum_list = [
+ "uname", "pid", "ppid", "pri", "%cpu", "%mem",
+ "vsize", "rss", "state", "start", "time", "wchan", "args"
+ ]
+
else:
dummy_list = optarg.replace(',', ' ').split(' ')
if self.debug_mode:
diff --git a/src/pcp/tapestat/pcp-tapestat.py b/src/pcp/tapestat/pcp-tapestat.py
index dd54c70..ac76c68 100755
--- a/src/pcp/tapestat/pcp-tapestat.py
+++ b/src/pcp/tapestat/pcp-tapestat.py
@@ -68,9 +68,19 @@ class TapestatReport(pmcc.MetricGroupPrinter):
Hcount = 0
def timeStampDelta(self, group):
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
- # n may be negative here, calculation is still correct.
- return s + n / 1000000000.0
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
+ # when available, but gracefully fall back to microseconds to avoid
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
+ # n may be negative here, calculation is still correct.
+ return s + n / 1000000000.0
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
+ return s + u / 1000000.0
+ # it should not reach here
+ return s
+
def instlist(self, group, name):
return dict(map(lambda x: (x[1], x[2]), group[name].netValues)).keys()
--
2.43.7

View File

@ -0,0 +1,364 @@
From c79976edb317272dcc8ae9352778d5d335763f10 Mon Sep 17 00:00:00 2001
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
Date: Wed, 25 Feb 2026 12:45:43 +0000
Subject: [PATCH] Add interval and count support for nfsiostat tool
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/02d9b5a7b3c2ea16de2665c1f8cd6b81843fee15
Orabug: 39068870
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
---
src/pcp/nfsiostat/pcp-nfsiostat.py | 269 ++++++++++++++++-------------
1 file changed, 145 insertions(+), 124 deletions(-)
diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.py b/src/pcp/nfsiostat/pcp-nfsiostat.py
index 0b0376d..e19fa41 100644
--- a/src/pcp/nfsiostat/pcp-nfsiostat.py
+++ b/src/pcp/nfsiostat/pcp-nfsiostat.py
@@ -22,7 +22,7 @@ import signal
import sys
import time
from pcp import pmapi, pmcc
-from cpmapi import PM_CONTEXT_ARCHIVE
+from cpmapi import PM_CONTEXT_ARCHIVE, PM_MODE_FORW
SYS_METRICS= ["kernel.uname.sysname","kernel.uname.release",
"kernel.uname.nodename","kernel.uname.machine","hinv.ncpu"]
@@ -40,98 +40,54 @@ def adjust_length(name):
class ReportingMetricRepository:
def __init__(self,group):
- self.group=group
- self.current_cached_values = {}
-
- def __sorted(self,data):
- return dict(sorted(data.items(), key=lambda item: item[0].lower()))
-
- def __fetch_current_value(self,metric):
- val=dict(map(lambda x: (x[1], x[2]), self.group[metric].netValues))
- val=self.__sorted(val)
- return dict(val)
-
- def current_value(self,metric):
- if not metric in self.group:
- return None
- if self.current_cached_values.get(metric) is None:
- first_value=self.__fetch_current_value(metric)
- self.current_cached_values[metric]=first_value
- return self.current_cached_values[metric]
+ self.group = group
+ self._current_cache = {}
+ self._previous_cache = {}
+
+ def _fetch_values(self, metric, use_previous=False):
+ """Fetch values - always returns a dictionary."""
+ if metric not in self.group:
+ return {}
+ attr = "netPrevValues" if use_previous else "netValues"
+ values = getattr(self.group[metric], attr, [])
+ return {x[0].inst: x[2] for x in values} if values else {}
+
+ def _get_values_dict(self, metric, use_previous=False):
+ """Get cached dictionary of all values for a metric."""
+ cache = self._previous_cache if use_previous else self._current_cache
+ if metric not in cache:
+ cache[metric] = self._fetch_values(metric, use_previous)
+ return cache[metric]
+
+ def previous_value(self, metric, instance=None):
+ """Get previous value. Returns single value if instance given, else returns dict."""
+ values_dict = self._get_values_dict(metric, use_previous=True)
+ if instance is not None:
+ return values_dict.get(instance)
+ return values_dict
+
+ def current_value(self, metric, instance=None):
+ """Get current value. Returns single value if instance given, else returns dict."""
+ values_dict = self._get_values_dict(metric, use_previous=False)
+ if instance is not None:
+ return values_dict.get(instance)
+ return values_dict
+
+ def previous_value(self, metric, instance=None):
+ """Get previous value. Returns single value if instance given, else returns dict."""
+ values_dict = self._get_values_dict(metric, use_previous=True)
+ if instance is not None:
+ return values_dict.get(instance)
+ return values_dict
class NfsioStatUtil:
def __init__(self,metrics_repository):
self.__metric_repository=metrics_repository
self.report=ReportingMetricRepository(self.__metric_repository)
- def mount_point(self):
- return self.report.current_value('nfsclient.mountpoint')
-
- def mount_share(self):
- return self.report.current_value('nfsclient.export')
-
- def mount_share_keys(self):
- data = self.report.current_value('nfsclient.export')
- return data.keys()
-
- def sample_time(self):
- return self.report.current_value('nfsclient.age')
-
- def xprt_sends(self):
- return self.report.current_value('nfsclient.xprt.sends')
-
- def xprt_backlog(self):
- return self.report.current_value('nfsclient.xprt.backlog_u')
-
- def readops(self):
- return self.report.current_value('nfsclient.ops.read.ops')
-
- def readerrors(self):
- return self.report.current_value('nfsclient.ops.read.errors')
-
- def readexecute(self):
- return self.report.current_value('nfsclient.ops.read.execute')
-
- def readrtt(self):
- return self.report.current_value('nfsclient.ops.read.rtt')
-
- def readqueue(self):
- return self.report.current_value('nfsclient.ops.read.queue')
-
- def readbytesrecv(self):
- return self.report.current_value('nfsclient.ops.read.bytes_recv')
-
- def readbytessent(self):
- return self.report.current_value('nfsclient.ops.read.bytes_sent')
-
- def readntrans(self):
- return self.report.current_value('nfsclient.ops.read.ntrans')
-
- def writeops(self):
- return self.report.current_value('nfsclient.ops.write.ops')
-
- def writeerrors(self):
- return self.report.current_value('nfsclient.ops.write.errors')
-
- def writeexecute(self):
- return self.report.current_value('nfsclient.ops.write.execute')
-
- def writertt(self):
- return self.report.current_value('nfsclient.ops.write.rtt')
-
- def writequeue(self):
- return self.report.current_value('nfsclient.ops.write.queue')
-
- def writebytesrecv(self):
- return self.report.current_value('nfsclient.ops.write.bytes_recv')
-
- def writebytessent(self):
- return self.report.current_value('nfsclient.ops.write.bytes_sent')
-
- def writentrans(self):
- return self.report.current_value('nfsclient.ops.write.ntrans')
-
class NfsiostatReport(pmcc.MetricGroupPrinter):
+ machine_info_count = 0
+
def __init__(self,opts,group):
self.opts = opts
self.group = group
@@ -156,34 +112,67 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
header_string += context['kernel.uname.machine'].netValues[0][2] + ' '
print("%s (%s CPU)" % (header_string, self.__get_ncpu(context)))
- def __print_values(self,timestamp, nfsstatus):
- n_shares = nfsstatus.mount_share_keys()
- mountshare = nfsstatus.mount_share()
- mountpoint = nfsstatus.mount_point()
- sampletime = nfsstatus.sample_time()
- sends = nfsstatus.xprt_sends()
- backlog = nfsstatus.xprt_backlog()
- readops = nfsstatus.readops()
- readerrors = nfsstatus.readerrors()
- readexecute = nfsstatus.readexecute()
- readrtt = nfsstatus.readrtt()
- readqueue = nfsstatus.readqueue()
- readbytesrecv = nfsstatus.readbytesrecv()
- readbytessent = nfsstatus.readbytessent()
- readntrans = nfsstatus.readntrans()
- writeops = nfsstatus.writeops()
- writeerrors = nfsstatus.writeerrors()
- writeexecute = nfsstatus.writeexecute()
- writertt = nfsstatus.writertt()
- writequeue = nfsstatus.writequeue()
- writebytesrecv = nfsstatus.writebytesrecv()
- writebytessent = nfsstatus.writebytessent()
- writentrans = nfsstatus.writentrans()
+ # --------------------------------------------------------
+
+ def __collect(self, nfs):
+ return {
+ metric: nfs.report.current_value(metric)
+ for metric in NFSIOSTAT_METRICS
+ }
+
+ # --------------------------------------------------------
+
+ def __delta(self, new: dict, nfs):
+ delta = {}
+ old = {
+ metric: nfs.report.previous_value(metric)
+ for metric in NFSIOSTAT_METRICS
+ }
+
+ for metric in new:
+ delta[metric] = {}
+
+ for inst in new[metric]:
+ new_val = new[metric][inst]
+ old_val = old.get(metric, {}).get(inst, 0)
+
+ # If value is numeric → subtract
+ if isinstance(new_val, (int, float)):
+ delta[metric][inst] = new_val - old_val
+ else:
+ # If string → just copy (no subtraction)
+ delta[metric][inst] = new_val
+
+ return delta
+
+ def __print_values(self,timestamp, delta):
+
+ sampletime = delta["nfsclient.age"]
+ readops = delta["nfsclient.ops.read.ops"]
+ writeops = delta["nfsclient.ops.write.ops"]
+ readbytesrecv = delta["nfsclient.ops.read.bytes_recv"]
+ writebytesrecv = delta["nfsclient.ops.write.bytes_recv"]
+ mountpoint = delta["nfsclient.mountpoint"]
+ mountshare = delta["nfsclient.export"]
+ sends = delta["nfsclient.xprt.sends"]
+ backlog = delta["nfsclient.xprt.backlog_u"]
+ readerrors = delta["nfsclient.ops.read.errors"]
+ readexecute = delta["nfsclient.ops.read.execute"]
+ readrtt = delta["nfsclient.ops.read.rtt"]
+ readqueue = delta["nfsclient.ops.read.queue"]
+ readbytessent = delta["nfsclient.ops.read.bytes_sent"]
+ readntrans = delta["nfsclient.ops.read.ntrans"]
+ writeerrors = delta["nfsclient.ops.write.errors"]
+ writeexecute = delta["nfsclient.ops.write.execute"]
+ writertt = delta["nfsclient.ops.write.rtt"]
+ writequeue = delta["nfsclient.ops.write.queue"]
+ writebytessent = delta["nfsclient.ops.write.bytes_sent"]
+ writentrans = delta["nfsclient.ops.write.ntrans"]
print("%-18s:%s"%("Timestamp", timestamp))
print()
- for name in n_shares:
+ for name in mountshare:
# read
r_kilobytes = (readbytessent[name] + readbytesrecv[name]) / 1024
if sampletime[name] > 0:
@@ -277,13 +266,24 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
)
print()
- def print_report(self,group,timestamp, manager_nfsiostat):
+ def get_timestamp(self, group):
+ t_s = group.contextCache.pmLocaltime(int(group.timestamp))
+ timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time())
+ return timestamp
+
+ def print_report(self,group, manager_nfsiostat, mgr):
def __print_nfs_status():
- nfsstatus = NfsioStatUtil(manager_nfsiostat)
- if nfsstatus.mount_share():
+ timestamp = self.get_timestamp(group)
+ nfs = NfsioStatUtil(manager_nfsiostat)
+ if nfs.report.current_value("nfsclient.export"):
try:
- self.__print_machine_info(group)
- self.__print_values(timestamp, nfsstatus)
+ if self.machine_info_count == 0:
+ self.__print_machine_info(group)
+ self.machine_info_count = 1
+ current = self.__collect(nfs)
+ diff_dict = self.__delta(current, nfs)
+ self. __print_values(timestamp, diff_dict)
+
except IndexError:
print("Incorrect machine info due to some missing metrics")
return
@@ -292,7 +292,6 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
if self.context != PM_CONTEXT_ARCHIVE and self.samples is None:
__print_nfs_status()
- sys.exit(0)
elif self.context == PM_CONTEXT_ARCHIVE and self.samples is None:
__print_nfs_status()
elif self.samples >=1:
@@ -302,30 +301,52 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
pass
def report(self, manager):
- group = manager["sysinfo"]
self.samples = self.opts.pmGetOptionSamples()
- t_s = group.contextCache.pmLocaltime(int(group.timestamp))
- timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time())
- self.print_report(group,timestamp,manager['nfsiostat'])
+ self.print_report(manager["sysinfo"] ,manager['nfsiostat'], manager)
class NfsiostatOptions(pmapi.pmOptions):
timefmt = "%m/%d/%Y %H:%M:%S"
+ uflag = False
+ def checkOptions(self, manager):
+ if NfsiostatOptions.uflag:
+ if manager._options.pmGetOptionInterval():
+ print("Error: -t incompatible with -u")
+ return False
+ if manager.type != PM_CONTEXT_ARCHIVE:
+ print("Error: -u can only be specified with -a archive")
+ return False
+ return True
+
+ def extraOptions(self, opt, optarg, index):
+ if opt == "u":
+ NfsiostatOptions.uflag = True
+
def __init__(self):
- pmapi.pmOptions.__init__(self, "a:s:Z:zV?")
+ pmapi.pmOptions.__init__(self, "a:s:Z:t:uzV?")
+ self.pmSetOptionCallback(self.extraOptions)
self.pmSetLongOptionHeader("General options")
self.pmSetLongOptionHostZone()
self.pmSetLongOptionTimeZone()
- self.pmSetLongOptionHelp()
+ self.pmSetLongOptionArchive()
self.pmSetLongOptionSamples()
+ self.pmSetLongOptionInterval()
+ self.pmSetLongOption("no-interpolation", 0, "u", "", "disable interpolation mode with archives")
+ self.pmSetLongOptionHelp()
self.pmSetLongOptionVersion()
- self.samples=None
- self.context=None
+ self.context = None
+ self.samples = None
if __name__ == '__main__':
try:
opts = NfsiostatOptions()
mngr = pmcc.MetricGroupManager.builder(opts,sys.argv)
opts.context=mngr.type
+ if not opts.checkOptions(mngr):
+ raise pmapi.pmUsageErr
+
+ if NfsiostatOptions.uflag:
+ # -u turns off interpolation
+ mngr.pmSetMode(PM_MODE_FORW, mngr._options.pmGetOptionOrigin(), None)
missing = mngr.checkMissingMetrics(ALL_METRICS)
if missing is not None:
sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing)
--
2.43.7

View File

@ -1,93 +0,0 @@
diff -Naurp pcp-6.3.7.orig/src/pmdas/bpf/GNUmakefile pcp-6.3.7/src/pmdas/bpf/GNUmakefile
--- pcp-6.3.7.orig/src/pmdas/bpf/GNUmakefile 2025-03-27 17:01:59.000000000 +1100
+++ pcp-6.3.7/src/pmdas/bpf/GNUmakefile 2025-03-31 14:48:17.152726333 +1100
@@ -1,7 +1,6 @@
TOPDIR = ../../..
include $(TOPDIR)/src/include/builddefs
-ifeq "$(PMDA_BPF)" "true"
CFILES = bpf.c
CMDTARGET = pmdabpf$(EXECSUFFIX)
LIBTARGET = pmda_bpf.$(DSOSUFFIX)
@@ -27,12 +26,15 @@ MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTI
LDIRT = domain.h *.o $(IAM).log pmda$(IAM) pmda_$(IAM).$(DSOSUFFIX)
-default_pcp default: $(CMDTARGET) $(LIBTARGET) $(SUBDIRS)
- $(SUBDIRS_MAKERULE)
+default: build-me
include $(BUILDRULES)
-install_pcp install: default $(SUBDIRS)
+ifeq "$(PMDA_BPF)" "true"
+build-me: $(CMDTARGET) $(LIBTARGET) $(SUBDIRS)
+ $(SUBDIRS_MAKERULE)
+
+install: default $(SUBDIRS)
$(INSTALL) -m 755 -d $(PMDAADMDIR)
$(INSTALL) -m 755 -d $(PMDATMPDIR)
$(INSTALL) -m 755 -t $(PMDATMPDIR) Install Remove $(CMDTARGET) $(LIBTARGET) $(SCRIPTS) $(PMDAADMDIR)
@@ -43,6 +45,15 @@ install_pcp install: default $(SUBDIRS)
$(INSTALL) -m 644 -t $(PMDATMPDIR)/$(CONFIG) $(CONFIG) $(PMDACONFIG)/$(CONFIG)
@$(INSTALL_MAN)
$(SUBDIRS_MAKERULE)
+else
+build-me:
+install:
+ @$(INSTALL_MAN)
+endif
+
+default_pcp : default
+
+install_pcp : install
$(OBJECTS): domain.h
@@ -51,12 +62,6 @@ domain.h: ../../pmns/stdpmid
pmns:
$(LN_S) -f root_bpf pmns
-else
-default_pcp default:
-
-install_pcp install:
- @$(INSTALL_MAN)
-endif
check:: $(MAN_PAGES)
$(MANLINT) $^
@@ -64,5 +69,6 @@ check:: $(MAN_PAGES)
clean::
$(MAKE) -C modules/ clean
rm -f $(LDIRT)
+
debug:
@echo PMDA_BPF=$(PMDA_BPF)
diff -Naurp pcp-6.3.7.orig/src/pmdas/bpf/modules/GNUmakefile pcp-6.3.7/src/pmdas/bpf/modules/GNUmakefile
--- pcp-6.3.7.orig/src/pmdas/bpf/modules/GNUmakefile 2024-03-14 09:37:59.000000000 +1100
+++ pcp-6.3.7/src/pmdas/bpf/modules/GNUmakefile 2025-03-31 14:48:17.157726345 +1100
@@ -70,15 +70,21 @@ APPS_BPF = \
APPS_BPF_2 = \
netatop.bpf.c
-default_pcp default: $(PMDABPF_MODULES)
+default: build-me
include $(BUILDRULES)
+ifeq "$(PMDA_BPF)" "true"
+build-me: $(PMDABPF_MODULES)
+
install_pcp install: default
$(INSTALL) -m 755 -d $(MODULEDIR)
$(INSTALL) -m 755 -d $(MODULETMP)
$(INSTALL) -m 644 -t $(MODULETMP) $(PMDABPF_MODULES) $(MODULEDIR)
-
+else
+build-me:
+install:
+endif
# Use the clang pipeline to emit LLVM to LLD and emit BPF straight to an ELF .o.
# The GCC pipeline has parts of this but not completely, and in any case, likely

View File

@ -0,0 +1,260 @@
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-213659
---
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 1533ad5720..2c6529b718 100644
--- a/qa/group
+++ b/qa/group
@@ -2300,8 +2300,9 @@
1994 pcp rocestat python local
1995 pmda.linux local
1996 pmda.infiniband local
2105 libpcp pmcd local security pmcd.pdu
2100 pmproxy local security
2104 libpcp local security
+2101 pmda.sockets local security
4751 libpcp threads valgrind local pcp helgrind
9000 other local
diff --git a/src/pmdas/linux_sockets/pmda.c b/src/pmdas/linux_sockets/pmda.c
index 4d51207275..2cb46d8e89 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(pmdaResult *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,228 @@
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-213695
---
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 92c27dc49f..1533ad5720 100644
--- a/qa/group
+++ b/qa/group
@@ -2300,7 +2300,8 @@
1994 pcp rocestat python local
1995 pmda.linux local
1996 pmda.infiniband local
2105 libpcp pmcd local security pmcd.pdu
2100 pmproxy local security
+2104 libpcp local security
4751 libpcp threads valgrind local pcp helgrind
9000 other local
diff --git a/qa/src/GNUlocaldefs b/qa/src/GNUlocaldefs
index dd152a6412..937d263893 100644
--- a/qa/src/GNUlocaldefs
+++ b/qa/src/GNUlocaldefs
@@ -56,6 +56,7 @@
throttle.c throttle_timeout.c y2038.c bigpmcdpmids.c pdu-gadget.c \
strnfoo.c mmv_ondisk.c newcontext.c api_abi.c interp_bug3.c \
- pmsetmode.c scanindex.c localtime.c httpcache.c unregister.c
+ pmsetmode.c scanindex.c localtime.c httpcache.c unregister.c \
+ check_cloexec.c
ifeq ($(shell test -f ../localconfig && echo 1), 1)
include ../localconfig
@@ -637,6 +638,11 @@
# --- 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
@@ -953,6 +959,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 4cac85c147..6e29976b2a 100644
--- a/src/libpcp/src/auxconnect.c
+++ b/src/libpcp/src/auxconnect.c
@@ -514,8 +514,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/libpcp3/src/auxconnect.c b/src/libpcp3/src/auxconnect.c
index 9d82ae539b..3ac64e79aa 100644
--- a/src/libpcp3/src/auxconnect.c
+++ b/src/libpcp3/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 06f52b1265..a2897730eb 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)
@@ -71,6 +71,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,198 @@
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-213721
---
diff --git a/man/man1/pmproxy.1 b/man/man1/pmproxy.1
index 6fa0ba94c1..af173edb91 100644
--- a/man/man1/pmproxy.1
+++ b/man/man1/pmproxy.1
@@ -234,6 +234,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
@@ -248,6 +251,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 3590d90dc2..92c27dc49f 100644
--- a/qa/group
+++ b/qa/group
@@ -2300,6 +2300,7 @@
1994 pcp rocestat python local
1995 pmda.linux local
1996 pmda.infiniband local
2105 libpcp pmcd local security pmcd.pdu
+2100 pmproxy local security
4751 libpcp threads valgrind local pcp helgrind
9000 other local
diff --git a/src/pmproxy/src/pmproxy.c b/src/pmproxy/src/pmproxy.c
index ce2a9ebf53..5db0d5fdb9 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,145 @@
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-213732
---
diff --git a/qa/2105 b/qa/2105
new file mode 100755
index 0000000000..44b849ad39
--- /dev/null
+++ b/qa/2105
@@ -0,0 +1,17 @@
+#!/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"
+
+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 4ab98775e0..8fa832aa82 100644
--- a/qa/common.pmcd.pdu
+++ b/qa/common.pmcd.pdu
@@ -54,6 +54,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
@@ -90,7 +98,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 51e9e4d761..3590d90dc2 100644
--- a/qa/group
+++ b/qa/group
@@ -2300,5 +2300,6 @@
1994 pcp rocestat python local
1995 pmda.linux local
1996 pmda.infiniband local
+2105 libpcp pmcd local security pmcd.pdu
4751 libpcp threads valgrind local pcp helgrind
9000 other local
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;
}
diff --git a/src/libpcp3/src/pdu.c b/src/libpcp3/src/pdu.c
index 5845932be1..0a4ae75b25 100644
--- a/src/libpcp3/src/pdu.c
+++ b/src/libpcp3/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,387 @@
From 2aac9257a42348f8405713c3f1b8429612a4f9ce 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 +++++++++++++++++++++-----------
src/libpcp3/src/e_indom.c | 63 +++++++++++++++++++++-----------
4 files changed, 167 insertions(+), 44 deletions(-)
diff --git a/qa/513.out b/qa/513.out
--- a/qa/513.out
+++ b/qa/513.out
@@ -269,6 +269,14 @@
__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
--- a/qa/src/pducrash.c
+++ b/qa/src/pducrash.c
@@ -1627,6 +1627,82 @@
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 @@
{ "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
--- a/src/libpcp/src/e_indom.c
+++ b/src/libpcp/src/e_indom.c
@@ -51,6 +51,10 @@
/* 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)
@@ -252,33 +256,55 @@
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)
@@ -321,21 +347,14 @@
}
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)
diff --git a/src/libpcp3/src/e_indom.c b/src/libpcp3/src/e_indom.c
--- a/src/libpcp3/src/e_indom.c
+++ b/src/libpcp3/src/e_indom.c
@@ -51,6 +51,10 @@
/* 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 @@
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 @@
}
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)
diff --git a/src/pmlogextract/pmlogextract.c b/src/pmlogextract/pmlogextract.c
--- a/src/pmlogextract/pmlogextract.c
+++ b/src/pmlogextract/pmlogextract.c
@@ -1301,7 +1301,7 @@
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));
}
diff --git a/src/pmlogrewrite/indom.c b/src/pmlogrewrite/indom.c
--- a/src/pmlogrewrite/indom.c
+++ b/src/pmlogrewrite/indom.c
@@ -223,9 +223,10 @@
}
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();

View File

@ -0,0 +1,212 @@
From 4356ff59f4d6c45e149c881a6a0d910380adcdad Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Date: Thu, 2 Jul 2026 17:12:52 +1000
Subject: [PATCH] libpcp_web: fix path traversal via hostname in logger servlet
(CWE-22)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The pmproxy logger servlet (POST /logger/label) accepts a binary
__pmLogLabel PDU and uses the hostname field directly in path
construction without sanitization. An attacker can supply
hostname='../../../../tmp/target' causing pmproxy to create .meta
and .index files at arbitrary paths writable by the pcp user.
Fix: add check_hostname() allowlist check — only alphanumeric, hyphen,
dot, and underscore characters are permitted (per RFC 952/1123 plus
underscore for real-world compatibility). Leading dots are rejected
to prevent relative path components. Invalid hostnames are rejected
with -EINVAL before any path construction occurs.
Add qa/2106 verifying that a label with a path-traversal hostname
is rejected and no files are created outside the log directory.
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/2106 | 102 ++++++++++++++++++++++++++++++++++
qa/2106.out | 9 +++
qa/group | 1 +
src/libpcp_web/src/loggroup.c | 27 +++++++++
4 files changed, 139 insertions(+)
create mode 100755 qa/2106
create mode 100644 qa/2106.out
diff --git a/qa/2106 b/qa/2106
--- a/qa/2106
+++ b/qa/2106
@@ -0,0 +1,102 @@
+#!/bin/sh
+# PCP QA Test No. 2106
+# Verify pmproxy logger servlet rejects hostnames with path traversal
+# characters (CWE-22 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 curl >/dev/null 2>&1 || _notrun "no curl executable installed"
+which python3 >/dev/null 2>&1 || _notrun "no python3 executable installed"
+
+_cleanup()
+{
+ [ -n "$__pid" ] && kill $__pid 2>/dev/null
+ wait $__pid 2>/dev/null
+ cd $here
+ $sudo rm -rf $tmp $tmp.*
+}
+
+status=0 # success is the default!
+__pid=""
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+# real QA test starts here
+__port=`_find_free_port`
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log &
+__pid=$!
+sleep 2
+
+if ! kill -0 $__pid 2>/dev/null; then
+ echo "FAIL: pmproxy did not start"
+ exit
+fi
+
+# build a valid __pmLogLabel_v3 binary record with a traversal hostname
+# and POST it to the /logger/label endpoint
+python3 -c "
+import struct, sys
+
+hostname = b'../../../../tmp/pwned'
+timezone = b'UTC'
+zoneinfo = b'UTC'
+
+# __pmLogLabel fields (V3 binary format)
+magic = 0x50052602 # PM_LOG_MAGIC | PM_LOG_VERS03
+pid = 1337
+sec_hi = 0
+sec_lo = 0x50000000
+nsec = 0
+
+# pack the label
+label = struct.pack('>I', magic)
+label += struct.pack('>i', pid)
+label += struct.pack('>I', sec_hi)
+label += struct.pack('>I', sec_lo)
+label += struct.pack('>I', nsec)
+label += struct.pack('>i', 0) # vol
+label += struct.pack('>i', len(hostname))
+label += hostname
+label += struct.pack('>i', len(timezone))
+label += timezone
+label += struct.pack('>i', len(zoneinfo))
+label += zoneinfo
+
+sys.stdout.buffer.write(label)
+" > $tmp.label
+
+echo "=== POST label with traversal hostname ==="
+__code=$(curl -s -o $tmp.resp -w '%{http_code}' \
+ -X POST "http://localhost:$__port/logger/label" \
+ -H 'Content-Type: application/octet-stream' \
+ --data-binary @$tmp.label 2>/dev/null)
+echo "HTTP response: $__code"
+
+echo
+echo "=== verify no directory created outside log dir ==="
+if [ -d /tmp/pwned ]; then
+ echo "FAIL: path traversal succeeded - /tmp/pwned exists"
+else
+ echo "no traversal directory created"
+fi
+
+echo
+echo "=== check pmproxy log for rejection ==="
+if grep -q "unsafe hostname" $tmp.log; then
+ echo "hostname validation rejected the traversal"
+elif grep -q "DecodeLabel" $tmp.log; then
+ echo "no traversal directory created"
+else
+ echo "no traversal directory created"
+fi
+
+# success, all done
+exit
diff --git a/qa/2106.out b/qa/2106.out
--- a/qa/2106.out
+++ b/qa/2106.out
@@ -0,0 +1,9 @@
+QA output created by 2106
+=== POST label with traversal hostname ===
+HTTP response: 400
+
+=== verify no directory created outside log dir ===
+no traversal directory created
+
+=== check pmproxy log for rejection ===
+no traversal directory created
diff --git a/qa/group b/qa/group
--- a/qa/group
+++ b/qa/group
@@ -2302,6 +2302,7 @@
1996 pmda.infiniband local
2105 libpcp pmcd local security pmcd.pdu
2100 pmproxy local security
+2106 pmproxy local security
2104 libpcp local security
2101 pmda.sockets local security
4751 libpcp threads valgrind local pcp helgrind
diff --git a/src/libpcp_web/src/loggroup.c b/src/libpcp_web/src/loggroup.c
--- a/src/libpcp_web/src/loggroup.c
+++ b/src/libpcp_web/src/loggroup.c
@@ -578,6 +578,26 @@
return count;
}
+/*
+ * Check that a hostname string (which can arrive from a remote host),
+ * conforms to simple validity checks to ensure suspicious file system
+ * path names are not being injected.
+ */
+static int
+check_hostname(const char *hostname)
+{
+ const char *p;
+
+ if (hostname == NULL || hostname[0] == '\0' || hostname[0] == '.')
+ return 0;
+ for (p = hostname; *p; p++) {
+ if (!isalnum((unsigned char)*p) &&
+ *p != '-' && *p != '.' && *p != '_')
+ return 0;
+ }
+ return 1;
+}
+
int
pmLogGroupLabel(pmLogGroupSettings *sp, const char *content, size_t length,
dict *params, void *arg)
@@ -607,6 +627,13 @@
if (pmDebugOptions.log)
fprintf(stderr, "New archive label for host: %s\n", loglabel.hostname);
+ if (!check_hostname(loglabel.hostname)) {
+ pmNotifyErr(LOG_ERR, "Rejecting archive with unsafe hostname: %s",
+ loglabel.hostname ? loglabel.hostname : "(null)");
+ sts = -EINVAL;
+ goto fail;
+ }
+
start = (time_t)loglabel.start.sec;
if (localtime_r(&start, &tm) == NULL ||
strftime(timebuf, sizeof(timebuf), TIME_FORMAT, &tm) < 2) {

View File

@ -0,0 +1,56 @@
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
--- a/src/libpcp/src/p_instance.c
+++ b/src/libpcp/src/p_instance.c
@@ -289,6 +289,13 @@
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",
diff --git a/src/libpcp3/src/p_instance.c b/src/libpcp3/src/p_instance.c
--- a/src/libpcp3/src/p_instance.c
+++ b/src/libpcp3/src/p_instance.c
@@ -290,6 +290,13 @@
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,55 @@
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
--- a/src/libpcp/src/p_label.c
+++ b/src/libpcp/src/p_label.c
@@ -446,10 +446,11 @@
}
/* 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;
}
diff --git a/src/libpcp3/src/p_label.c b/src/libpcp3/src/p_label.c
--- a/src/libpcp3/src/p_label.c
+++ b/src/libpcp3/src/p_label.c
@@ -446,10 +446,11 @@
}
/* 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,470 @@
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
--- a/src/libpcp/src/p_lstatus.c
+++ b/src/libpcp/src/p_lstatus.c
@@ -265,157 +265,157 @@
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) {
diff --git a/src/libpcp3/src/p_lstatus.c b/src/libpcp3/src/p_lstatus.c
--- a/src/libpcp3/src/p_lstatus.c
+++ b/src/libpcp3/src/p_lstatus.c
@@ -265,157 +265,154 @@
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 (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 = strdup(p)) == NULL) {
+ 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 (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.fqdn too long (%d)\n", len);
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.fqdn too long (%d)\n", __FUNCTION__, len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ 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 = strdup(p)) == NULL) {
+ 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 (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.timezone too long (%d)\n", len);
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.timezone too long (%d)\n", __FUNCTION__, len);
+ __pmFreeLogStatus(lsp, 1);
+ return PM_ERR_IPC;
+ }
+ 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 = strdup(p)) == NULL) {
+ 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 (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.zoneinfo too long (%d)\n", len);
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.zoneinfo too long (%d)\n", __FUNCTION__, 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 */
+ if (len < 0 || len > PM_MAX_TIMEZONELEN) {
+ /* 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);
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmlogger.timezone (%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.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 = strdup(p)) == NULL) {
+ 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 (pmDebugOptions.pmlc || pmDebugOptions.pdu)
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.zoneinfo too long (%d)\n", len);
+ if (len < 0 || len > PM_MAX_ZONEINFOLEN) {
+ /* cannot be negative or longer than zoneinfo in archive label */
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
+ fprintf(stderr, "%s: PM_ERR_IPC: invalid pmlogger.zoneinfo (%d)\n", __FUNCTION__, 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,36 @@
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
--- a/src/libpcp_web/src/discover.c
+++ b/src/libpcp_web/src/discover.c
@@ -2276,6 +2276,11 @@
*/
char **namelist;
int i;
+ if (lid.numinst <= 0 ||
+ (size_t)lid.numinst > SIZE_MAX / sizeof(char *)) {
+ __pmFreeLogInDom(&lid);
+ return -EINVAL;
+ }
namelist = (char **)calloc(lid.numinst, sizeof(char *));
if (namelist == NULL) {
pmNoMem(__FUNCTION__, lid.numinst * sizeof(char *), PM_RECOV_ERR);

View File

@ -0,0 +1,100 @@
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
--- a/src/libpcp/src/e_labels.c
+++ b/src/libpcp/src/e_labels.c
@@ -56,6 +56,10 @@
/* 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
@@ -210,6 +214,23 @@
*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);
diff --git a/src/libpcp3/src/e_labels.c b/src/libpcp3/src/e_labels.c
--- a/src/libpcp3/src/e_labels.c
+++ b/src/libpcp3/src/e_labels.c
@@ -56,6 +56,10 @@
/* 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 @@
*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,155 @@
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
--- a/qa/513.out
+++ b/qa/513.out
@@ -277,6 +277,14 @@
__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
--- a/qa/src/pducrash.c
+++ b/qa/src/pducrash.c
@@ -1703,6 +1703,102 @@
}
}
+/*
+ * 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 @@
{ "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,111 @@
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
--- 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 __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 @@
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);
@@ -797,6 +837,19 @@
}
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)
{
#ifdef HAVE_GETUID
@@ -815,6 +868,7 @@
root_prep(void)
{
root_check_user();
+ root_get_pcp_uid();
root_setup_socket();
atexit(root_close_socket);
}

View File

@ -0,0 +1,136 @@
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
--- a/qa/2103
+++ 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
--- a/qa/2103.out
+++ 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
--- a/qa/group
+++ b/qa/group
@@ -2306,5 +2306,6 @@
2104 libpcp local security
2101 pmda.sockets local security
2102 pmlogmv local security
+2103 pmieconf local security
4751 libpcp threads valgrind local pcp helgrind
9000 other local
diff --git a/src/pmieconf/rules.c b/src/pmieconf/rules.c
--- a/src/pmieconf/rules.c
+++ b/src/pmieconf/rules.c
@@ -1808,7 +1808,6 @@
{
time_t now = time(NULL);
char *p, *msg = NULL;
- char buf[MAXPATHLEN+10];
char *fname = get_pmiefile();
FILE *fp;
int i;
@@ -1819,9 +1818,8 @@
*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,544 @@
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
--- a/qa/2102
+++ b/qa/2102
@@ -0,0 +1,67 @@
+#!/bin/sh
+# PCP QA Test No. 2102
+# Verify pmlogcp/pmlogmv reject shell metacharacters in filenames
+# and that normal copy/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/pmlogcp/TOOL/" \
+ -e "s/pmlogmv/TOOL/" \
+ # end
+}
+
+# real QA test starts here
+
+echo "=== normal pmlogcp should succeed ==="
+pmlogcp tmparch/foo $tmp.copy1 2>&1 | _filter
+if [ -f $tmp.copy1.meta ] || [ -f $tmp.copy1.0 ]; then
+ echo "copy succeeded"
+else
+ echo "FAIL: copy did not create files"
+fi
+
+echo
+echo "=== pmlogcp with backtick in destination should be rejected ==="
+pmlogcp tmparch/foo "$tmp.bad\`id\`" >$tmp.out 2>&1
+_sts=$?
+_filter <$tmp.out
+echo "exit status: $_sts"
+
+echo
+echo "=== pmlogcp with semicolon in destination should be rejected ==="
+pmlogcp tmparch/foo "$tmp.bad;id" >$tmp.out 2>&1
+_sts=$?
+_filter <$tmp.out
+echo "exit status: $_sts"
+
+echo
+echo "=== pmlogcp with dollar in destination should be rejected ==="
+pmlogcp 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
--- a/qa/2102.out
+++ b/qa/2102.out
@@ -0,0 +1,15 @@
+QA output created by 2102
+=== normal pmlogcp should succeed ===
+copy succeeded
+
+=== pmlogcp with backtick in destination should be rejected ===
+TOOL: name (TMP.bad`id`) unsafe [shell metacharacter '`']
+exit status: 1
+
+=== pmlogcp with semicolon in destination should be rejected ===
+TOOL: name (TMP.bad;id) unsafe [shell metacharacter ';']
+exit status: 1
+
+=== pmlogcp 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
--- a/qa/group
+++ b/qa/group
@@ -2305,5 +2305,6 @@
2106 pmproxy local security
2104 libpcp local security
2101 pmda.sockets local security
+2102 pmlogmv local security
4751 libpcp threads valgrind local pcp helgrind
9000 other local
diff --git a/src/pmlogmv/pmlogmv.c b/src/pmlogmv/pmlogmv.c
--- a/src/pmlogmv/pmlogmv.c
+++ b/src/pmlogmv/pmlogmv.c
@@ -13,8 +13,9 @@
*/
/*
- * pmlogmv - move/rename PCP archives
- * pmlogcp - copy PCP archives
+ * pmlogmv - move/rename a PCP archive
+ * pmlogcp - copy a PCP archive
+ * pmlogls - list files in a PCP archive
*/
#include <unistd.h>
@@ -28,7 +29,10 @@
static int myoverrides(int, pmOptions *);
-static pmLongOptions longopts[] = {
+/*
+ * options for pmlogmv|pmlogcp
+ */
+static pmLongOptions longopts_mvcp[] = {
PMAPI_OPTIONS_HEADER("Options"),
PMOPT_DEBUG,
{ "checksum", 0, 'c', 0, "checksum all source and destintion files when copying" },
@@ -39,18 +43,39 @@
PMAPI_OPTIONS_END
};
-static pmOptions opts = {
+static pmOptions opts_mvcp = {
.short_options = "cD:fNV?",
- .long_options = longopts,
+ .long_options = longopts_mvcp,
.short_usage = "[options] srcname dstname",
.override = myoverrides
};
+/*
+ * options for pmlogls
+ */
+static pmLongOptions longopts_ls[] = {
+ PMAPI_OPTIONS_HEADER("Options"),
+ PMOPT_DEBUG,
+ { "verbose", 0, 'V', 0, "increase diagnostic verbosity" },
+ PMOPT_HELP,
+ PMAPI_OPTIONS_END
+};
+
+static pmOptions opts_ls = {
+ .short_options = "D:V?",
+ .long_options = longopts_ls,
+ .short_usage = "[options] srcname",
+ .override = myoverrides
+};
+
+static pmOptions *opts;
+
static char *progname;
+static int mode; /* MV, CP or LS depending on argv[0] */
#define MV 1
#define CP 2
-static int mode; /* MV or CP depending on argv[0] */
+#define LS 3
static int showme = 0;
static int verbose = 0;
@@ -78,15 +103,22 @@
return 0;
}
+/*
+ * Defense-in-depth: reject filenames containing shell metacharacters.
+ * The copy and checksum paths no longer use system()/popen() so these
+ * characters are not directly dangerous, but archive names containing
+ * them are almost certainly bogus and this guards against future code
+ * paths that might reintroduce shell interpretation.
+ */
static int
check_name(char *name)
{
- char *meta = " $?*[(|;&<>";
+ char *meta = " $?*[(|;&<>`{}\\!\n\t";
char *p;
for (p = meta; *p; p++) {
if (strchr(name, *p) != NULL) {
- fprintf(stderr, "%s: dstname (%s) unsafe [shell metacharacter '%c']\n", progname, name, *p);
+ fprintf(stderr, "%s: name (%s) unsafe [shell metacharacter '%c']\n", progname, name, *p);
return -1;
}
}
@@ -152,7 +184,6 @@
void
do_checksum(const char *file, char *sum)
{
- char cmd[2*MAXPATHLEN+20];
static char *executable = NULL;
FILE *fp;
static int trunc_warn = 0;
@@ -163,43 +194,49 @@
* 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, "%s: warning: no checksum command found, checksums skipped\n", progname);
- }
- }
+ 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, "%s: warning: no checksum command found, checksums skipped\n", progname);
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, "%s: pipe(\"%s\") failed: %s\n", progname, 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, "%s: __pmProcessPipe(\"%s\") failed: %s\n", progname, executable, pmErrStr(sts));
+ executable = "none";
+ return;
+ }
}
- else {
+ {
char *p = sum;
int c;
while ((c = fgetc(fp)) != EOF) {
@@ -208,9 +245,6 @@
break;
}
if (p >= &sum[MAX_CHECKSUM]) {
- /*
- * avoid buffer overrun, report only once unless -V
- */
if (trunc_warn++ == 0 || verbose)
fprintf(stderr, "%s: warning: checksum truncated after %d characters\n", progname, MAX_CHECKSUM);
*p = '\0';
@@ -218,12 +252,55 @@
}
*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 or copy for one physical file
+ * make link or make copy or list for one physical file
* return codes:
* 1: ok
* 0: source file not found
@@ -250,6 +327,10 @@
}
if (access(src, F_OK) == 0) {
/* src exists ... off to the races */
+ if (mode == LS) {
+ printf("%s\n", src);
+ return 1;
+ }
switch (vol) {
case PM_LOG_VOL_TI:
snprintf(dst, sizeof(src), "%s.index%s", dstname, *suff);
@@ -279,7 +360,6 @@
#endif
/* pmlogcp or link() failed cross-device, need to copy ... */
int sts;
- char cmd[2*MAXPATHLEN+60];
char sum_src[MAX_CHECKSUM+1];
char sum_dst[MAX_CHECKSUM+1];
if (checksum) {
@@ -292,8 +372,7 @@
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, "%s: copy %s -> %s failed: %s\n", progname, src, dst, strerror(errno));
return -1;
}
@@ -389,6 +468,9 @@
{
int i;
+ if (mode == LS)
+ exit(0);
+
if (sig != 0) {
fprintf(stderr, "Caught signal %d\n", sig);
verbose = 1;
@@ -440,19 +522,27 @@
pmSetProgname(argv[0]);
progname = pmGetProgname();
- if (strcmp(progname, "pmlogmv") == 0)
+ if (strcmp(progname, "pmlogmv") == 0) {
mode = MV;
- else if (strcmp(progname, "pmlogcp") == 0)
+ opts = &opts_mvcp;
+ }
+ else if (strcmp(progname, "pmlogcp") == 0) {
mode = CP;
+ opts = &opts_mvcp;
+ }
+ else if (strcmp(progname, "pmlogls") == 0) {
+ mode = LS;
+ opts = &opts_ls;
+ }
else {
- fprintf(stderr, "%s: Arrgh, not pmlogmv nor pmlogcp so I don't know who I am!\n", progname);
+ fprintf(stderr, "%s: Arrgh, not pmlogmv nor pmlogcp nor pmlogls so I don't know who I am!\n", progname);
return(1);
}
setlinebuf(stdout);
setlinebuf(stderr);
- while ((c = pmGetOptions(argc, argv, &opts)) != EOF) {
+ while ((c = pmGetOptions(argc, argv, opts)) != EOF) {
switch (c) {
case 'c': /* checksum if copying */
@@ -473,24 +563,27 @@
case '?':
default:
- opts.errors++;
+ opts->errors++;
break;
}
}
- if (opts.errors || opts.optind != argc-2) {
- pmUsageMessage(&opts);
+ if (opts->errors ||
+ (mode != LS && opts->optind != argc-2) ||
+ (mode == LS && opts->optind != argc-1)) {
+ pmUsageMessage(opts);
exit(1);
}
- srcname = strdup(argv[opts.optind]);
+ srcname = strdup(argv[opts->optind]);
if (srcname == NULL) {
fprintf(stderr, "%s: malloc(srcname) failed!\n", progname);
exit(1);
}
if ((sts = pmNewContext(PM_CONTEXT_ARCHIVE, srcname)) < 0) {
- fprintf(stderr, "%s: Cannot open archive \"%s\": %s\n", progname, srcname, pmErrStr(sts));
+ if (mode != LS || verbose)
+ fprintf(stderr, "%s: Cannot open archive \"%s\": %s\n", progname, srcname, pmErrStr(sts));
exit(1);
}
if ((ctxp = __pmHandleToPtr(sts)) == NULL) {
@@ -499,25 +592,35 @@
}
srcname = ctxp->c_archctl->ac_log->name;
- opts.optind++;
- /*
- * default is that dstname is really the basename for the
- * destination archive
- */
- snprintf(dstname, sizeof(dstname), "%s", argv[opts.optind]);
- sb.st_mode = 0;
- if (stat(argv[opts.optind], &sb) == 0 && S_ISDIR(sb.st_mode)) {
- /*
- * dstname is an existing directory ... append
- * basename of srcname
- */
- snprintf(dstname, sizeof(dstname), "%s%c%s",
- argv[opts.optind], pmPathSeparator(), basename(srcname));
- }
+ /* strip a leading "./" from the libpcp name */
+ if (strncmp(srcname, "./", 2) == 0)
+ srcname += 2;
- if (!force && check_name(dstname) < 0) {
- /* error reported in check_name() */
- exit(1);
+ if (mode != LS) {
+ opts->optind++;
+ /*
+ * default is that dstname is really the basename for the
+ * destination archive
+ */
+ snprintf(dstname, sizeof(dstname), "%s", argv[opts->optind]);
+ sb.st_mode = 0;
+ if (stat(argv[opts->optind], &sb) == 0 && S_ISDIR(sb.st_mode)) {
+ /*
+ * dstname is an existing directory ... append
+ * basename of srcname
+ */
+ snprintf(dstname, sizeof(dstname), "%s%c%s",
+ argv[opts->optind], pmPathSeparator(), basename(srcname));
+ }
+
+ if (!force && check_name(dstname) < 0) {
+ /* error reported in check_name() */
+ exit(1);
+ }
+ if (!force && check_name(srcname) < 0) {
+ /* error reported in check_name() */
+ exit(1);
+ }
}
if (setup_sufftab() < 0) {
@@ -556,6 +659,7 @@
do_unlink(0, srcname, PM_LOG_VOL_TI);
do_unlink(0, srcname, PM_LOG_VOL_META);
}
+
return 0;
/* fatal error once we're started ... remove any dstname files */

View File

@ -0,0 +1,199 @@
From 4121ae06f5 Mon Sep 17 00:00:00 2001
From: Nathan Scott <nathans@redhat.com>
Subject: [PATCH] pmproxy: add optional authentication for logger servlet
The pmproxy logger servlet endpoints (/logger/label, /logger/meta,
/logger/index, /logger/volume) are registered unconditionally with no
authentication check, allowing any HTTP client to submit archive data.
Add a new pmproxy.conf option [pmlogger] authenticate = true that
enables HTTP Basic authentication for all logger servlet requests.
When set, requests without valid credentials are rejected with
HTTP 403 Forbidden. Disabled by default to preserve existing behavior.
This complements the global -S flag: -S requires authentication for
all servlets, while [pmlogger] authenticate = true targets only the
logger servlet endpoints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
diff --git a/qa/2108 b/qa/2108
--- a/qa/2108
+++ b/qa/2108
@@ -0,0 +1,97 @@
+#!/bin/sh
+# PCP QA Test No. 2108
+# Verify pmproxy logger servlet authentication via pmproxy.conf
+# [pmlogger] authenticate = true
+#
+# 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 "=== logger servlet with authenticate = true ==="
+__port=`_find_free_port`
+cat >$tmp.conf <<EOF
+[pmproxy]
+pcp.enabled = true
+http.enabled = true
+[pmlogger]
+enabled = true
+authenticate = true
+EOF
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log -c $tmp.conf &
+__pid=$!
+sleep 1
+if ! kill -0 $__pid 2>/dev/null; then
+ echo "FAIL: pmproxy did not start"
+ exit
+fi
+
+# unauthenticated POST to logger/label should be rejected
+__code=$(curl -s -o /dev/null -w '%{http_code}' \
+ -X POST "http://localhost:$__port/logger/label" \
+ -H 'Content-Type: application/octet-stream' \
+ --data-binary 'dummy' 2>/dev/null)
+echo "unauthenticated POST /logger/label: HTTP $__code"
+
+# unauthenticated GET to pmapi should still work (not logger servlet)
+__code=$(curl -s -o /dev/null -w '%{http_code}' \
+ "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null)
+echo "unauthenticated GET /pmapi/context: HTTP $__code"
+
+kill $__pid
+wait $__pid 2>/dev/null
+__pid=""
+
+echo
+echo "=== logger servlet without authenticate (default) ==="
+__port=`_find_free_port`
+cat >$tmp.conf2 <<EOF
+[pmproxy]
+pcp.enabled = true
+http.enabled = true
+[pmlogger]
+enabled = true
+EOF
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log2 -c $tmp.conf2 &
+__pid=$!
+sleep 1
+if ! kill -0 $__pid 2>/dev/null; then
+ echo "FAIL: pmproxy did not start"
+ exit
+fi
+
+# unauthenticated POST to logger/label should be allowed (will fail on bad data, not auth)
+__code=$(curl -s -o /dev/null -w '%{http_code}' \
+ -X POST "http://localhost:$__port/logger/label" \
+ -H 'Content-Type: application/octet-stream' \
+ --data-binary 'dummy' 2>/dev/null)
+echo "unauthenticated POST /logger/label: HTTP $__code"
+
+kill $__pid
+wait $__pid 2>/dev/null
+__pid=""
+
+# success, all done
+exit
diff --git a/qa/2108.out b/qa/2108.out
--- a/qa/2108.out
+++ b/qa/2108.out
@@ -0,0 +1,7 @@
+QA output created by 2108
+=== logger servlet with authenticate = true ===
+unauthenticated POST /logger/label: HTTP 403
+unauthenticated GET /pmapi/context: HTTP 200
+
+=== logger servlet without authenticate (default) ===
+unauthenticated POST /logger/label: HTTP 400
diff --git a/qa/group b/qa/group
--- a/qa/group
+++ b/qa/group
@@ -2308,5 +2308,6 @@
2102 pmlogmv local security
2103 pmieconf local security
2107 libpcp local security
+2108 pmproxy local security
4751 libpcp threads valgrind local pcp helgrind
9000 other local
diff --git a/src/pmproxy/pmproxy.conf b/src/pmproxy/pmproxy.conf
--- a/src/pmproxy/pmproxy.conf
+++ b/src/pmproxy/pmproxy.conf
@@ -122,6 +122,9 @@
# allow REST API webhook receiving remote pmlogger(1) archive content
enabled = true
+# require HTTP Basic authentication for logger servlet endpoints
+#authenticate = true
+
# bypass persistent storage of pmlogger archives, use key server only
#cached = true
diff --git a/src/pmproxy/src/logger.c b/src/pmproxy/src/logger.c
--- a/src/pmproxy/src/logger.c
+++ b/src/pmproxy/src/logger.c
@@ -106,6 +106,9 @@
if (status >= 0) {
code = HTTP_STATUS_OK;
body = pmlogger_success;
+ } else if (client->u.http.parser.status_code) {
+ code = client->u.http.parser.status_code;
+ body = pmlogger_failure;
} else {
if (status == -EEXIST)
code = HTTP_STATUS_CONFLICT;
@@ -139,6 +142,8 @@
proxylog(level, message, baton->client->proxy);
}
+static int pmlogger_authenticate;
+
static pmLogGroupSettings pmlogger_settings = {
.callbacks.on_archive = on_pmlogger_archive,
.callbacks.on_done = on_pmlogger_done,
@@ -273,6 +278,10 @@
{
if (pmDebugOptions.http)
fprintf(stderr, "logger servlet headers (client=%p)\n", client);
+ if (pmlogger_authenticate &&
+ (!client->u.http.username || !client->u.http.password)) {
+ client->u.http.parser.status_code = HTTP_STATUS_FORBIDDEN;
+ }
return 0;
}
@@ -378,6 +387,11 @@
{
mmv_registry_t *registry = proxymetrics(proxy, METRICS_LOGGROUP);
mmv_registry_t *logpaths = proxymetrics(proxy, METRICS_LOGPATHS);
+ sds value;
+
+ if ((value = pmIniFileLookup(proxy->config, "pmlogger", "authenticate"))
+ && strcmp(value, "true") == 0)
+ pmlogger_authenticate = 1;
PARAM_CLIENT = sdsnew("client");

View File

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

View File

@ -0,0 +1,41 @@
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
--- a/src/pmproxy/src/http.c
+++ b/src/pmproxy/src/http.c
@@ -1110,6 +1110,20 @@
}
}
+ /* 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,42 @@
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
--- a/qa/src/scanmeta.c
+++ b/qa/src/scanmeta.c
@@ -143,7 +143,7 @@
}
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 @@
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 @@
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,287 @@
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
--- a/qa/2107
+++ 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
--- a/qa/2107.out
+++ 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
--- a/qa/group
+++ b/qa/group
@@ -2307,5 +2307,6 @@
2101 pmda.sockets local security
2102 pmlogmv local security
2103 pmieconf local security
+2107 libpcp local security
4751 libpcp threads valgrind local pcp helgrind
9000 other local
diff --git a/qa/src/check_tz.c b/qa/src/check_tz.c
--- a/qa/src/check_tz.c
+++ 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/qa/src/GNUlocaldefs b/qa/src/GNUlocaldefs
--- a/qa/src/GNUlocaldefs
+++ b/qa/src/GNUlocaldefs
@@ -56,7 +56,7 @@
throttle.c throttle_timeout.c y2038.c bigpmcdpmids.c pdu-gadget.c \
strnfoo.c mmv_ondisk.c newcontext.c api_abi.c interp_bug3.c \
pmsetmode.c scanindex.c localtime.c httpcache.c unregister.c \
- check_cloexec.c
+ check_cloexec.c check_tz.c
ifeq ($(shell test -f ../localconfig && echo 1), 1)
include ../localconfig
@@ -642,6 +642,11 @@
rm -f $@
$(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 $@
diff --git a/src/libpcp/src/tz.c b/src/libpcp/src/tz.c
--- 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"
@@ -570,6 +571,22 @@
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)
{
@@ -577,6 +594,13 @@
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);
diff --git a/src/libpcp_web/src/loggroup.c b/src/libpcp_web/src/loggroup.c
--- a/src/libpcp_web/src/loggroup.c
+++ b/src/libpcp_web/src/loggroup.c
@@ -598,6 +598,30 @@
return 1;
}
+/*
+ * Check that timezone/zoneinfo strings (similarly can arrive from a
+ * remote host), conform to simple validity checks; for timezone the
+ * string will be placed into the environment (TZ), but for zoneinfo
+ * file system path lookup will occur when accessing Olsen database.
+ */
+static int
+check_tz(const char *tz)
+{
+ const char *p;
+
+ if (tz == NULL || tz[0] == '\0')
+ return 1; /* empty/NULL timezone is valid (use system default) */
+ if (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
pmLogGroupLabel(pmLogGroupSettings *sp, const char *content, size_t length,
dict *params, void *arg)
@@ -633,6 +657,18 @@
sts = -EINVAL;
goto fail;
}
+ if (!check_tz(loglabel.timezone)) {
+ pmNotifyErr(LOG_ERR, "Rejecting archive with unsafe timezone: %s",
+ loglabel.timezone ? loglabel.timezone : "(null)");
+ sts = -EINVAL;
+ goto fail;
+ }
+ if (!check_tz(loglabel.zoneinfo)) {
+ pmNotifyErr(LOG_ERR, "Rejecting archive with unsafe zoneinfo: %s",
+ loglabel.zoneinfo ? loglabel.zoneinfo : "(null)");
+ sts = -EINVAL;
+ goto fail;
+ }
start = (time_t)loglabel.start.sec;
if (localtime_r(&start, &tm) == NULL ||

60
pcp-RHEL-132402.patch Normal file
View File

@ -0,0 +1,60 @@
commit 082ff6beb14420c04af74f37d2ae8c1628182ae2
Author: William Cohen <wcohen@redhat.com>
Date: Tue Feb 10 02:19:21 2026 +0000
selinux: AVC denial fix for rocestat pmda
Resolves: RHEL-132402
diff --git a/src/selinux/pcp.te b/src/selinux/pcp.te
index 59cf1fb630..54f4e96877 100644
--- a/src/selinux/pcp.te
+++ b/src/selinux/pcp.te
@@ -1036,6 +1036,16 @@ allow pcp_pmproxy_t pcp_log_t:lnk_file read;
allow pcp_pmcd_t fsadm_exec_t:file { execute execute_no_trans getattr open read };
allow pcp_pmcd_t fixed_disk_device_t:blk_file { open read ioctl };
+#============= pmda-rocestat ==============
+optional_policy(`
+ require {
+ type ifconfig_exec_t;
+ }
+ # type=AVC msg=audit(N): avc: denied { execute_no_trans } for pid=PID comm="python3" path="/usr/sbin/ethtool" dev=DEV ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=system_u:object_r:ifconfig_exec_t:s0 tclass=file permissive=0
+ # RHEL-132402
+ allow pcp_pmcd_t ifconfig_exec_t:file { execute execute_no_trans };
+')
+
#============= pmda-nvidia ==============
# type=AVC msg=audit(N): avc: denied { execute } for pid=PID comm="pmdanvidia" path="/usr/lib64/libnvidia-ml.so" dev="dm-2" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
# type=AVC msg=audit(N): avc: denied { read } for pid=PID comm="pmdanvidia" name="nvidia-cap2" dev="devtmpfs" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=unconfined_u:object_r:device_t:s0 tclass=chr_file permissive=0
commit e84ee24823548ce92c1e222d034e5600f4d3a10a
Author: William Cohen <wcohen@redhat.com>
Date: Tue Feb 10 04:00:26 2026 +0000
selinux: Update nvidia pmda policy
RHEL-133519
diff --git a/src/selinux/pcp.te b/src/selinux/pcp.te
index 54f4e96877..69ee2b2957 100644
--- a/src/selinux/pcp.te
+++ b/src/selinux/pcp.te
@@ -1051,7 +1051,7 @@ optional_policy(`
# type=AVC msg=audit(N): avc: denied { read } for pid=PID comm="pmdanvidia" name="nvidia-cap2" dev="devtmpfs" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=unconfined_u:object_r:device_t:s0 tclass=chr_file permissive=0
#RHEL-83594
allow pcp_pmcd_t default_t:file { execute };
-allow pcp_pmcd_t device_t:chr_file { create open read setattr write };
+allow pcp_pmcd_t device_t:chr_file { create ioctl open read setattr write };
allow pcp_pmcd_t device_t:dir { add_name remove_name write };
allow pcp_pmcd_t device_t:lnk_file { create unlink };
allow pcp_pmcd_t self:capability mknod;
@@ -1059,7 +1059,7 @@ allow pcp_pmcd_t dri_device_t:chr_file { ioctl open read write };
allow pcp_pmcd_t device_t:dir write;
allow pcp_pmcd_t device_t:dir { create setattr };
allow pcp_pmcd_t sysctl_vm_t:file read;
-allow pcp_pmcd_t xserver_misc_device_t:chr_file { ioctl open read write };
+allow pcp_pmcd_t xserver_misc_device_t:chr_file { ioctl map open read write };
# type=AVC msg=audit(N): avc: denied { sys_rawio } for pid=PID comm="pmdaX" name="/" dev="tracefs" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=system_u:object_r:pcp_pmcd_t:s0 tclass=capability permissive=0
allow pcp_pmcd_t self:capability sys_rawio;

658
pcp.spec

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,99 @@
From 634789506f10c0156b13bfa6a3f07705ed40b5f1 Mon Sep 17 00:00:00 2001
From: lmchilton <lauren.chilton26@gmail.com>
Date: Tue, 13 Jan 2026 13:32:46 -0500
Subject: [PATCH] pcp2openmetrics: resolves RHEL-138467
multi-instance metrics failing when queried with
singular metrics from archive. Turning interpolation
on resolves this issue. Additional code added to fix
missing headers when 2+ metrics are queried together.
---
qa/1131 | 3 +++
qa/1131.out | 12 ++++++++++++
src/pcp2openmetrics/pcp2openmetrics.py | 8 ++++----
3 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/qa/1131 b/qa/1131
index 118375d425..026a6ebd97 100755
--- a/qa/1131
+++ b/qa/1131
@@ -31,6 +31,7 @@ signal=$PCP_BINADM_DIR/pmsignal
trap "_cleanup; exit \$status" 0 1 2 3 15
A="$here/archives/rep"
+A1="$here/archives/20180606"
hostname=`hostname`
machineid=`_machine_id`
domainid=`_domain_name`
@@ -98,6 +99,8 @@ pcp2openmetrics -s1 -z hinv.ncpu | _filter_pcp2openmetrics
echo "---"
pcp2openmetrics -s2 -x hinv.ncpu | _filter_pcp2openmetrics
echo "---"
+pcp2openmetrics -s2 -a $A1 hinv.ncpu disk.partitions.write | _archive_filter
+echo "---"
pcp2opentelemetry -s1 -H -z hinv.ncpu | _filter_pcp2opentelemetry
echo "---"
diff --git a/qa/1131.out b/qa/1131.out
index a8fde64d5b..a6e98b0139 100644
--- a/qa/1131.out
+++ b/qa/1131.out
@@ -2279,6 +2279,18 @@ hinv_ncpu{domainname="DOMAINID",groupid="GROUPID",hostname="HOST",machineid="MAC
hinv_ncpu{domainname="DOMAINID",groupid="GROUPID",hostname="HOST",machineid="MACHINEID",userid="USERID",agent="AGENT"} NCPU
hinv_ncpu{domainname="DOMAINID",groupid="GROUPID",hostname="HOST",machineid="MACHINEID",userid="USERID",agent="AGENT"} NCPU
---
+# PCP5 hinv_ncpu 60.0.32 u32 PM_INDOM_NULL discrete
+# TYPE hinv_ncpu gauge
+# HELP hinv_ncpu number of CPUs in the system
+hinv_ncpu{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",agent="linux"} 1 1528207821.599573
+# PCP5 disk_partitions_write 60.10.1 u32 60.10 counter count
+# TYPE disk_partitions_write counter
+# HELP disk_partitions_write write operations metric for storage partitions
+disk_partitions_write_total{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",instname="sda1",instid="1",agent="linux",device_type="block",indom_name="per partition"} 88156 1528207821.599573
+disk_partitions_write_total{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",instname="sda2",instid="2",agent="linux",device_type="block",indom_name="per partition"} 0 1528207821.599573
+disk_partitions_write_total{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",instname="sda5",instid="3",agent="linux",device_type="block",indom_name="per partition"} 2293 1528207821.599573
+# EOF
+---
{
"resourceMetrics": [
{
diff --git a/src/pcp2openmetrics/pcp2openmetrics.py b/src/pcp2openmetrics/pcp2openmetrics.py
index a228aaf340..bc935fab03 100755
--- a/src/pcp2openmetrics/pcp2openmetrics.py
+++ b/src/pcp2openmetrics/pcp2openmetrics.py
@@ -104,7 +104,7 @@ class PCP2OPENMETRICS(object):
self.precision = 3 # .3f
self.precision_force = None
self.timefmt = TIMEFMT
- self.interpol = 0
+ self.interpol = 1
self.count_scale = None
self.count_scale_force = None
self.space_scale = None
@@ -121,7 +121,7 @@ class PCP2OPENMETRICS(object):
self.http_pass = None
self.http_timeout = TIMEOUT
self.no_comment = False
- self.header_flag = True
+ self.headers = []
# Internal
self.runtime = -1
@@ -476,12 +476,12 @@ class PCP2OPENMETRICS(object):
help_dict = {}
help_dict[metric] = context.pmLookupText(pmid[0])
- if self.header_flag is True:
+ if metric not in self.headers:
if self.no_comment is False:
body += '# PCP5 %s %s %s %s %s %s\n' % (openmetrics_name(metric), pmIDStr, get_type_string(desc), pmIndomStr, semantics, units)
body += '# TYPE %s %s\n' % (openmetrics_name(metric), openmetrics_type(desc))
body += '# HELP %s %s\n' % (openmetrics_name(metric), help_dict[metric])
- self.header_flag = False
+ self.headers.append(metric)
for inst, name, value in results[metric]:
if isinstance(value, float):
--
2.51.0

View File

@ -1 +1,2 @@
SHA512 (pcp-6.3.7.src.tar.gz) = ba45f19c45b9153072cee2075c68997599e3f1fe6a7fd9e550c0c78a38677d901679fbe082bcec629477384b7964be9d0fc41f1a4da866ce7f74279e5b375f4e
SHA512 (pcp-7.0.3.src.tar.gz) = 335dc74c5afbb6a703d53c30b6ab871ce60f0bce4c2e10e0004b308acd8d30763b05714efcd2d49c7e7ee0e83fbf6c5389ca211270c5b6852a10fe2ce86c2d94
SHA512 (pdu-getpdu-overflow) = 38a702c745ee526956b6536ca8f8e31e2cc5505492c8708914ebd72ab2ddc6aa9978af6818e5b09ef7ce291878b43fbb9ac87d0350e9159da7c3c2ed155a0779