From 5f0a3da4bf2625eb4b59d83a770fbe5b1ac7a52a Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Wed, 19 Aug 2026 04:34:07 -0400 Subject: [PATCH] import Oracle_OSS pcp-7.0.3-5.0.1.el10_2 --- .gitignore | 3 +- ...8724847-fix-nfsclient-per-op-parsing.patch | 38 + ...duce-PCP-implementation-of-nfsiostat.patch | 575 ++++++++ ...-fix-not-to-terminate-unauthorized-p.patch | 178 +++ ...-sort-option-to-allow-sorting-by-cpu.patch | 1277 ++++++++++++++++ ...-disk-option-for-archive-space-limit.patch | 604 ++++++++ ...-restore-backward-compatibility-with.patch | 332 +++++ ...70-adds-interval-option-in-nfsiostat.patch | 364 +++++ ...ics-and-adds-numastat-process-option.patch | 1287 +++++++++++++++++ ...mdabpf-noarch-man-page-build-failure.patch | 93 -- pcp-7.0.3-CVE-2026-16524.patch | 260 ++++ pcp-7.0.3-CVE-2026-16526.patch | 228 +++ pcp-7.0.3-CVE-2026-16527.patch | 198 +++ pcp-7.0.3-CVE-2026-16529.patch | 145 ++ pcp-7.0.3-CVE-2026-16530.patch | 387 +++++ pcp-7.0.3-CVE-2026-16531.patch | 212 +++ pcp-7.0.3-OOB-pmDecodeInstance.patch | 56 + pcp-7.0.3-OOB-pmDecodeLabel.patch | 55 + pcp-7.0.3-OOB-pmDecodeLogStatus.patch | 470 ++++++ pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch | 36 + pcp-7.0.3-OOB-pmLogLoadLabelSet.patch | 100 ++ pcp-7.0.3-pducrash-oob-tests.patch | 155 ++ pcp-7.0.3-pmdaroot-peer-credentials.patch | 111 ++ pcp-7.0.3-pmieconf-command-injection.patch | 136 ++ pcp-7.0.3-pmlogmv-command-injection.patch | 544 +++++++ pcp-7.0.3-pmproxy-logger-auth.patch | 199 +++ pcp-7.0.3-pmproxy-logger-meta-network.patch | 179 +++ pcp-7.0.3-pmproxy-rest-certreqd.patch | 41 + pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch | 42 + pcp-7.0.3-timezone-zoneinfo-validation.patch | 287 ++++ pcp-RHEL-132402.patch | 60 + pcp.spec | 658 ++++----- pcp2openmetrics-archive.patch | 99 ++ sources | 3 +- 34 files changed, 8947 insertions(+), 465 deletions(-) create mode 100644 1011-orabug38724847-fix-nfsclient-per-op-parsing.patch create mode 100644 1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch create mode 100644 1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch create mode 100644 1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch create mode 100644 1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch create mode 100644 1016-pcp-system-tools-restore-backward-compatibility-with.patch create mode 100644 1017-orabug39068870-adds-interval-option-in-nfsiostat.patch create mode 100644 1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch delete mode 100644 fix-pmdabpf-noarch-man-page-build-failure.patch create mode 100644 pcp-7.0.3-CVE-2026-16524.patch create mode 100644 pcp-7.0.3-CVE-2026-16526.patch create mode 100644 pcp-7.0.3-CVE-2026-16527.patch create mode 100644 pcp-7.0.3-CVE-2026-16529.patch create mode 100644 pcp-7.0.3-CVE-2026-16530.patch create mode 100644 pcp-7.0.3-CVE-2026-16531.patch create mode 100644 pcp-7.0.3-OOB-pmDecodeInstance.patch create mode 100644 pcp-7.0.3-OOB-pmDecodeLabel.patch create mode 100644 pcp-7.0.3-OOB-pmDecodeLogStatus.patch create mode 100644 pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch create mode 100644 pcp-7.0.3-OOB-pmLogLoadLabelSet.patch create mode 100644 pcp-7.0.3-pducrash-oob-tests.patch create mode 100644 pcp-7.0.3-pmdaroot-peer-credentials.patch create mode 100644 pcp-7.0.3-pmieconf-command-injection.patch create mode 100644 pcp-7.0.3-pmlogmv-command-injection.patch create mode 100644 pcp-7.0.3-pmproxy-logger-auth.patch create mode 100644 pcp-7.0.3-pmproxy-logger-meta-network.patch create mode 100644 pcp-7.0.3-pmproxy-rest-certreqd.patch create mode 100644 pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch create mode 100644 pcp-7.0.3-timezone-zoneinfo-validation.patch create mode 100644 pcp-RHEL-132402.patch create mode 100644 pcp2openmetrics-archive.patch diff --git a/.gitignore b/.gitignore index 78ed498..fa9d2ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -pcp-6.3.7.src.tar.gz +pcp-7.0.3.src.tar.gz +pdu-getpdu-overflow diff --git a/1011-orabug38724847-fix-nfsclient-per-op-parsing.patch b/1011-orabug38724847-fix-nfsclient-per-op-parsing.patch new file mode 100644 index 0000000..3303ad1 --- /dev/null +++ b/1011-orabug38724847-fix-nfsclient-per-op-parsing.patch @@ -0,0 +1,38 @@ +From c80dc758eb0eb27fafa0b594d6c2aa1f4c3803fa Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +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 + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/85671f8874d7d4b4e57eb45bfe295de92b95415c + +Orabug: 38724847 + +Signed-off-by: Sourav Sharma + +--- + 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 + diff --git a/1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch b/1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch new file mode 100644 index 0000000..0db3d2c --- /dev/null +++ b/1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch @@ -0,0 +1,575 @@ +From ca8fbd2a827e3e7caa72331e003f93523c9f5241 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +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 +--- + 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 + diff --git a/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch b/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch new file mode 100644 index 0000000..67ed7ae --- /dev/null +++ b/1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch @@ -0,0 +1,178 @@ +From 7bc4a59bfe06f9b988ea21ad6a2decc86c942097 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +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 + +--- + 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 + diff --git a/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch b/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch new file mode 100644 index 0000000..978e16e --- /dev/null +++ b/1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch @@ -0,0 +1,1277 @@ +From abfdd79d85c170ec896594f210c69667a2b2a7ae Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Thu, 26 Mar 2026 15:36:00 +0000 +Subject: [PATCH OL9 1014/1016] pcp-ps implement --sort option to allow sorting + by %cpu, %mem + +- Implements the '--sort' option in 'pcp ps' include sorting by '%cpu' and '%mem'. + +upstream ref:- https://github.com/performancecopilot/pcp/pull/2436/ + +[Orabug:38719615] +Signed-off-by: sagar sagar + +--- + src/pcp/ps/pcp-ps.1 | 17 + + src/pcp/ps/pcp-ps.py | 926 +++++++++++++++++++++++++------------------ + 2 files changed, 566 insertions(+), 377 deletions(-) + +diff --git a/src/pcp/ps/pcp-ps.1 b/src/pcp/ps/pcp-ps.1 +index b824ebd..be2eb9c 100755 +--- a/src/pcp/ps/pcp-ps.1 ++++ b/src/pcp/ps/pcp-ps.1 +@@ -27,6 +27,8 @@ + [\f3\-P\f1 \f2pid1,pid2..\f1] + [\f3\-p\f1 \f2pid1,pid2..\f1] + [\f3\-o\f1 \f2col1,col2... or ALL\f1] ++[\f3\-O\f1 \f2%mem,%cpu\f1] ++[\f3\-d\f1] + [\f3\-Z\f1 \f2timezone\f1] + [\f3\-z\f1] + [\f3\-?\f1] +@@ -243,6 +245,21 @@ WCHAN and COMMAND. + T} + .TE + .TP ++.BR \-O\ \fI%mem,%cpu\fR ", " \fB\-\-sort=\fI%mem,%cpu\fR ++Sort the output processes by the specified column. ++This sorting can now be used in conjunction with the ++.BR \-o ++option to sort on any user-specified output column (e.g., -o pid,%mem -O %mem). ++The column given to -O/--sort must appear in the -o list. ++If the sorting column is missing from the output format, an error will be shown. ++(In case of an unsupported or missing field name, sorting will default to %cpu.) ++.TP ++.BR \-d\, ++debug mode ++.br ++Enable debug mode, which will cause debug messages to be written to ++stdout. ++.TP + .BR \-Z \ \fItimezone\fR ", "\fB\-\-timezone=\fItimezone\fR + By default, + .B pcp-ps +diff --git a/src/pcp/ps/pcp-ps.py b/src/pcp/ps/pcp-ps.py +index 987c09c..e2992ea 100755 +--- a/src/pcp/ps/pcp-ps.py ++++ b/src/pcp/ps/pcp-ps.py +@@ -60,106 +60,87 @@ class NoneHandlingPrinterDecorator: + class ReportingMetricRepository: + def __init__(self, group): + self.group = group +- self.current_cached_values = {} +- self.previous_cached_values = {} +- +- def __fetch_current_values(self, metric, instance): +- if instance: +- return dict(map(lambda x: (x[0].inst, x[2]), self.group[metric].netValues)) +- else: +- return self.group[metric].netValues[0][2] +- +- def __fetch_previous_values(self, metric, instance): +- if instance: +- return dict(map(lambda x: (x[0].inst, x[2]), self.group[metric].netPrevValues)) +- else: +- return self.group[metric].netPrevValues[0][2] +- +- def current_value(self, metric, instance): +- if not metric in self.group: +- return None +- if instance: +- if self.current_cached_values.get(metric, None) is None: +- lst = self.__fetch_current_values(metric, instance) +- self.current_cached_values[metric] = lst +- +- return self.current_cached_values[metric].get(instance, None) +- else: +- if self.current_cached_values.get(metric, None) is None: +- self.current_cached_values[metric] = self.__fetch_current_values(metric, instance) +- return self.current_cached_values.get(metric, None) +- +- def previous_value(self, metric, instance): +- if not metric in self.group: +- return None +- if instance: +- if self.previous_cached_values.get(metric, None) is None: +- lst = self.__fetch_previous_values(metric, instance) +- self.previous_cached_values[metric] = lst +- +- return self.previous_cached_values[metric].get(instance, None) +- else: +- if self.previous_cached_values.get(metric, None) is None: +- self.previous_cached_values[metric] = self.__fetch_previous_values(metric, instance) +- return self.previous_cached_values.get(metric, None) ++ 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 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 + + def current_values(self, metric_name): +- if self.group.get(metric_name, None) is None: +- return None +- if self.current_cached_values.get(metric_name, None) is None: +- self.current_cached_values[metric_name] = self.__fetch_current_values(metric_name, True) +- return self.current_cached_values.get(metric_name, None) ++ """Get all current values for a metric (returns dict).""" ++ return self._get_values_dict(metric_name, use_previous=False) + + def previous_values(self, metric_name): +- if self.group.get(metric_name, None) is None: +- return None +- if self.previous_cached_values.get(metric_name, None) is None: +- self.previous_cached_values[metric_name] = self.__fetch_previous_values(metric_name, True) +- return self.previous_cached_values.get(metric_name, None) +- ++ """Get all previous values for a metric (returns dict).""" ++ return self._get_values_dict(metric_name, use_previous=True) + + class ProcessFilter: ++ """ ++ Optimized filtering of processes based on user-provided options. ++ Precomputes filter predicates at instantiation to minimize overhead per process, ++ and leverages generator expressions with all() for efficiency. ++ """ ++ + def __init__(self, options): + self.options = options ++ self._predicates = [] ++ flag = getattr(options, "universal_flag", None) ++ # Username filter ++ if flag == "username" and getattr(options, "filtered_process_user", None) is not None: ++ required_user = options.filtered_process_user.strip() ++ self._predicates.append(lambda proc: proc.user_name().strip() == required_user) ++ # PID filter ++ if flag == "pid" and getattr(options, "pid_list", None) is not None: ++ try: ++ pids = set(int(pid) for pid in options.pid_list) ++ self._predicates.append(lambda proc: int(proc.pid()) in pids) ++ except Exception: ++ self._predicates.append(lambda proc: False) ++ # PPID filter ++ if flag == "ppid" and getattr(options, "ppid_list", None) is not None: ++ try: ++ ppids = set(int(ppid) for ppid in options.ppid_list) ++ self._predicates.append(lambda proc: int(proc.ppid()) in ppids) ++ except Exception: ++ self._predicates.append(lambda proc: False) ++ # Command name filter ++ if flag == "command" and getattr(options, "command_list", None) is not None: ++ commands = set(cmd.strip() for cmd in options.command_list if cmd is not None) ++ self._predicates.append(lambda proc: (proc.process_name() or "").strip() in commands) ++ # If no filter flag: no filter ++ if not getattr(options, "filter_flag", False) or not self._predicates: ++ self._predicates.append(lambda proc: True) + +- def filter_processes(self, processes): +- return filter(lambda p: self.__predicate(p), processes) +- +- def __predicate(self, process): +- if self.options.filter_flag: +- return bool(self.__matches_process_username(process) +- and self.__matches_process_pid(process) +- and self.__matches_process_name(process) +- and self.__matches_process_ppid(process)) +- else: +- return True + +- def __matches_process_username(self, process): +- if self.options.username_filter_flag is True and self.options.filtered_process_user is not None: +- return process.user_name().strip() == self.options.filtered_process_user.strip() +- else: +- return True +- +- def __matches_process_pid(self, process): +- if self.options.pid_filter_flag: +- if self.options.pid_list is not None: +- pid = int(process.pid()) +- return bool(pid in self.options.pid_list) +- return True +- +- def __matches_process_ppid(self, process): +- if self.options.ppid_filter_flag: +- if self.options.ppid_list is not None: +- ppid = int(process.ppid()) +- return bool(ppid in self.options.ppid_list) +- return True +- +- def __matches_process_name(self, process): +- name = process.process_name() +- if self.options.command_filter_flag is True and self.options.command_list is not None and name is not None: +- return name.strip() in self.options.command_list +- else: +- return True ++ def filter_processes(self, processes): ++ """Yield processes matching all active predicates.""" ++ return (proc for proc in processes if all(pred(proc) for pred in self._predicates)) + + + class ProcessStatusUtil: +@@ -169,106 +150,89 @@ class ProcessStatusUtil: + self.__delta_time = delta_time + self.__metric_repository = metrics_repository + ++ def __get_value(self, metric, instance=None): ++ return self.__metric_repository.current_value(metric, instance) ++ def __get_previous_value(self, metric, instance=None): ++ return self.__metric_repository.previous_value(metric, instance) ++ + def pid(self): +- data = str(self.__metric_repository.current_value('proc.psinfo.pid', self.instance)) +- if len(str(data)) < 8: +- whitespace = 8 - len(str(data)) +- res = data.ljust(whitespace + len(str(data)), ' ') +- return res +- else: +- return data ++ data = str(self.__get_value('proc.psinfo.pid', self.instance)) ++ return data.ljust(8) if len(data) < 8 else data + + def ppid(self): +- data = str(self.__metric_repository.current_value('proc.psinfo.ppid', self.instance)) +- if len(str(data)) < 8: +- whitespace = 8 - len(str(data)) +- res = data.ljust(whitespace + len(str(data)), ' ') +- return res +- else: +- return data ++ data = str(self.__get_value('proc.psinfo.ppid', self.instance)) ++ return data.ljust(8) if len(data) < 8 else data + + def user_name(self): +- data = self.__metric_repository.current_value('proc.id.uid_nm', self.instance)[:10] +- if len(data) < 10: +- whitespace = 10 - len(data) +- res = data.ljust(whitespace + len(data), ' ') +- return res +- else: ++ data = self.__get_value('proc.id.uid_nm', self.instance) ++ if data is None: + return data ++ return data.ljust(10) if len(data) < 10 else data[:10] + + def process_name(self): +- try: +- data = self.__metric_repository.current_value('proc.psinfo.cmd', self.instance)[:20] +- if len(data) < 20: +- whitespace = 20 - len(data) +- res = data.ljust(whitespace + len(data), ' ') +- return res +- else: +- return data +- except TypeError: +- data = '-' ++ data = self.__get_value('proc.psinfo.cmd', self.instance) ++ if data is None: ++ return '-' ++ return data.ljust(20) if len(data) < 20 else data[:20] ++ ++ def process_name_with_args(self, flag=False): ++ data = self.__get_value('proc.psinfo.psargs', self.instance) ++ if data is None: + return data +- +- def process_name_with_args(self,flag = False): +- if flag is True: +- data = self.__metric_repository.current_value('proc.psinfo.psargs', self.instance) +- else: +- data = self.__metric_repository.current_value('proc.psinfo.psargs', self.instance)[:30] +- if len(data) < 30: +- whitespace = 30 - len(data) +- res = data.ljust(whitespace + len(data), ' ') +- return res ++ max_length = 30 ++ if not flag: ++ return data[:max_length].ljust(max_length) + else: + return data + + def process_name_with_args_last(self): + return self.process_name_with_args(True) + def vsize(self): +- return self.__metric_repository.current_value('proc.psinfo.vsize', self.instance) ++ return self.__get_value('proc.psinfo.vsize', self.instance) + + def rss(self): +- return self.__metric_repository.current_value('proc.psinfo.rss', self.instance) ++ return self.__get_value('proc.psinfo.rss', self.instance) + + def mem(self): +- total_mem = self.__metric_repository.current_value('mem.physmem', None) +- rss = self.__metric_repository.current_value('proc.psinfo.rss', self.instance) ++ total_mem = self.__get_value('mem.physmem', None) ++ if isinstance(total_mem, dict): ++ total_mem = next(iter(total_mem.values()), None) ++ rss = self.__get_value('proc.psinfo.rss', self.instance) + if total_mem is not None and rss is not None: + return float("%.2f" % (100 * float(rss) / total_mem)) + else: + return None + + def s_name(self): +- return self.__metric_repository.current_value('proc.psinfo.sname', self.instance) ++ return self.__get_value('proc.psinfo.sname', self.instance) + + def cpu_number(self): +- return self.__metric_repository.current_value('proc.psinfo.processor', self.instance) ++ return self.__get_value('proc.psinfo.processor', self.instance) + + def system_percent(self): +- c_systemtime = self.__metric_repository.current_value('proc.psinfo.stime', self.instance) +- p_systemtime = self.__metric_repository.previous_value('proc.psinfo.stime', self.instance) ++ c_systemtime = self.__get_value('proc.psinfo.stime', self.instance) ++ p_systemtime = self.__get_previous_value('proc.psinfo.stime', self.instance) ++ + if c_systemtime is not None and p_systemtime is not None: +- percent_of_time = 100 * float(c_systemtime - p_systemtime) / float(1000 * self.__delta_time) ++ system_time_diff = float(c_systemtime - p_systemtime) ++ delta_time_in_ms = float(1000 * self.__delta_time) ++ percent_of_time = 100 * system_time_diff / delta_time_in_ms + return float("%.2f" % percent_of_time) + else: +- return None ++ return '-' + + def wchan_s(self): +- process = self.__metric_repository.current_value('proc.psinfo.wchan_s', self.instance) +- if process is None: +- process = '-' +- return process +- elif len(process) < 30: +- whitespace = 30 - len(process) +- res = process.ljust(whitespace + len(process), ' ') +- return res +- return process[:30] ++ process = self.__get_value('proc.psinfo.wchan_s', self.instance) ++ if process is None or process == "0" or process == '': ++ return '-' + ' ' * 29 ++ return process.ljust(30) if len(process) < 30 else process[:30] + + def priority(self): +- return self.__metric_repository.current_value('proc.psinfo.priority', self.instance) ++ return self.__get_value('proc.psinfo.priority', self.instance) + + def user_percent(self): +- c_usertime = self.__metric_repository.current_value('proc.psinfo.utime', self.instance) +- p_usertime = self.__metric_repository.previous_value('proc.psinfo.utime', self.instance) ++ c_usertime = self.__get_value('proc.psinfo.utime', self.instance) ++ p_usertime = self.__get_previous_value('proc.psinfo.utime', self.instance) + if c_usertime is not None and p_usertime is not None: + percent_of_time = 100 * float(c_usertime - p_usertime) / float(1000 * self.__delta_time) + return float("%.2f" % percent_of_time) +@@ -276,8 +240,8 @@ class ProcessStatusUtil: + return None + + def guest_percent(self): +- c_guesttime = self.__metric_repository.current_value('proc.psinfo.guest_time', self.instance) +- p_guesttime = self.__metric_repository.previous_value('proc.psinfo.guest_time', self.instance) ++ c_guesttime = self.__get_value('proc.psinfo.guest_time', self.instance) ++ p_guesttime = self.__get_previous_value('proc.psinfo.guest_time', self.instance) + if c_guesttime is not None and p_guesttime is not None: + percent_of_time = 100 * float(c_guesttime - p_guesttime) / float(1000 * self.__delta_time) + return float("%.2f" % percent_of_time) +@@ -291,8 +255,8 @@ class ProcessStatusUtil: + return None + + def stime(self): +- c_systime = self.__metric_repository.current_value('proc.psinfo.stime', self.instance) +- p_systime = self.__metric_repository.previous_value('proc.psinfo.stime', self.instance) ++ c_systime = self.__get_value('proc.psinfo.stime', self.instance) ++ p_systime = self.__get_previous_value('proc.psinfo.stime', self.instance) + # sometimes the previous_value seems to be Nonetype, not sure why + if p_systime is None: # print a '?' here + return '?' +@@ -300,21 +264,30 @@ class ProcessStatusUtil: + return c_systime - p_systime + + def start(self): +- s_time = self.__metric_repository.current_value('proc.psinfo.start_time', self.instance) ++ s_time = self.__get_value('proc.psinfo.start_time', self.instance) ++ + group = self.manager['psstat'] +- kernel_boottime = group['kernel.all.boottime'].netValues[0][2] +- ts = group.contextCache.pmLocaltime(int(kernel_boottime + (s_time / 1000))) +- if group.timestamp.tv_sec - (kernel_boottime + s_time / 1000) >= 24*60*60: +- # started one day or more ago, use MmmDD HH:MM ++ try: ++ kernel_boottime = group['kernel.all.boottime'].netValues[0][2] ++ except (KeyError, IndexError, TypeError, AttributeError): ++ return '?' ++ if s_time is None or kernel_boottime is None: ++ return '?' ++ ts_val = kernel_boottime + (s_time / 1000.0) ++ try: ++ ts = group.contextCache.pmLocaltime(int(ts_val)) ++ except Exception: ++ return '?' ++ if group.timestamp.tv_sec - ts_val >= 24*60*60: + return time.strftime("%b%d %H:%M", ts.struct_time()) + else: +- # started less than one day ago, use HH:MM:SS + return time.strftime("%H:%M:%S", ts.struct_time()) + + + def total_time(self): +- c_usertime = self.__metric_repository.current_value('proc.psinfo.stime', self.instance) +- p_guesttime = self.__metric_repository.previous_value('proc.psinfo.utime', self.instance) ++ c_usertime = self.__get_value('proc.psinfo.stime', self.instance) ++ p_guesttime = self.__get_previous_value('proc.psinfo.utime', self.instance) ++ + timefmt = "%H:%M:%S" + if c_usertime and p_guesttime is not None: + total_time = (c_usertime / 1000) + (p_guesttime / 1000) +@@ -323,29 +296,30 @@ class ProcessStatusUtil: + return time.strftime(timefmt, time.gmtime(total_time)) + + def tty_name(self): +- return self.__metric_repository.current_value('proc.psinfo.ttyname', self.instance) ++ return self.__get_value('proc.psinfo.ttyname', self.instance) + + def user_id(self): +- return self.__metric_repository.current_value('proc.id.uid', self.instance) ++ return self.__get_value('proc.id.uid', self.instance) + + def start_time(self): +- return self.__metric_repository.current_value('proc.psinfo.start_time', self.instance) ++ return self.__get_value('proc.psinfo.start_time', self.instance) + + def func_state(self): +- s_name = self.__metric_repository.current_value('proc.psinfo.sname', self.instance) ++ s_name = self.__get_value('proc.psinfo.sname', self.instance) ++ + if s_name == 'R': +- return 'N/A' ++ return '-' + elif s_name is None: +- return '?' ++ return '-' + else: + return self.wchan_s() + + def policy(self): +- policy_int = self.__metric_repository.current_value('proc.psinfo.policy', self.instance) +- if policy_int is not None and policy_int <= len(SCHED_POLICY): +- # return policy_int ++ policy_int = self.__get_value('proc.psinfo.policy', self.instance) ++ if isinstance(policy_int, int) and 0 <= policy_int < len(SCHED_POLICY): ++ + return SCHED_POLICY[policy_int] +- return None ++ return '?' + + + PIDINFO_PAIR = {"%cpu": ('%CPU', ProcessStatusUtil.system_percent), +@@ -353,8 +327,8 @@ PIDINFO_PAIR = {"%cpu": ('%CPU', ProcessStatusUtil.system_percent), + "start": ("START\t", ProcessStatusUtil.start), + "time": ("TIME\t", ProcessStatusUtil.total_time), + "cls": ("CLS", ProcessStatusUtil.policy), +- "cmd": ("Command\t\t\t", ProcessStatusUtil.process_name), +- "args": ("Command\t\t\t", ProcessStatusUtil.process_name_with_args), ++ "cmd": ("Command\t\t", ProcessStatusUtil.process_name), ++ "args_last": ("Command", ProcessStatusUtil.process_name_with_args_last), + "args_last": ("Command\t\t\t", ProcessStatusUtil.process_name_with_args_last), + "pid": ("PID\t", ProcessStatusUtil.pid), + "ppid": ("PPID\t", ProcessStatusUtil.ppid), +@@ -362,7 +336,7 @@ PIDINFO_PAIR = {"%cpu": ('%CPU', ProcessStatusUtil.system_percent), + "state": ("S", ProcessStatusUtil.s_name), + "rss": ("RSS", ProcessStatusUtil.rss), + "rtprio": ("RTPRIO", ProcessStatusUtil.priority), +- "tty": ("TTY\t", ProcessStatusUtil.tty_name), ++ "tty": ("TTY", ProcessStatusUtil.tty_name), + "pname": ("Pname\t\t", ProcessStatusUtil.process_name), + "vsize": ("VSZ", ProcessStatusUtil.vsize), + "uname": ("USER\t", ProcessStatusUtil.user_name), +@@ -376,8 +350,8 @@ class ProcessStatus: + self.__metric_repository = metric_repository + + def get_processes(self, delta_time): +- return map(lambda pid: +- (ProcessStatusUtil(pid, self.__manager, delta_time, self.__metric_repository)), self.__pids()) ++ # Use generator expression for lazy evaluation and reduced memory usage ++ return (ProcessStatusUtil(pid, self.__manager, delta_time, self.__metric_repository) for pid in self.__pids()) + + def __pids(self): + pid_dict = self.__metric_repository.current_values('proc.psinfo.pid') +@@ -397,66 +371,113 @@ class DynamicProcessReporter: + self.processStatOptions.colum_list.index(key) == len(self.processStatOptions.colum_list) - 1 + def print_report(self, timestamp, header_indentation, value_indentation): + +- # when the print count is exhausted exit the program gracefully +- # we can't use break here because it's being called by the run manager +- if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: +- if self.processStatOptions.print_count == 0: +- sys.exit(0) ++ # Exit logic for non-archive context ++ if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE and self.processStatOptions.print_count == 0: ++ sys.exit(0) ++ ++ # 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: +- self.processStatOptions.print_count -= 1 ++ 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 ++ ++ # Always compute process list ONCE ++ processes = self.process_filter.filter_processes( ++ self.process_report.get_processes(self.delta_time) ++ ) ++ ++ 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}" ++ # ) + +- if self.processStatOptions.filterstate is not None: +- self.printer("Timestamp" + header_indentation + +- "USER\t\tPID\t\tPPID\t\tPRI\t%CPU\t%MEM\tVSZ\tRSS\tS\tSTARTED\t\tTIME\t\tWCHAN\t\t\t\tCommand") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) + for process in processes: +- total_percent = process.total_percent() +- current_process_pid = process.pid() +- current_process_sname = process.s_name() +- if process.wchan_s() is not None: +- wchan = process.wchan_s() +- else: +- wchan = '-' +- key = (current_process_sname, current_process_pid) +- if key in process_state_info: +- process_state_info[key] = process_state_info[key] + self.delta_time +- else: +- process_state_info[key] = self.delta_time +- process_name = process.process_name_with_args() +- if 7 < len(wchan) < 15: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\t%s" % +- (timestamp, value_indentation, process.user_name(), process.pid(), process.ppid(), +- process.priority(), total_percent, process.system_percent(), process.vsize(), +- process.rss(), current_process_sname, process.start(), process.total_time(), wchan, +- process_name)) +- elif len(wchan) >= 15: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % +- (timestamp, value_indentation, process.user_name(), process.pid(), process.ppid(), +- process.priority(), total_percent, process.system_percent(), process.vsize(), +- process.rss(), current_process_sname, process.start(), process.total_time(), wchan, +- process_name)) +- else: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t\t\t%s" % +- (timestamp, value_indentation, process.user_name(), process.pid(), process.ppid(), +- process.priority(), total_percent, process.system_percent(), process.vsize(), +- process.rss(), current_process_sname, process.start(), process.total_time(), wchan, +- process_name)) ++ # 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: +- header = "Timestamp" + '\t' ++ ++ header = "Timestamp\t" + for key in self.processStatOptions.colum_list: + if key in PIDINFO_PAIR: +- header += PIDINFO_PAIR[key][0] + '\t\t' +- print(header) +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ header += PIDINFO_PAIR[key][0] + "\t" + for process in processes: +- data_to_print = timestamp + '\t' ++ row = [timestamp] + for key in self.processStatOptions.colum_list: + if self._is_last_and_args(key): +- data_to_print += str(PIDINFO_PAIR["args_last"][1](process)) + '\t\t' ++ row.append(str(PIDINFO_PAIR["args_last"][1](process))) + elif key in PIDINFO_PAIR: +- data_to_print += str(PIDINFO_PAIR[key][1](process)) + '\t\t' +- print(data_to_print) ++ row.append(str(PIDINFO_PAIR[key][1](process))) ++ # 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 -------- # ++ # 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 ++ ) ++ ++ # --------- Print output --------- # ++ self.printer(header) ++ self.printer('\n'.join(output_list)) + + class ProcessStatusReporter: + def __init__(self, process_report, process_filter, delta_time, printer, processStatOptions): +@@ -467,87 +488,105 @@ class ProcessStatusReporter: + self.processStatOptions = processStatOptions + + def print_report(self, timestamp, header_indentation, value_indentation): +- +- # when the print count is exhausted exit the program gracefully +- # we can't use break here because it's being called by the run manager +- if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: +- if self.processStatOptions.print_count == 0: +- sys.exit(0) +- else: +- self.processStatOptions.print_count -= 1 +- +- if self.processStatOptions.show_all_process: +- self.printer("Timestamp" + header_indentation + "PID\t\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ if self.processStatOptions.debug_mode: ++ print("option selected: %s" % self.processStatOptions.universal_flag) ++ print("filer option status: %s" % self.processStatOptions.filterstate) ++ FORMAT_MAP = { ++ "empty_arg" : "Timestamp" + header_indentation + "PID\t\tTIME\t\tCMD", ++ "all" : "Timestamp" + header_indentation + "PID\t\t\tTTY\tTIME\t\tCMD", ++ "user": "Timestamp" + header_indentation + "USERNAME\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + ++ "TTY\tSTAT\tTIME\t\tSTART\t\tCOMMAND", ++ "pid": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD", ++ "ppid": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD", ++ "username": "Timestamp" + header_indentation + "USERNAME\t\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + ++ "TTY\tSTAT\t\tTIME\t\tSTART\t\tCOMMAND", ++ "command": "Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD" ++ } ++ selected_flag = self.processStatOptions.universal_flag ++ header = FORMAT_MAP.get(selected_flag, FORMAT_MAP["all"]) ++ # Bulk buffer for all rows, print all at once for efficiency ++ output_rows = [] ++ cpu_idx , mem_idx = 0, 0 ++ processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ def safe_str(val): ++ return '' if val is None else str(val) ++ if selected_flag == "all": + for process in processes: +- command = process.process_name_with_args(True) +- ttyname = process.tty_name() +- self.printer("%s%s%s\t\t%s\t%s\t%s" % (timestamp, value_indentation, process.pid(), ttyname, +- process.total_time(), command)) +- elif self.processStatOptions.empty_arg_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.tty_name()), safe_str(process.total_time()), ++ safe_str(process.process_name_with_args(True)))) ++ elif selected_flag == "empty_arg": + for process in processes: +- pid = process.pid() +- command = process.process_name() +- self.printer("%s%s%s\t%s\t%s" % (timestamp, value_indentation, pid, +- process.total_time(), command)) +- elif self.processStatOptions.pid_filter_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.total_time()), safe_str(process.process_name()))) ++ elif selected_flag == "pid": + for process in processes: +- pid = process.pid() +- command = process.process_name() +- ttyname = process.tty_name() +- self.printer("%s%s%s\t%s\t%s\t%s\t%s" % (timestamp, value_indentation, pid, process.ppid(), +- ttyname, process.total_time(), command)) +- elif self.processStatOptions.ppid_filter_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), safe_str(process.ppid()), ++ safe_str(process.tty_name()), safe_str(process.total_time()), safe_str(process.process_name()))) ++ elif selected_flag == "ppid": + for process in processes: +- ppid = process.ppid() +- command = process.process_name() +- ttyname = process.tty_name() +- self.printer("%s%s%s\t%s\t%s\t%s\t%s" % (timestamp, value_indentation, process.pid(), ppid, ttyname, +- process.total_time(), command)) +- elif self.processStatOptions.username_filter_flag: +- self.printer("Timestamp" + header_indentation + "USERNAME\t\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + +- "TTY\tSTAT\t\tTIME\t\tSTART\t\tCOMMAND") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.ppid()), safe_str(process.tty_name()), ++ safe_str(process.total_time()), safe_str(process.process_name()))) ++ elif selected_flag == "username": + for process in processes: +- self.printer("%s%s%s\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( +- timestamp, value_indentation, process.user_name(), process.pid(), +- process.system_percent(), process.total_percent(), process.vsize(), process.rss(), +- process.tty_name(), process.ppid(), process.total_time(), process.start(), +- process.process_name())) +- +- elif self.processStatOptions.user_oriented_format: +- self.printer("Timestamp" + header_indentation + "USERNAME\tPID\t\t%CPU\t%MEM\tVSZ\tRSS\t" + +- "TTY\tSTAT\tTIME\t\tSTART\t\tCOMMAND") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ 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.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 ++ elif selected_flag == "user": + for process in processes: +- self.printer("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % ( +- timestamp, value_indentation, process.user_name(), process.pid(), +- process.system_percent(), process.total_percent(), process.vsize(), process.rss(), +- process.tty_name(), process.s_name(), process.total_time(), process.start(), +- process.process_name())) +- +- elif self.processStatOptions.command_filter_flag: +- self.printer("Timestamp" + header_indentation + "PID\t\tPPID\t\tTTY\tTIME\t\tCMD") +- processes = self.process_filter.filter_processes(self.process_report.get_processes(self.delta_time)) ++ 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.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 ++ elif selected_flag == "command": + for process in processes: +- ppid = process.ppid() +- command = process.process_name() +- ttyname = process.tty_name() +- self.printer("%s%s%s\t%s\t%s\t%s\t%s" % (timestamp, value_indentation, process.pid(), ppid, ttyname, +- process.total_time(), command)) +- ++ output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % ( ++ safe_str(timestamp), safe_str(value_indentation), safe_str(process.pid()), ++ safe_str(process.ppid()), safe_str(process.tty_name()), ++ safe_str(process.total_time()), safe_str(process.process_name()))) ++ else: # default fallback, print nothing extra ++ pass ++ if self.processStatOptions.sorting_flag: ++ if cpu_idx == 0 and mem_idx == 0: ++ raise ValueError("Sorting indices not set for selected flag, " ++ "please remove sorting flag or choose another flag for output") ++ if self.processStatOptions.sorting_order == '%cpu': ++ output_rows.sort( ++ key=lambda x: float(x.split()[cpu_idx]) if x.split()[cpu_idx].replace('.', '', 1).isdigit() ++ else float('-inf'), ++ reverse=True) ++ elif self.processStatOptions.sorting_order == '%mem': ++ output_rows.sort( ++ key=lambda x: float(x.split()[mem_idx]) if x.split()[mem_idx].replace('.', '', 1).isdigit() ++ else float('-inf'), ++ reverse=True) ++ if output_rows: ++ self.printer(header) ++ self.printer('\n'.join(output_rows)) + + class ProcessStatReport(pmcc.MetricGroupPrinter): + Machine_info_count = 0 + group = None +- def __init__(self, group=None): ++ def __init__(self, group=None, options = None): + self.group = group ++ self.processStatOptions = options + + def timeStampDelta(self): + s = self.group.timestamp.tv_sec - self.group.prevTimestamp.tv_sec +@@ -573,70 +612,88 @@ class ProcessStatReport(pmcc.MetricGroupPrinter): + return group['hinv.ncpu'].netValues[0][2] + + def __print_report(self, manager,timestamp, header_indentation, value_indentation,interval_in_seconds): ++ if self.processStatOptions.debug_mode: ++ print("Printing standard report") + metric_repository = ReportingMetricRepository(self.group) + process_report = ProcessStatus(manager, metric_repository) +- process_filter = ProcessFilter(ProcessStatOptions) ++ process_filter = ProcessFilter(self.processStatOptions) + stdout = StdoutPrinter() + printdecorator = NoneHandlingPrinterDecorator(stdout) + report = ProcessStatusReporter(process_report, process_filter, interval_in_seconds, +- printdecorator.Print, ProcessStatOptions) ++ printdecorator.Print, self.processStatOptions) + report.print_report(timestamp, header_indentation, value_indentation) + def __print_dynamic_report(self, manager,timestamp, header_indentation, value_indentation,interval_in_seconds): ++ if self.processStatOptions.debug_mode: ++ print("Printing dynamic report") + metric_repository = ReportingMetricRepository(self.group) + process_report = ProcessStatus(manager, metric_repository) +- process_filter = ProcessFilter(ProcessStatOptions) ++ process_filter = ProcessFilter(self.processStatOptions) + stdout = StdoutPrinter() + printdecorator = NoneHandlingPrinterDecorator(stdout) + report = DynamicProcessReporter(process_report, process_filter, interval_in_seconds, +- printdecorator.Print, ProcessStatOptions) ++ printdecorator.Print, self.processStatOptions) + report.print_report(timestamp, header_indentation, value_indentation) + def __get_timestamp(self): + ts = self.group.contextCache.pmLocaltime(int(self.group.timestamp)) +- timestamp = time.strftime(ProcessStatOptions.timefmt, ts.struct_time()) ++ timestamp = time.strftime(self.processStatOptions.timefmt, ts.struct_time()) + return timestamp + + def report(self, manager): + try: + if self.group['proc.psinfo.utime'].netPrevValues is None: +- # need two fetches to report rate converted counter metrics +- return ++ return False # Not ready, skip increment ++ + if not self.group['hinv.ncpu'].netValues or not self.group['kernel.uname.sysname'].netValues: +- return ++ return False + try: + if not self.Machine_info_count: + self.print_machine_info(manager) + self.Machine_info_count = 1 + except IndexError: +- return ++ if self.processStatOptions.debug_mode: ++ print("IndexError while printing machine info") ++ return False ++ if self.processStatOptions.debug_mode: ++ print("Starting report generation") ++ print("Need to print samples: %s" % self.processStatOptions.print_count) ++ if self.processStatOptions.print_count == 0: ++ if self.processStatOptions.debug_mode: ++ print("Print count exhausted, exiting") ++ sys.exit(0) ++ + timestamp = self.__get_timestamp() + interval_in_seconds = self.timeStampDelta() + header_indentation = " " if len(timestamp) < 9 else (len(timestamp) - 7) * " " + value_indentation = ((len(header_indentation) + 9) - len(timestamp)) * " " + + # Doing this for one single print instance in case there is no count specified +- if ProcessStatOptions.print_count is None: +- ProcessStatOptions.print_count = 1 ++ if self.processStatOptions.print_count is None \ ++ and self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: ++ self.processStatOptions.print_count = 1 + # ================================================================ +- if ProcessStatOptions.selective_colum_flag: ++ if self.processStatOptions.selective_colum_flag: ++ if self.processStatOptions.debug_mode: ++ print("Selective column flag is set") ++ + self.__print_dynamic_report(manager,timestamp, header_indentation, + value_indentation, interval_in_seconds) + else: + self.__print_report(manager,timestamp, header_indentation, value_indentation, interval_in_seconds) ++ if self.processStatOptions.context is not PM_CONTEXT_ARCHIVE: ++ self.processStatOptions.print_count -= 1 ++ return True # Data was printed + finally: + sys.stdout.flush() + + + class ProcessStatOptions(pmapi.pmOptions): +- show_all_process = False +- command_filter_flag = False +- ppid_filter_flag = False +- pid_filter_flag = False +- username_filter_flag = False ++ universal_flag = None + selective_colum_flag = False + filter_flag = False +- user_oriented_format = False +- empty_arg_flag = False + filterstate = None ++ debug_mode = False ++ sorting_flag = False ++ sorting_order = None + timefmt = "%H:%M:%S" + print_count = None + colum_list = [] +@@ -647,7 +704,7 @@ class ProcessStatOptions(pmapi.pmOptions): + context = None + + def __init__(self): +- pmapi.pmOptions.__init__(self, "t:c:e::p:ukVZ:z?:o:P:l:U:k") ++ pmapi.pmOptions.__init__(self, "t:c:e::p:ukVZ:z?:o:P:l:U:k:O:d") + self.pmSetOptionCallback(self.extraOptions) + self.pmSetOverrideCallback(self.override) + self.options() +@@ -662,108 +719,220 @@ class ProcessStatOptions(pmapi.pmOptions): + "Select the process by process ID") + self.pmSetLongOption("", 1, "P", "[ppid1,ppid2,...]", "Select the process by process parent ID") + self.pmSetLongOption("", 1, "U", "[User Name]", "Select the process by user name") +- self.pmSetLongOption("", 1, "o", "[col1,col2,... Or ALL]", "User -defined format " + +- "USE -o [all] or -B [col1, col2 , ...]" + +- "\n\t\t\tsupported user defined colums are command, wchan, started, Time, pid, ppid, " +- "%mem, pri, user, %cpu and S " +- "\n\t\t\tALL option shows USER,PID,PPID,PRI,%CPU,%MEM,VSZ,RSS,S,STARTED,TIME,WCHAN and " +- "Command") ++ self.pmSetLongOption( ++ "", ++ 1, ++ "o", ++ "[col1,col2,... Or ALL]", ++ ( ++ "User -defined format USE -o [all] or -B [col1, col2 , ...]\n" ++ "\t\t\tsupported user defined colums are command, wchan, started, Time, pid, ppid, " ++ "%mem, pri, user, %cpu and S\n" ++ "\t\t\tALL option shows USER,PID,PPID,PRI,%CPU,%MEM,VSZ,RSS,S,STARTED,TIME,WCHAN and\n" ++ "Command" ++ ), ++ ) ++ + self.pmSetLongOptionText("\tCOL\tHEADER \tDESCRIPTION") + self.pmSetLongOptionText("\t%cpu\t%CPU \tcpu utilization of the process") + self.pmSetLongOptionText("\t%mem\t%MEM \tphysical memory on the machine expressed as a percentage") + self.pmSetLongOptionText("\tstart\tSTART \ttime the command started") + self.pmSetLongOptionText("\ttime\tTIME \taccumulated cpu time, user + system") +- self.pmSetLongOptionText("\tcls\tCLS \tscheduling class of the process") +- self.pmSetLongOptionText("\tcmd\tCMD\tsee args. (alias args, command).") +- self.pmSetLongOptionText("\tpid\tPID \tthe process ID") +- self.pmSetLongOptionText("\tppid\tPPID\tparent process ID") +- self.pmSetLongOptionText("\tpri\tPRI \tpriority of the process") +- self.pmSetLongOptionText("\tstate\tS \tsee s") +- self.pmSetLongOptionText("\trss\tRSS \tthe non-swapped physical memory that a task has used") +- self.pmSetLongOptionText("\trtprio\tRTPRIO \trealtime priority") +- self.pmSetLongOptionText("\tpname\tPname\tProcess name") +- # self.pmSetLongOptionText("\ttime\tTIME \tcumulative CPU time") +- self.pmSetLongOptionText("\ttty\tTT \tcontrolling tty (terminal)") +- self.pmSetLongOptionText("\tuid\tUID \tsee euid") +- self.pmSetLongOptionText("\tvsize\tVSZ \tsee vsz") +- self.pmSetLongOptionText("\tuname\tUSER \tsee euser") +- self.pmSetLongOptionText("\twchan\tWCHAN \tname of the kernel function in which the process is sleeping") +- self.pmSetLongOption("", 0, 'u', "", "Display user-oriented format") ++ self.pmSetLongOptionText( ++ "\tcls\tCLS \tscheduling class of the process" ++ ) ++ self.pmSetLongOptionText( ++ "\tcmd\tCMD\tsee args. (alias args, command)." ++ ) ++ self.pmSetLongOptionText( ++ "\tpid\tPID \tthe process ID" ++ ) ++ self.pmSetLongOptionText( ++ "\tppid\tPPID\tparent process ID" ++ ) ++ self.pmSetLongOptionText( ++ "\tpri\tPRI \tpriority of the process" ++ ) ++ self.pmSetLongOptionText( ++ "\tstate\tS \tsee s" ++ ) ++ self.pmSetLongOptionText( ++ "\trss\tRSS \tthe non-swapped physical memory that a task has used" ++ ) ++ self.pmSetLongOptionText( ++ "\trtprio\tRTPRIO \trealtime priority" ++ ) ++ self.pmSetLongOptionText( ++ "\tpname\tPname\tProcess name" ++ ) ++ self.pmSetLongOptionText( ++ "\ttty\tTT \tcontrolling tty (terminal)" ++ ) ++ self.pmSetLongOptionText( ++ "\tuid\tUID \tsee euid" ++ ) ++ self.pmSetLongOptionText( ++ "\tvsize\tVSZ \tsee vsz" ++ ) ++ self.pmSetLongOptionText( ++ "\tuname\tUSER \tsee euser" ++ ) ++ self.pmSetLongOptionText( ++ "\twchan\tWCHAN \tname of the kernel function in which the process is sleeping" ++ ) ++ self.pmSetLongOption("", 0, 'u', "", ++ "Display user-oriented format" ++ ) ++ self.pmSetLongOption( ++ "sort", 1, "O", "%cpu,%mem", ++ "sort the process list by %cpu or %mem values " ++ ) ++ self.pmSetLongOption("", 0, "d", "", "enable debug mode") + self.pmSetLongOptionVersion() + self.pmSetLongOptionTimeZone() + self.pmSetLongOptionHostZone() + self.pmSetLongOptionHelp() + + def override(self, opts): +- ProcessStatOptions.print_count = self.pmGetOptionSamples() ++ self.print_count = self.pmGetOptionSamples() + # """Override standard Pcp-ps option to show all process """ +- return bool(opts in ['p', 'c', 'o', 'P', 'U']) ++ return bool(opts in ['p', 'c', 'o', 'P', 'U', 'O', 'd']) + + def extraOptions(self, opts, optarg, index): +- if opts == 'e': +- ProcessStatOptions.show_all_process = True +- elif opts == 'c': +- ProcessStatOptions.command_filter_flag = True +- ProcessStatOptions.filter_flag = True ++ ++ def handle_e(): ++ if self.debug_mode: ++ print("e option selected") ++ self.universal_flag = "all" ++ ++ def handle_c(): ++ if self.debug_mode: ++ print("command option selected") ++ self.universal_flag = "command" ++ self.command_filter_flag = True ++ self.filter_flag = True ++ + try: + if optarg is not None: +- ProcessStatOptions.command_list += optarg.replace(',', ' ').split(' ') ++ self.command_list += optarg.replace(',', ' ').split(' ') ++ if self.debug_mode: ++ print("Command List: %s" % self.command_list) + except ValueError: + print("Invalid command Id List: use comma separated pids without whitespaces") + sys.exit(1) +- elif opts == 'p': +- ProcessStatOptions.filter_flag = True +- ProcessStatOptions.pid_filter_flag = True ++ def handle_d(): ++ print("Debug mode selected") ++ self.debug_mode = True ++ ++ def handle_p(): ++ if self.debug_mode: ++ print("pid option selected") ++ self.universal_flag = "pid" ++ self.filter_flag = True ++ self.pid_filter_flag = True ++ + try: + if optarg is not None: + dummy_list = optarg.replace(',', ' ').split(' ') +- ProcessStatOptions.pid_list += [int(x) for x in dummy_list] ++ self.pid_list += [int(x) for x in dummy_list] + except ValueError: + print("Invalid pid Id List: use comma separated pids without whitespaces") + sys.exit(1) +- elif opts == 'P': +- ProcessStatOptions.filter_flag = True +- ProcessStatOptions.ppid_filter_flag = True ++ ++ def handle_P(): ++ if self.debug_mode: ++ print("ppid option selected") ++ self.universal_flag = "ppid" ++ self.filter_flag = True ++ self.ppid_filter_flag = True ++ + try: + if optarg is not None: + dummy_list = optarg.replace(',', ' ').split(' ') +- ProcessStatOptions.ppid_list += [int(x) for x in dummy_list] ++ self.ppid_list += [int(x) for x in dummy_list] + except ValueError: + print("Invalid ppid Id List: use comma separated pids without whitespaces") + sys.exit(1) +- elif opts == 'u': +- ProcessStatOptions.user_oriented_format = True +- elif opts == 'o': +- ProcessStatOptions.selective_colum_flag = True ++ ++ def handle_u(): ++ if self.debug_mode: ++ print("User-oriented format option selected") ++ self.universal_flag = "user" ++ self.user_oriented_format = True ++ ++ def handle_o(): ++ if self.debug_mode: ++ print("User-defined format option selected") ++ self.selective_colum_flag = True ++ + try: + if optarg.upper() == "ALL": +- ProcessStatOptions.filterstate = optarg.upper() ++ self.filterstate = optarg.upper() + else: + dummy_list = optarg.replace(',', ' ').split(' ') ++ if self.debug_mode: ++ print("Custom Column List: %s" % dummy_list) + for key in dummy_list: + if key.lower() in PIDINFO_PAIR: +- ProcessStatOptions.colum_list.append(key.lower()) ++ self.colum_list.append(key.lower()) + else: + raise ValueError + except ValueError: +- print("Invalid ppid Id List: Either column name is not correct " +- "or use comma separated column names without whitespaces") ++ print("Invalid Column List: incorrect name or improper comma-separated format") + sys.exit(1) +- elif opts == 'U': +- ProcessStatOptions.username_filter_flag = True +- ProcessStatOptions.filter_flag = True +- ProcessStatOptions.filtered_process_user = optarg +- elif opts is None: +- ProcessStatOptions.show_all_process = True +- +- @staticmethod +- def checkOptions(): +- if ProcessStatOptions.selective_colum_flag or \ +- ProcessStatOptions.filter_flag or \ +- ProcessStatOptions.user_oriented_format: ++ ++ def handle_U(): ++ if self.debug_mode: ++ print("username option selected") ++ self.universal_flag = "username" ++ self.username_filter_flag = True ++ self.filter_flag = True ++ self.filtered_process_user = optarg ++ ++ def handle_none(): ++ if self.debug_mode: ++ print("No option selected, defaulting to show all process") ++ self.universal_flag = "all" ++ # self.show_all_process = True ++ ++ def handle_O(): ++ if self.debug_mode: ++ print("Sorting option selected") ++ self.sorting_flag = True ++ if optarg.lower() not in ['%cpu', '%mem']: ++ print("Invalid sorting option, defaulting to %cpu") ++ self.sorting_order = '%cpu' # default to %cpu ++ return ++ self.sorting_order = optarg.lower() ++ ++ # Dispatch map simulating switch-case ++ dispatch = { ++ 'e': handle_e, ++ 'c': handle_c, ++ 'p': handle_p, ++ 'P': handle_P, ++ 'u': handle_u, ++ 'o': handle_o, ++ 'U': handle_U, ++ 'O': handle_O, ++ 'd': handle_d, ++ None: handle_none, ++ } ++ # Execute handler or fallback to error ++ handler = dispatch.get(opts) ++ if handler: ++ handler() ++ else: ++ print("Unknown option: %s" % opts) ++ sys.exit(1) ++ def checkOptions(self): ++ if self.universal_flag is not None : ++ + return True + else: +- ProcessStatOptions.empty_arg_flag = True ++ if self.debug_mode: ++ print("No filtering option selected, defaulting to empty argument mode") ++ self.universal_flag = "empty_arg" + return True + + +@@ -779,7 +948,7 @@ if __name__ == "__main__": + sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing) + sys.exit(1) + manager['psstat'] = PSSTAT_METRICS +- manager.printer = ProcessStatReport(manager['psstat']) ++ manager.printer = ProcessStatReport(manager['psstat'],opts) + sts = manager.run() + sys.exit(sts) + except pmapi.pmErr as pmerror: +@@ -787,6 +956,9 @@ if __name__ == "__main__": + except pmapi.pmUsageErr as usage: + usage.message() + sys.exit(1) ++ except ValueError as e: ++ sys.stderr.write("%s\n" % str(e)) ++ sys.exit(1) + except IOError: + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + except KeyboardInterrupt: +-- +2.43.7 + diff --git a/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch b/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch new file mode 100644 index 0000000..2ee15ec --- /dev/null +++ b/1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch @@ -0,0 +1,604 @@ +From 09f39fb28a3171300afa5ed15e7f75ac0fbfdabc Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +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 + +--- + 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/ ++and ++.IR $PCP_REMOTE_ARCHIVE_DIR/ ++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 ... ++ # | ++ # ++ 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" &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 + diff --git a/1016-pcp-system-tools-restore-backward-compatibility-with.patch b/1016-pcp-system-tools-restore-backward-compatibility-with.patch new file mode 100644 index 0000000..1f14993 --- /dev/null +++ b/1016-pcp-system-tools-restore-backward-compatibility-with.patch @@ -0,0 +1,332 @@ +From a6bb22bf407522ef95557de0cebde717de347bde Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +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 + +--- + 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 + diff --git a/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch b/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch new file mode 100644 index 0000000..eb311f2 --- /dev/null +++ b/1017-orabug39068870-adds-interval-option-in-nfsiostat.patch @@ -0,0 +1,364 @@ +From c79976edb317272dcc8ae9352778d5d335763f10 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Wed, 25 Feb 2026 12:45:43 +0000 +Subject: [PATCH] Add interval and count support for nfsiostat tool + +Signed-off-by: Sourav Sharma + +Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/02d9b5a7b3c2ea16de2665c1f8cd6b81843fee15 + +Orabug: 39068870 + +Signed-off-by: Sourav Sharma + +--- + 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 + diff --git a/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch b/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch new file mode 100644 index 0000000..1e11c0b --- /dev/null +++ b/1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch @@ -0,0 +1,1287 @@ +From 8d05c8d7756fbe50ee87aa433626ddb88e8a0a70 Mon Sep 17 00:00:00 2001 +From: Sourav Sharma +Date: Thu, 26 Mar 2026 13:16:48 +0000 +Subject: [PATCH] Introduces numa_maps metrics inside linux_proc pmda + +Implemented option -p in pcp numastat tool same as numactl numastat tool + + - Add new per-process NUMA maps metrics derived from /proc//numa_maps: proc.numa_maps.huge, proc.numa_maps.heap, proc.numa_maps.stack, + proc.numa_maps.private (per-node usage in MB, exported as nodeN: pairs). + - Extend pcp numastat/pcp-numastat with -p/--process to display per-process NUMA memory usage in a numastat -p-style layout. + - Convert N= counts to MB using system page size (and Hugepagesize where applicable, with fallback). + + How to Test + + - pcp numastat -p (detail view) and pcp numastat -p (summary view) + +Signed-off-by: Sourav Sharma + +Cherry-pick-commit: +https://github.com/sourav-sharma796/pcp/commit/0ea70c7fda4021cb9e35a0d4e26a30dd97409ddb +https://github.com/sourav-sharma796/pcp/commit/95fe35c176c9394ddcef0af5f4d50606308c77d1 +https://github.com/sourav-sharma796/pcp/commit/757c2a1e1c4b51ee3e304f6c3f3038e6030432f3 + +Orabug: 39096683 + +Signed-off-by: Sourav Sharma + +--- + src/pcp/numastat/pcp-numastat.1 | 23 +- + src/pcp/numastat/pcp-numastat.py | 573 ++++++++++++++++++++++++++++++- + src/pmdas/linux_proc/clusters.h | 3 +- + src/pmdas/linux_proc/help | 13 + + src/pmdas/linux_proc/indom.h | 1 + + src/pmdas/linux_proc/pmda.c | 74 ++++ + src/pmdas/linux_proc/pmdaproc.1 | 4 + + src/pmdas/linux_proc/proc_pid.c | 269 +++++++++++++++ + src/pmdas/linux_proc/proc_pid.h | 17 + + src/pmdas/linux_proc/root_proc | 8 + + 10 files changed, 972 insertions(+), 13 deletions(-) + +diff --git a/src/pcp/numastat/pcp-numastat.1 b/src/pcp/numastat/pcp-numastat.1 +index d7f0304..efa9560 100644 +--- a/src/pcp/numastat/pcp-numastat.1 ++++ b/src/pcp/numastat/pcp-numastat.1 +@@ -18,7 +18,7 @@ + \f3pcp-numastat\f1 \- report on NUMA memory allocation + .SH SYNOPSIS + \f3pcp\f1 [\f2pcp\ options\f1] \f3numastat\f1 +-[\fB\-mnV?\fR] [\fB\-w\fR \fIwidth\fR] ++[\fB-V\fR] [\fB-w\fR \fIwidth\fR] [\fB-m\fR][-n\fR] [\fB\-p\fR [\fIprocess\fR ...]] [\fB-?\fR] + .SH DESCRIPTION + .B pcp-numastat + displays NUMA allocation statistics from the kernel memory +@@ -68,6 +68,24 @@ Display meminfo-like system-wide memory usage. + \fB\-n\fR, \fB\-\-numastat\fR + Display the numastat statistics info. + .TP ++\fB\-p\fR, \fB\-\-process\fR ++Display per-process NUMA memory usage from \fBproc.numa_maps\fR metrics. ++Any trailing \fIprocess\fR operands are treated as process IDs (when the ++operand is all digits) or regular expressions for command name matching. ++Matching for command names is performed against the full command line ++(as reported by \fBproc.psinfo.psargs\fR). ++When a single process ID operand is provided and it matches exactly one ++process, a detailed table of Hugepage, Heap, Stack and Private mappings is ++shown (as in \fBnumastat \-p\fR). When multiple process ID operands are ++provided, a per-process summary table is shown (also like ++\fBnumastat \-p\fR) and may be split into multiple node column blocks ++based on the available display width. ++.PP ++In the per-process view, node columns are always shown for all NUMA nodes ++present on the system (as reported by PCP NUMA inventory metrics), even if ++individual \fBproc.numa_maps\fR metric instances omit some nodes. Missing ++per-process metrics are reported as zero. ++.TP + \fB\-V\fR, \fB\-\-version\fR + Display the current version of the command. + .TP +@@ -81,7 +99,8 @@ Display usage message and exit. + .B pcp-numastat + is inspired by the + .BR numastat (1) +-command and produces exactly the same output. ++command and produces the same output for the system-wide views. The ++per-process view (\fB\-p\fR) follows the \fBnumastat \-p\fR layout. + .SH PCP ENVIRONMENT + Environment variables with the prefix \fBPCP_\fP are used to parameterize + the file and directory names used by PCP. +diff --git a/src/pcp/numastat/pcp-numastat.py b/src/pcp/numastat/pcp-numastat.py +index 2a044c9..decdf12 100755 +--- a/src/pcp/numastat/pcp-numastat.py ++++ b/src/pcp/numastat/pcp-numastat.py +@@ -17,6 +17,7 @@ + """ Display NUMA memory allocation statistucs """ + + import os ++import re + import signal + import sys + import time +@@ -86,6 +87,23 @@ SYS_METRICS = [ + + ALL_METRICS = NUMA_METRICS + MEM_METRICS + ++PROCESS_METRICS = [ ++ "proc.psinfo.pid", ++ "proc.psinfo.cmd", ++ "proc.psinfo.psargs", ++ "proc.numa_maps.hugepage", ++ "proc.numa_maps.heap", ++ "proc.numa_maps.stack", ++ "proc.numa_maps.private", ++] ++ ++PROCESS_NUMA_METRICS = [ ++ ("Huge", "proc.numa_maps.hugepage"), ++ ("Heap", "proc.numa_maps.heap"), ++ ("Stack", "proc.numa_maps.stack"), ++ ("Private", "proc.numa_maps.private"), ++] ++ + def prefix(metric): + last_part = metric.split('.')[-1] + result = last_part[0].upper() + last_part[1:] +@@ -96,6 +114,7 @@ class MetricRepository: + self.group = group + self.current_cached_values = {} + self.previous_cached_values = {} ++ self.current_cached_instance_names = {} + + def _fetch_current_values(self, metric, instance): + if instance is not None: +@@ -192,22 +211,465 @@ class NUMAStat: + def print_numa(self, width, nodes, data): + self.__format_table(width, nodes, data) + ++class ProcessNUMAStat: ++ def __init__(self, group, ignore_pid=None): ++ self.group = group ++ self.repo = MetricRepository(group) ++ self.ignore_pid = ignore_pid ++ ++ def __resize(self, width): ++ """ Find a suitable display width limit (matches NUMAStat.resize) """ ++ if width == 0: ++ if not sys.stdout.isatty(): ++ width = 1000000000 # mimic numastat(1) here ++ else: ++ try: ++ # popen() is SAFE, command is a literal string ++ (_, width) = os.popen('stty size', 'r').read().split() ++ width = int(width) ++ except Exception: ++ width = 80 ++ width = int(os.getenv('NUMASTAT_WIDTH', str(width))) ++ return max(int(width), 32) ++ ++ def __normalize_value(self, value): ++ if value is None: ++ return "" ++ if hasattr(value, "decode"): ++ try: ++ return value.decode("utf-8") ++ except Exception: ++ return value.decode("utf-8", "ignore") ++ return str(value) ++ ++ def __normalize_cmdline(self, value): ++ if value is None: ++ return "" ++ ++ # Convert bytes → string ++ if isinstance(value, bytes): ++ value = value.decode("utf-8", "ignore") ++ ++ # /proc//cmdline uses NULL separators ++ if "\0" in value: ++ parts = value.split("\0") ++ value = " ".join(p for p in parts if p) ++ ++ return value.strip() ++ ++ def __is_missing_command(self, command): ++ normalized = self.__normalize_value(command).strip() ++ return normalized == "" or normalized.lower() == "(null)" ++ ++ def __parse_nodes(self, value): ++ node_values = {} ++ text = self.__normalize_value(value) ++ if not text: ++ return node_values ++ for token in text.split(','): ++ if ':' not in token: ++ continue ++ name, raw_value = token.split(':', 1) ++ name = name.strip() ++ if not name.startswith("node"): ++ continue ++ try: ++ node_id = int(name[4:]) ++ node_values[node_id] = float(raw_value) ++ except (TypeError, ValueError): ++ continue ++ return node_values ++ ++ def __matches_process(self, pid, selectors, haystack): ++ if not selectors: ++ return True ++ haystack = self.__normalize_value(haystack) ++ for selector in selectors: ++ selector_text = self.__normalize_value(selector) ++ if re.fullmatch(r"\d+", selector_text): ++ try: ++ if int(selector_text) == int(pid): ++ return True ++ except (TypeError, ValueError): ++ continue ++ continue ++ if selector_text in haystack: ++ return True ++ return False ++ ++ def __collect_process_categories(self, inst_id, metric_maps): ++ category_values = {} ++ nodes = set() ++ has_data = False ++ for label, metric in PROCESS_NUMA_METRICS: ++ parsed = self.__parse_nodes(metric_maps[metric].get(inst_id, "")) ++ if parsed: ++ has_data = True ++ category_values[label] = parsed ++ nodes.update(parsed.keys()) ++ return category_values, nodes, has_data ++ ++ def __sum_categories(self, category_values): ++ node_totals = {} ++ for values in category_values.values(): ++ for node_id, value in values.items(): ++ node_totals[node_id] = node_totals.get(node_id, 0.0) + value ++ return node_totals ++ ++ def __process_rows(self, selectors, system_nodes=None): ++ rows = [] ++ nodes = set(system_nodes or []) ++ selectors_provided = bool(selectors) ++ requested_pids = set() ++ for selector in selectors or []: ++ selector_text = self.__normalize_value(selector) ++ if re.fullmatch(r"\d+", selector_text): ++ try: ++ requested_pids.add(int(selector_text)) ++ except (TypeError, ValueError): ++ continue ++ pid_map = self.repo.current_values("proc.psinfo.pid") or {} ++ command_map = self.repo.current_values("proc.psinfo.cmd") or {} ++ psargs_map = self.repo.current_values("proc.psinfo.psargs") or {} ++ metric_maps = {} ++ for _, metric in PROCESS_NUMA_METRICS: ++ metric_maps[metric] = self.repo.current_values(metric) or {} ++ ++ # Restrict scanning to processes that are actually present in proc.numa_maps.*. ++ numa_inst_ids = set() ++ for values in metric_maps.values(): ++ try: ++ numa_inst_ids.update(values.keys()) ++ except Exception: ++ continue ++ ++ for inst_id, pid in sorted(pid_map.items(), key=lambda item: item[1]): ++ if inst_id not in numa_inst_ids and int(pid) not in requested_pids: ++ continue ++ if ( ++ self.ignore_pid is not None ++ and int(pid) == int(self.ignore_pid) ++ and int(pid) not in requested_pids ++ ): ++ continue ++ command = command_map.get(inst_id, "") ++ full_command = psargs_map.get(inst_id, command) ++ if self.__is_missing_command(command): ++ if int(pid) in requested_pids: ++ command = "unknown" ++ full_command = "unknown" ++ else: ++ continue ++ # Constrain matching to the same process set and labels as the ++ # proc.numa_maps.* instance domains (e.g. `pminfo -f proc.numa_maps.heap`). ++ match_text = "%s %s" % ( ++ self.__normalize_value(command), ++ self.__normalize_cmdline(full_command), ++ ) ++ if not self.__matches_process(pid, selectors, match_text): ++ continue ++ ++ category_values, category_nodes, has_data = self.__collect_process_categories( ++ inst_id, ++ metric_maps, ++ ) ++ if not has_data and not selectors_provided: ++ continue ++ if not has_data and self.__is_missing_command(command): ++ continue ++ ++ if category_nodes: ++ nodes.update(category_nodes) ++ node_totals = self.__sum_categories(category_values) ++ rows.append((pid, command, node_totals, category_values)) ++ return rows, sorted(nodes) ++ ++ def __node_blocks_for_table(self, nodes, width, pid_col_width): ++ num_col_width = 15 ++ sep = " " ++ col_width = len(sep) + num_col_width ++ ++ width = self.__resize(width) ++ max_cols = int((width - pid_col_width) / col_width) ++ max_cols = max(1, max_cols) ++ max_nodes_no_total = max_cols ++ ++ # If we can only fit one numeric column, there is no room to display ++ # any node column together with a Total column. Emit the nodes first ++ # (one per block), then a final Total-only block. ++ if max_cols <= 1: ++ for node_id in nodes: ++ yield [node_id], False ++ yield [], True ++ return ++ ++ max_nodes_with_total = max_cols - 1 ++ ++ if len(nodes) <= max_nodes_with_total: ++ yield nodes, True ++ return ++ ++ done = 0 ++ while len(nodes) - done > max_nodes_with_total: ++ remaining = len(nodes) - done ++ chunk_size = min(max_nodes_no_total, remaining) ++ ++ # Avoid consuming all remaining nodes in a non-total block (which ++ # would otherwise suppress the Total column entirely when nodes ++ # exactly fill the display width). ++ if remaining <= max_nodes_no_total and remaining - chunk_size == 0: ++ chunk_size = max(1, remaining - 1) ++ ++ chunk = nodes[done:done + chunk_size] ++ if not chunk: ++ break ++ yield chunk, False ++ done += chunk_size ++ ++ yield nodes[done:], True ++ ++ def __print_process_table(self, rows, nodes, width): ++ print("Per-node process memory usage (in MBs)") ++ pid_col_width = max( ++ 16, ++ max( ++ len("%s (%s)" % (pid, self.__normalize_value(command))) ++ for pid, command, _, _ in rows ++ ), ++ ) ++ num_col_width = 15 ++ sep = " " ++ ++ node_totals_all = dict((node_id, 0.0) for node_id in nodes) ++ row_totals = {} ++ grand_total = 0.0 ++ for pid, _, per_node, _ in rows: ++ total = 0.0 ++ for node_id in nodes: ++ value = per_node.get(node_id, 0.0) ++ node_totals_all[node_id] += value ++ total += value ++ row_totals[pid] = total ++ grand_total += total ++ ++ for chunk, include_total in self.__node_blocks_for_table(nodes, width, pid_col_width): ++ header = "%-*s" % (pid_col_width, "PID") ++ for node_id in chunk: ++ header += "%s%*s" % (sep, num_col_width, "Node %d" % node_id) ++ if include_total: ++ header += "%s%*s" % (sep, num_col_width, "Total") ++ print(header) ++ ++ line = "-" * pid_col_width ++ for _ in range(len(chunk) + (1 if include_total else 0)): ++ line += "%s%s" % (sep, "-" * num_col_width) ++ print(line) ++ ++ for pid, command, per_node, _ in rows: ++ label = "%s (%s)" % (pid, self.__normalize_value(command)) ++ row = "%-*s" % (pid_col_width, label) ++ for node_id in chunk: ++ row += "%s%*.2f" % (sep, num_col_width, per_node.get(node_id, 0.0)) ++ if include_total: ++ row += "%s%*.2f" % (sep, num_col_width, row_totals.get(pid, 0.0)) ++ print(row) ++ ++ print(line) ++ total_row = "%-*s" % (pid_col_width, "Total") ++ for node_id in chunk: ++ total_row += "%s%*.2f" % (sep, num_col_width, node_totals_all.get(node_id, 0.0)) ++ if include_total: ++ total_row += "%s%*.2f" % (sep, num_col_width, grand_total) ++ print(total_row) ++ print() ++ ++ def __node_blocks(self, nodes, width): ++ label_width = 16 ++ num_col_width = 15 ++ sep = " " ++ col_width = len(sep) + num_col_width ++ ++ width = self.__resize(width) ++ max_cols = int((width - label_width) / col_width) ++ max_cols = max(1, max_cols) ++ max_nodes_no_total = max_cols ++ ++ # If we can only fit one numeric column, there is no room to display ++ # any node column together with a Total column. Emit the nodes first ++ # (one per block), then a final Total-only block. ++ if max_cols <= 1: ++ for node_id in nodes: ++ yield [node_id], False ++ yield [], True ++ return ++ ++ max_nodes_with_total = max_cols - 1 ++ ++ if len(nodes) <= max_nodes_with_total: ++ yield nodes, True ++ return ++ ++ done = 0 ++ while len(nodes) - done > max_nodes_with_total: ++ remaining = len(nodes) - done ++ chunk_size = min(max_nodes_no_total, remaining) ++ ++ # Avoid consuming all remaining nodes in a non-total block (which ++ # would otherwise suppress the Total column entirely when nodes ++ # exactly fill the display width). ++ if remaining <= max_nodes_no_total and remaining - chunk_size == 0: ++ chunk_size = max(1, remaining - 1) ++ ++ chunk = nodes[done:done + chunk_size] ++ if not chunk: ++ break ++ yield chunk, False ++ done += chunk_size ++ ++ yield nodes[done:], True ++ ++ def __print_process_detail(self, row, nodes, width): ++ pid, command, _, categories = row ++ print("Per-node process memory usage (in MBs) for PID %s (%s)" % ++ (pid, self.__normalize_value(command))) ++ label_width = 16 ++ num_col_width = 15 ++ sep = " " ++ ++ if not nodes: ++ nodes = [0] ++ ++ category_totals = {} ++ node_totals = dict((node_id, 0.0) for node_id in nodes) ++ all_total = 0.0 ++ for label, _ in PROCESS_NUMA_METRICS: ++ values = categories.get(label, {}) ++ total = 0.0 ++ for node_id in nodes: ++ value = values.get(node_id, 0.0) ++ total += value ++ node_totals[node_id] += value ++ category_totals[label] = total ++ all_total += total ++ ++ for chunk, include_total in self.__node_blocks(nodes, width): ++ header = "%-*s" % (label_width, "") ++ for node_id in chunk: ++ header += "%s%*s" % (sep, num_col_width, "Node %d" % node_id) ++ if include_total: ++ header += "%s%*s" % (sep, num_col_width, "Total") ++ print(header) ++ ++ header_line = " " * label_width ++ for _ in range(len(chunk) + (1 if include_total else 0)): ++ header_line += "%s%s" % (sep, "-" * num_col_width) ++ print(header_line) ++ ++ for label, _ in PROCESS_NUMA_METRICS: ++ row = "%-*s" % (label_width, label) ++ values = categories.get(label, {}) ++ for node_id in chunk: ++ row += "%s%*.2f" % (sep, num_col_width, values.get(node_id, 0.0)) ++ if include_total: ++ row += "%s%*.2f" % (sep, num_col_width, category_totals.get(label, 0.0)) ++ print(row) ++ ++ line = "-" * label_width ++ for _ in range(len(chunk) + (1 if include_total else 0)): ++ line += "%s%s" % (sep, "-" * num_col_width) ++ print(line) ++ ++ total_row = "%-*s" % (label_width, "Total") ++ for node_id in chunk: ++ total_row += "%s%*.2f" % (sep, num_col_width, node_totals.get(node_id, 0.0)) ++ if include_total: ++ total_row += "%s%*.2f" % (sep, num_col_width, all_total) ++ print(total_row) ++ print() ++ ++ def print_processes(self, selectors, system_nodes=None, width=0): ++ rows, nodes = self.__process_rows(selectors, system_nodes=system_nodes) ++ if not rows: ++ if selectors: ++ print("No matching processes with NUMA maps data.") ++ else: ++ print("No processes with NUMA maps data.") ++ print() ++ return ++ if not nodes: ++ nodes = [0] ++ selector_texts = [self.__normalize_value(s).strip() for s in (selectors or [])] ++ selectors_are_pids = bool(selector_texts) and all( ++ re.fullmatch(r"\d+", text) for text in selector_texts ++ ) ++ if selectors_are_pids: ++ requested = [] ++ for text in selector_texts: ++ try: ++ requested.append(int(text)) ++ except (TypeError, ValueError): ++ continue ++ ++ rows_by_pid = {} ++ for pid, command, per_node, categories in rows: ++ try: ++ rows_by_pid[int(pid)] = (pid, command, per_node, categories) ++ except (TypeError, ValueError): ++ continue ++ ++ if len(requested) == 1: ++ pid = requested[0] ++ found = rows_by_pid.get(pid) ++ if found is None: ++ print("No matching process with NUMA maps data for PID %s." % pid) ++ print() ++ return ++ self.__print_process_detail(found, nodes, width) ++ return ++ ++ selected_rows = [] ++ for pid in requested: ++ found = rows_by_pid.get(pid) ++ if found is None: ++ print("No matching process with NUMA maps data for PID %s." % pid) ++ print() ++ continue ++ row_pid, command, per_node, categories = found ++ selected_rows.append((row_pid, command, per_node, categories)) ++ if selected_rows: ++ self.__print_process_table(selected_rows, nodes, width) ++ return ++ ++ single_selector = len(selectors or []) == 1 ++ if single_selector and len(rows) == 1: ++ self.__print_process_detail(rows[0], nodes, width) ++ return ++ ++ self.__print_process_table(rows, nodes, width) ++ + class NumaStatOption(pmapi.pmOptions): + context = None + timefmt = "%m/%d/%Y %H:%M:%S" + width = 0 + mem_out = False + numa_out = False ++ process_out = False ++ process_filters = [] + + def override(self,opt): + """ Override standard PCP options to match numastat(1) """ +- if opt == 'n': ++ if opt in ('n', 'p'): + return True + return False + + def __init__(self): + pmapi.pmOptions.__init__(self) +- self.pmSetShortOptions("w:mV?:n") ++ self.width = 0 ++ self.mem_out = False ++ self.numa_out = False ++ self.process_out = False ++ self.process_filters = [] ++ self.pmSetShortOptions("w:mV?np") + self.pmSetOptionCallback(self.extraOptions) + self.pmSetOverrideCallback(self.override) + self.pmSetLongOptionHeader("Numastat options") +@@ -215,6 +677,7 @@ class NumaStatOption(pmapi.pmOptions): + # Map long options to our non-conflicting short letters + self.pmSetLongOption("meminfo", 0, 'm', "", "show meminfo-like system-wide memory usage") + self.pmSetLongOption("numastat", 0, 'n', "", "show the numastat statistics info") ++ self.pmSetLongOption("process", 0, 'p', "", "show per-process NUMA memory usage") + self.pmSetLongOptionVersion() + self.pmSetLongOptionHelp() + +@@ -225,6 +688,8 @@ class NumaStatOption(pmapi.pmOptions): + self.mem_out = True + elif opt == "n": + self.numa_out = True ++ elif opt == "p": ++ self.process_out = True + elif opt == "V": + pass + else: +@@ -232,7 +697,7 @@ class NumaStatOption(pmapi.pmOptions): + return True + + def checkoptions(self): +- if (not self.mem_out) and (not self.numa_out) and (self.width == 0): ++ if (not self.mem_out) and (not self.numa_out) and (not self.process_out): + self.numa_out = True + if self.width < 0: + return False +@@ -284,6 +749,33 @@ class NumaStatReport(pmcc.MetricGroupPrinter): + nodes.sort(key=lambda t: t[0]) + return nodes + ++ def __discover_nodes_all(self, group, name): ++ # Build list of nodes from instances (instance id, instance name) ++ nodes = [] ++ try: ++ for ent in group[name].netValues: ++ inst_id = ent[0].inst ++ inst_name = ent[1] # usually "node0", "node1", ... ++ nodes.append((inst_id, inst_name)) ++ except Exception: ++ pass ++ nodes.sort(key=lambda t: t[0]) ++ return nodes ++ ++ def __node_ids(self, nodes): ++ node_ids = [] ++ for inst_id, inst_name in nodes or []: ++ match = re.match(r"node(\d+)$", str(inst_name)) ++ if match: ++ node_ids.append(int(match.group(1))) ++ continue ++ try: ++ node_ids.append(int(inst_id)) ++ except (TypeError, ValueError): ++ continue ++ node_ids = sorted(set(node_ids)) ++ return node_ids or None ++ + def report(self, manager): + # Print in a stable order + group = manager["sys_info"] +@@ -296,17 +788,54 @@ class NumaStatReport(pmcc.MetricGroupPrinter): + + output_numa = ( + self.options.numa_out +- or (not self.options.mem_out and not self.options.numa_out) + ) + output_mem = self.options.mem_out +- group = manager["numastat"] +- nodes = self.__discover_nodes(group, "mem.numa.util.total") +- timestamp = self.__get_timestamp(group) ++ output_process = self.options.process_out ++ ++ if output_mem or output_numa: ++ group = manager["numastat"] ++ timestamp_group = group ++ elif output_process: ++ group = manager["process_numa"] ++ timestamp_group = group ++ else: ++ return ++ ++ timestamp = self.__get_timestamp(timestamp_group) + print("%-20s : %s"%("Timestamp", timestamp)) + if output_mem: ++ nodes = self.__discover_nodes(group, "mem.numa.util.total") + NUMAStat(group).print_mem(self.options.width, nodes, "meminfo") + if output_numa: ++ nodes = self.__discover_nodes(group, "mem.numa.util.total") + NUMAStat(group).print_numa(self.options.width, nodes, "numastat") ++ if output_process: ++ ignore_pid = None ++ if ( ++ NumaStatOption.context is not PM_CONTEXT_ARCHIVE ++ and not self.options.pmGetOptionHosts() ++ ): ++ ignore_pid = os.getpid() ++ system_nodes = None ++ try: ++ system_nodes = self.__node_ids( ++ self.__discover_nodes(manager["sys_info"], "hinv.node.online") ++ ) ++ except Exception: ++ system_nodes = None ++ if system_nodes is None: ++ try: ++ system_nodes = self.__node_ids( ++ self.__discover_nodes_all(manager["sys_info"], "mem.numa.alloc.hit") ++ ) ++ except Exception: ++ system_nodes = None ++ ++ ProcessNUMAStat(manager["process_numa"], ignore_pid).print_processes( ++ self.options.process_filters, ++ system_nodes=system_nodes, ++ width=self.options.width, ++ ) + + if ( + NumaStatOption.context is not PM_CONTEXT_ARCHIVE +@@ -323,13 +852,37 @@ if __name__ == '__main__': + raise pmapi.pmUsageErr() + NumaStatOption.context = mngr.type + +- missing = mngr.checkMissingMetrics(ALL_METRICS) ++ opts.process_filters = opts.pmGetOperands() ++ if opts.process_filters and not opts.process_out: ++ print("Process selectors require -p/--process option") ++ raise pmapi.pmUsageErr() ++ if not opts.process_filters and opts.process_out: ++ print("Provide pid or process name for -p/--process option") ++ raise pmapi.pmUsageErr() ++ ++ required_metrics = list(SYS_METRICS) ++ if opts.mem_out or opts.numa_out: ++ required_metrics.extend(ALL_METRICS) ++ if opts.process_out: ++ required_metrics.extend(PROCESS_METRICS) ++ required_metrics = list(dict.fromkeys(required_metrics)) ++ ++ missing = mngr.checkMissingMetrics(required_metrics) + if missing is not None: + sys.stderr.write('Error: not all required metrics are available\nMissing: %s\n' % (missing)) + sys.exit(1) + +- mngr["numastat"] = ALL_METRICS +- mngr["sys_info"] = SYS_METRICS ++ sys_info_metrics = list(SYS_METRICS) ++ if mngr.checkMissingMetrics(["hinv.node.online"]) is None: ++ sys_info_metrics.append("hinv.node.online") ++ if mngr.checkMissingMetrics(["mem.numa.alloc.hit"]) is None: ++ sys_info_metrics.append("mem.numa.alloc.hit") ++ ++ if opts.mem_out or opts.numa_out: ++ mngr["numastat"] = ALL_METRICS ++ if opts.process_out: ++ mngr["process_numa"] = PROCESS_METRICS ++ mngr["sys_info"] = sys_info_metrics + mngr.printer = NumaStatReport(opts) + sts = mngr.run() + sys.exit(sts) +diff --git a/src/pmdas/linux_proc/clusters.h b/src/pmdas/linux_proc/clusters.h +index edfd5ad..547c195 100644 +--- a/src/pmdas/linux_proc/clusters.h ++++ b/src/pmdas/linux_proc/clusters.h +@@ -72,8 +72,9 @@ + #define CLUSTER_CGROUP2_IRQ_PRESSURE 76 + #define CLUSTER_PID_FDINFO 77 /* /proc//fdinfo */ + #define CLUSTER_HOTPROC_PID_FDINFO 78 /* /proc//fdinfo */ ++#define CLUSTER_PID_NUMA_MAPS 79 /* /proc//numa_maps */ + + #define MIN_CLUSTER 8 /* first cluster number we use here */ +-#define MAX_CLUSTER 79 /* one more than highest cluster number used */ ++#define MAX_CLUSTER 80 /* one more than highest cluster number used */ + + #endif /* _CLUSTERS_H */ +diff --git a/src/pmdas/linux_proc/help b/src/pmdas/linux_proc/help +index 79824bb..c1b2198 100644 +--- a/src/pmdas/linux_proc/help ++++ b/src/pmdas/linux_proc/help +@@ -75,6 +75,19 @@ kernel threads + @ proc.runq.kernel number of kernel threads + Instantaneous number of processes with virtual size of zero (kernel threads) + ++@ proc.numa_maps.hugepage per-node hugepage mapped memory in MB (/proc//numa_maps) ++Hugepage map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++@ proc.numa_maps.heap per-node heap mapped memory in MB (/proc//numa_maps) ++Heap map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++@ proc.numa_maps.stack per-node stack mapped memory in MB (/proc//numa_maps) ++Stack map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++@ proc.numa_maps.private per-node private mapped memory in MB (/proc//numa_maps) ++Private map values are reported as comma-separated node/value pairs, e.g. ++node0:0.00,node1:4.00 where each value is in megabytes. ++ + @ proc.control.all.threads process indom includes threads + If set to one, the process instance domain as reported by pmdaproc + contains all threads as well as the processes that started them. +diff --git a/src/pmdas/linux_proc/indom.h b/src/pmdas/linux_proc/indom.h +index abe2596..c5c00a6 100644 +--- a/src/pmdas/linux_proc/indom.h ++++ b/src/pmdas/linux_proc/indom.h +@@ -62,6 +62,7 @@ extern FILE *proc_statsfile(const char *, char *, int); + + /* Generic globals setup during PMDA startup */ + extern size_t _pm_system_pagesize; ++extern size_t _pm_system_hugepagesize; + extern long _pm_hertz; + + /* +diff --git a/src/pmdas/linux_proc/pmda.c b/src/pmdas/linux_proc/pmda.c +index 80d9653..0083419 100644 +--- a/src/pmdas/linux_proc/pmda.c ++++ b/src/pmdas/linux_proc/pmda.c +@@ -66,6 +66,7 @@ static int autogroup = -1; /* =1 autogroup enabled */ + static unsigned int threads; /* control.all.threads */ + static char * cgroups; /* control.all.cgroups */ + size_t _pm_system_pagesize; ++size_t _pm_system_hugepagesize; + long _pm_hertz; + + /* +@@ -1395,6 +1396,23 @@ static pmdaMetric metrictab[] = { + { NULL, { PMDA_PMID(CLUSTER_PID_FDINFO,12), PM_TYPE_U64, PROC_INDOM, + PM_SEM_INSTANT, PMDA_PMUNITS(1,0,0,PM_SPACE_KBYTE,0,0)}}, + ++/* ++* numa_maps cluster ++*/ ++ ++/* proc.numa_maps.hugepage */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,0), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++/* proc.numa_maps.heap */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,1), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++/* proc.numa_maps.stack */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,2), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++/* proc.numa_maps.private */ ++ { NULL, { PMDA_PMID(CLUSTER_PID_NUMA_MAPS,3), PM_TYPE_STRING, PROC_INDOM, ++ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0)}}, ++ + }; + + pmInDom +@@ -3534,6 +3552,34 @@ proc_fetchCallBack(pmdaMetric *mdesc, unsigned int inst, pmAtomValue *atom) + return PM_ERR_PMID; + } + break; ++ case CLUSTER_PID_NUMA_MAPS: ++ if (!have_access) ++ return PM_ERR_PERMISSION; ++ if ((entry = fetch_proc_pid_numa_maps(inst, active_proc_pid, &sts)) == NULL) ++ return sts; ++ if (!(entry->success & PROC_PID_FLAG_NUMA_MAPS)) ++ return 0; ++ ++ switch(item) { ++ case 0: /* proc.numa_maps.hugepage */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.huge_id); ++ break; ++ ++ case 1: /* proc.numa_maps.heap */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.heap_id); ++ break; ++ ++ case 2: /* proc.numa_maps.stack */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.stack_id); ++ break; ++ ++ case 3: /* proc.numa_maps.private */ ++ atom->cp = proc_strings_lookup(entry->numa_maps.private_id); ++ break; ++ default: /* unknown cluster */ ++ return PM_ERR_PMID; ++ } ++ break; + default: /* unknown cluster */ + return PM_ERR_PMID; + } +@@ -3896,6 +3942,30 @@ proc_gidname_lookup(int gid) + return ""; + } + ++static size_t ++proc_hugepagesize(void) ++{ ++ unsigned long huge_page_size_kb = 0; ++ char buf[128]; ++ FILE *fs; ++ ++ if ((fs = fopen("/proc/meminfo", "r")) == NULL) ++ return 0; ++ ++ while (fgets(buf, sizeof(buf), fs)) { ++ if (sscanf(buf, "Hugepagesize: %lu kB", &huge_page_size_kb) == 1) ++ break; ++ } ++ fclose(fs); ++ ++ /* Guard against overflow (CID 502014) */ ++ if (huge_page_size_kb > ULONG_MAX / 1024UL) { ++ return 0; ++ } ++ ++ return huge_page_size_kb * 1024UL; ++} ++ + /* + * Initialise the agent (both daemon and DSO). + */ +@@ -3917,6 +3987,10 @@ proc_init(pmdaInterface *dp) + _pm_system_pagesize = atoi(envpath); + else + _pm_system_pagesize = getpagesize(); ++ if ((envpath = getenv("PROC_HUGEPAGESIZE")) != NULL) ++ _pm_system_hugepagesize = atoi(envpath); ++ else ++ _pm_system_hugepagesize = proc_hugepagesize(); + if ((envpath = getenv("PROC_STATSPATH")) != NULL) + proc_statspath = envpath; + if ((envpath = getenv("PROC_THREADS")) != NULL) +diff --git a/src/pmdas/linux_proc/pmdaproc.1 b/src/pmdas/linux_proc/pmdaproc.1 +index b817d97..1c6f281 100644 +--- a/src/pmdas/linux_proc/pmdaproc.1 ++++ b/src/pmdas/linux_proc/pmdaproc.1 +@@ -35,6 +35,10 @@ The + PMDA exports metrics that measure the memory, processor and + other resource use of each process, as well as summary information + collated across all of the running processes. ++This includes per-process NUMA placement summaries from ++.I /proc//numa_maps ++via the ++.BR proc.numa_maps.*\ metrics. + The PMDA uses credentials passed from the + .BR PMAPI (3) + monitoring tool identifying the user requesting the information, +diff --git a/src/pmdas/linux_proc/proc_pid.c b/src/pmdas/linux_proc/proc_pid.c +index acb046c..0f8ea7d 100644 +--- a/src/pmdas/linux_proc/proc_pid.c ++++ b/src/pmdas/linux_proc/proc_pid.c +@@ -2530,3 +2530,272 @@ fetch_proc_pid_fdinfo(int id, proc_pid_t *proc_pid, int *sts) + + return (*sts < 0) ? NULL : ep; + } ++ ++#define PROCESS_HUGE_INDEX 0 ++#define PROCESS_HEAP_INDEX 1 ++#define PROCESS_STACK_INDEX 2 ++#define PROCESS_PRIVATE_INDEX 3 ++#define PROCESS_CATEGORY_COUNT 4 ++ ++#define MEGABYTE (1024.0 * 1024.0) ++ ++static const char *process_mem_tokens[] = { ++ "huge", ++ "heap", ++ "stack", ++}; ++ ++typedef struct { ++ char *s; ++ size_t len; ++ size_t cap; ++} strbuf_t; ++ ++typedef struct { ++ int node; ++ double values[PROCESS_CATEGORY_COUNT]; ++} numa_node_totals_t; ++ ++static int ++append_numa_maps_node(strbuf_t *b, int node_num, double value_mb) ++{ ++ char tmp[64]; ++ char *newptr; ++ size_t needed, newcap; ++ int n; ++ ++ n = pmsprintf(tmp, sizeof(tmp), "%snode%d:%.2f", ++ b->len > 0 ? "," : "", node_num, value_mb); ++ if (n < 0 || n >= (int)sizeof(tmp)) ++ return -E2BIG; ++ ++ if (b->s == NULL) { ++ b->cap = 128; ++ b->s = (char *)malloc(b->cap); ++ if (b->s == NULL) ++ return -ENOMEM; ++ b->len = 0; ++ b->s[0] = '\0'; ++ } ++ ++ needed = b->len + (size_t)n + 1; ++ if (needed > b->cap) { ++ newcap = b->cap; ++ while (newcap < needed) ++ newcap *= 2; ++ ++ newptr = (char *)realloc(b->s, newcap); ++ if (newptr == NULL) ++ return -ENOMEM; ++ ++ b->s = newptr; ++ b->cap = newcap; ++ } ++ ++ memcpy(b->s + b->len, tmp, (size_t)n); ++ b->len += (size_t)n; ++ b->s[b->len] = '\0'; ++ return 0; ++} ++ ++static int ++find_or_add_numa_node(numa_node_totals_t **nodes, int *node_count, int node) ++{ ++ numa_node_totals_t *new_nodes; ++ int i; ++ ++ for (i = 0; i < *node_count; i++) { ++ if ((*nodes)[i].node == node) ++ return i; ++ } ++ ++ new_nodes = (numa_node_totals_t *)realloc(*nodes, ++ (*node_count + 1) * sizeof(*new_nodes)); ++ if (new_nodes == NULL) ++ return -ENOMEM; ++ ++ *nodes = new_nodes; ++ (*nodes)[*node_count].node = node; ++ memset((*nodes)[*node_count].values, 0, sizeof((*nodes)[*node_count].values)); ++ (*node_count)++; ++ return *node_count - 1; ++} ++ ++static int ++compare_numa_node_totals(const void *a, const void *b) ++{ ++ const numa_node_totals_t *node_a = (const numa_node_totals_t *)a; ++ const numa_node_totals_t *node_b = (const numa_node_totals_t *)b; ++ ++ return node_a->node - node_b->node; ++} ++ ++static int ++numa_maps_category(const char *line) ++{ ++ char *copy, *tok, *saveptr = NULL; ++ int i; ++ ++ if ((copy = strdup(line)) == NULL) ++ return PROCESS_PRIVATE_INDEX; ++ ++ tok = strtok_r(copy, " \t", &saveptr); ++ while (tok != NULL) { ++ for (i = PROCESS_HUGE_INDEX; i <= PROCESS_STACK_INDEX; i++) { ++ if (strcmp(tok, process_mem_tokens[i]) == 0) { ++ free(copy); ++ return i; ++ } ++ } ++ tok = strtok_r(NULL, " \t", &saveptr); ++ } ++ free(copy); ++ return PROCESS_PRIVATE_INDEX; ++} ++ ++static int ++parse_proc_numa_maps(proc_pid_entry_t *ep, size_t buflen, char *buf) ++{ ++ strbuf_t huge = {0}, heap = {0}, stack = {0}, priv = {0}; ++ numa_node_totals_t *nodes = NULL; ++ double page_size_bytes, huge_page_size_bytes; ++ int node_count = 0; ++ int sts = 0; ++ char *cur = buf; ++ char *end; ++ ++ if (buf == NULL || buflen == 0) ++ return 0; ++ /* ++ * Ensure the proc buffer is NUL-terminated so the string routines below ++ * cannot read past the end. read_proc_entry() allocates len+1 bytes and ++ * uses buflen (len) for the bytes-read value passed here. ++ */ ++ buf[buflen] = '\0'; ++ end = buf + buflen; ++ ++ page_size_bytes = (double)_pm_system_pagesize; ++ if (page_size_bytes <= 0.0) ++ page_size_bytes = 4096.0; ++ ++ huge_page_size_bytes = (double)_pm_system_hugepagesize; ++ if (huge_page_size_bytes <= 0.0) ++ huge_page_size_bytes = 2048 * 1024; // Taking a common hugepagesize value as a fallback ++ ++ while (cur < end && *cur) { ++ char *nl = memchr(cur, '\n', (size_t)(end - cur)); ++ char *tok, *saveptr = NULL; ++ int category; ++ ++ if (nl != NULL) ++ *nl = '\0'; ++ ++ category = numa_maps_category(cur); ++ tok = strtok_r(cur, " \t", &saveptr); ++ while (tok != NULL) { ++ int node, index; ++ double pages, bytes; ++ ++ if (tok[0] == 'N' && sscanf(tok, "N%d=%lf", &node, &pages) == 2) { ++ index = find_or_add_numa_node(&nodes, &node_count, node); ++ if (index < 0) { ++ sts = index; ++ goto cleanup; ++ } ++ ++ if (category == PROCESS_HUGE_INDEX) ++ bytes = pages * huge_page_size_bytes; ++ else ++ bytes = pages * page_size_bytes; ++ ++ nodes[index].values[category] += bytes / MEGABYTE; ++ } ++ tok = strtok_r(NULL, " \t", &saveptr); ++ } ++ ++ if (nl == NULL) ++ break; ++ *nl = '\n'; ++ cur = nl + 1; ++ } ++ ++ if (node_count > 1) ++ qsort(nodes, node_count, sizeof(*nodes), compare_numa_node_totals); ++ ++ for (int i = 0; i < node_count; i++) { ++ sts = append_numa_maps_node(&huge, nodes[i].node, ++ nodes[i].values[PROCESS_HUGE_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ sts = append_numa_maps_node(&heap, nodes[i].node, ++ nodes[i].values[PROCESS_HEAP_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ sts = append_numa_maps_node(&stack, nodes[i].node, ++ nodes[i].values[PROCESS_STACK_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ sts = append_numa_maps_node(&priv, nodes[i].node, ++ nodes[i].values[PROCESS_PRIVATE_INDEX]); ++ if (sts < 0) ++ goto cleanup; ++ } ++ ++ if (huge.s != NULL) ++ ep->numa_maps.huge_id = proc_strings_insert(huge.s); ++ if (heap.s != NULL) ++ ep->numa_maps.heap_id = proc_strings_insert(heap.s); ++ if (stack.s != NULL) ++ ep->numa_maps.stack_id = proc_strings_insert(stack.s); ++ if (priv.s != NULL) ++ ep->numa_maps.private_id = proc_strings_insert(priv.s); ++ ++cleanup: ++ free(nodes); ++ free(huge.s); ++ free(heap.s); ++ free(stack.s); ++ free(priv.s); ++ return sts; ++} ++ ++static int ++refresh_proc_pid_numa_maps(proc_pid_entry_t *ep) ++{ ++ int fd, sts; ++ ++ if (ep->success & PROC_PID_FLAG_NUMA_MAPS) ++ return 0; ++ if ((fd = proc_open("numa_maps", ep)) < 0) ++ return maperr(); ++ ep->numa_maps.huge_id = -1; ++ ep->numa_maps.heap_id = -1; ++ ep->numa_maps.stack_id = -1; ++ ep->numa_maps.private_id = -1; ++ if ((sts = read_proc_entry(fd, &procbuflen, &procbuf)) >= 0) { ++ sts = parse_proc_numa_maps(ep, procbuflen, procbuf); ++ if (sts >= 0) ++ ep->success |= PROC_PID_FLAG_NUMA_MAPS; ++ } ++ close(fd); ++ return sts; ++} ++ ++/* ++ * fetch data from /proc//numa_maps entries for pid ++ */ ++proc_pid_entry_t * ++fetch_proc_pid_numa_maps(int id, proc_pid_t *proc_pid, int *sts) ++{ ++ proc_pid_entry_t *ep = proc_pid_entry_lookup(id, proc_pid); ++ ++ *sts = 0; ++ if (!ep) ++ return NULL; ++ ++ if (!(ep->fetched & PROC_PID_FLAG_NUMA_MAPS)) { ++ *sts = refresh_proc_pid_numa_maps(ep); ++ ep->fetched |= PROC_PID_FLAG_NUMA_MAPS; ++ } ++ return (*sts < 0) ? NULL : ep; ++} +diff --git a/src/pmdas/linux_proc/proc_pid.h b/src/pmdas/linux_proc/proc_pid.h +index f5762aa..145ec8d 100644 +--- a/src/pmdas/linux_proc/proc_pid.h ++++ b/src/pmdas/linux_proc/proc_pid.h +@@ -202,6 +202,16 @@ typedef struct { + uint64_t locked; + } proc_pid_smaps_t; + ++/* ++ * metrics in /proc//numa_maps ++ */ ++typedef struct { ++ int huge_id; ++ int heap_id; ++ int stack_id; ++ int private_id; ++} proc_pid_numa_maps_t; ++ + /* + * metrics in /proc//fdinfo/ + */ +@@ -245,6 +255,7 @@ enum { + PROC_PID_FLAG_EXE = 1<<15, + PROC_PID_FLAG_AUTOGROUP = 1<<16, + PROC_PID_FLAG_FDINFO = 1<<17, ++ PROC_PID_FLAG_NUMA_MAPS = 1<<18, + }; + + typedef struct { +@@ -279,6 +290,9 @@ typedef struct { + /* /proc//smaps_rollup cluster */ + proc_pid_smaps_t smaps; + ++ /* /proc//numa_maps cluster */ ++ proc_pid_numa_maps_t numa_maps; ++ + /* /proc//maps cluster */ + size_t maps_buflen; + +@@ -330,6 +344,9 @@ typedef struct { + /* lookup a proc hash entry */ + extern proc_pid_entry_t *proc_pid_entry_lookup(int, proc_pid_t *); + ++/* fetch a proc//numa_maps entry for pid */ ++extern proc_pid_entry_t *fetch_proc_pid_numa_maps(int, proc_pid_t *, int *); ++ + /* refresh the proc indom, reset all "fetched" flags */ + extern int refresh_proc_pid(proc_pid_t *, proc_runq_t *, int, const char *, const char *, int); + +diff --git a/src/pmdas/linux_proc/root_proc b/src/pmdas/linux_proc/root_proc +index 3e5b50d..16e75dd 100644 +--- a/src/pmdas/linux_proc/root_proc ++++ b/src/pmdas/linux_proc/root_proc +@@ -454,6 +454,14 @@ proc { + smaps PROC:*:* + autogroup PROC:*:* + control ++ numa_maps ++} ++ ++proc.numa_maps { ++ hugepage PROC:79:0 ++ heap PROC:79:1 ++ stack PROC:79:2 ++ private PROC:79:3 + } + + hotproc { +-- +2.43.7 + diff --git a/fix-pmdabpf-noarch-man-page-build-failure.patch b/fix-pmdabpf-noarch-man-page-build-failure.patch deleted file mode 100644 index 87dde64..0000000 --- a/fix-pmdabpf-noarch-man-page-build-failure.patch +++ /dev/null @@ -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 diff --git a/pcp-7.0.3-CVE-2026-16524.patch b/pcp-7.0.3-CVE-2026-16524.patch new file mode 100644 index 0000000..531c9ae --- /dev/null +++ b/pcp-7.0.3-CVE-2026-16524.patch @@ -0,0 +1,260 @@ +From c5cbeceb7d Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) + +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 $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 + #include ++#include + #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); + } diff --git a/pcp-7.0.3-CVE-2026-16526.patch b/pcp-7.0.3-CVE-2026-16526.patch new file mode 100644 index 0000000..fc6416f --- /dev/null +++ b/pcp-7.0.3-CVE-2026-16526.patch @@ -0,0 +1,228 @@ +From 7e27614006 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) + +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 ++#include "libpcp.h" ++#include ++ ++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, diff --git a/pcp-7.0.3-CVE-2026-16527.patch b/pcp-7.0.3-CVE-2026-16527.patch new file mode 100644 index 0000000..dc165c0 --- /dev/null +++ b/pcp-7.0.3-CVE-2026-16527.patch @@ -0,0 +1,198 @@ +From d96ba5a716 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) + +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, + }; + diff --git a/pcp-7.0.3-CVE-2026-16529.patch b/pcp-7.0.3-CVE-2026-16529.patch new file mode 100644 index 0000000..a83dd98 --- /dev/null +++ b/pcp-7.0.3-CVE-2026-16529.patch @@ -0,0 +1,145 @@ +From ef848fb978 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) + +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; + } diff --git a/pcp-7.0.3-CVE-2026-16530.patch b/pcp-7.0.3-CVE-2026-16530.patch new file mode 100644 index 0000000..c946bec --- /dev/null +++ b/pcp-7.0.3-CVE-2026-16530.patch @@ -0,0 +1,387 @@ +From 2aac9257a42348f8405713c3f1b8429612a4f9ce Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- + 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(); diff --git a/pcp-7.0.3-CVE-2026-16531.patch b/pcp-7.0.3-CVE-2026-16531.patch new file mode 100644 index 0000000..f2abc30 --- /dev/null +++ b/pcp-7.0.3-CVE-2026-16531.patch @@ -0,0 +1,212 @@ +From 4356ff59f4d6c45e149c881a6a0d910380adcdad Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- + 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) { diff --git a/pcp-7.0.3-OOB-pmDecodeInstance.patch b/pcp-7.0.3-OOB-pmDecodeInstance.patch new file mode 100644 index 0000000..8351f67 --- /dev/null +++ b/pcp-7.0.3-OOB-pmDecodeInstance.patch @@ -0,0 +1,56 @@ +From b743fc5879 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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", diff --git a/pcp-7.0.3-OOB-pmDecodeLabel.patch b/pcp-7.0.3-OOB-pmDecodeLabel.patch new file mode 100644 index 0000000..b4d1614 --- /dev/null +++ b/pcp-7.0.3-OOB-pmDecodeLabel.patch @@ -0,0 +1,55 @@ +From e512482e7d Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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; + } diff --git a/pcp-7.0.3-OOB-pmDecodeLogStatus.patch b/pcp-7.0.3-OOB-pmDecodeLogStatus.patch new file mode 100644 index 0000000..75dff11 --- /dev/null +++ b/pcp-7.0.3-OOB-pmDecodeLogStatus.patch @@ -0,0 +1,470 @@ +From 5366a546d6 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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) { diff --git a/pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch b/pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch new file mode 100644 index 0000000..7d0e9b3 --- /dev/null +++ b/pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch @@ -0,0 +1,36 @@ +From 7f42013d33 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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); diff --git a/pcp-7.0.3-OOB-pmLogLoadLabelSet.patch b/pcp-7.0.3-OOB-pmLogLoadLabelSet.patch new file mode 100644 index 0000000..bda1fd3 --- /dev/null +++ b/pcp-7.0.3-OOB-pmLogLoadLabelSet.patch @@ -0,0 +1,100 @@ +From ccd1bb1679 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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); diff --git a/pcp-7.0.3-pducrash-oob-tests.patch b/pcp-7.0.3-pducrash-oob-tests.patch new file mode 100644 index 0000000..7973f9d --- /dev/null +++ b/pcp-7.0.3-pducrash-oob-tests.patch @@ -0,0 +1,155 @@ +From 7465d7cbdb Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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 diff --git a/pcp-7.0.3-pmdaroot-peer-credentials.patch b/pcp-7.0.3-pmdaroot-peer-credentials.patch new file mode 100644 index 0000000..2962876 --- /dev/null +++ b/pcp-7.0.3-pmdaroot-peer-credentials.patch @@ -0,0 +1,111 @@ +From 2a4bd9f81a Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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 ++#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); + } diff --git a/pcp-7.0.3-pmieconf-command-injection.patch b/pcp-7.0.3-pmieconf-command-injection.patch new file mode 100644 index 0000000..a88e5d6 --- /dev/null +++ b/pcp-7.0.3-pmieconf-command-injection.patch @@ -0,0 +1,136 @@ +From cdc9676ab6 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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; + } + } diff --git a/pcp-7.0.3-pmlogmv-command-injection.patch b/pcp-7.0.3-pmlogmv-command-injection.patch new file mode 100644 index 0000000..93e98ec --- /dev/null +++ b/pcp-7.0.3-pmlogmv-command-injection.patch @@ -0,0 +1,544 @@ +From dc73ec0f57 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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 +--- +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 +@@ -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 */ diff --git a/pcp-7.0.3-pmproxy-logger-auth.patch b/pcp-7.0.3-pmproxy-logger-auth.patch new file mode 100644 index 0000000..97779f9 --- /dev/null +++ b/pcp-7.0.3-pmproxy-logger-auth.patch @@ -0,0 +1,199 @@ +From 4121ae06f5 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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 </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 </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"); + diff --git a/pcp-7.0.3-pmproxy-logger-meta-network.patch b/pcp-7.0.3-pmproxy-logger-meta-network.patch new file mode 100644 index 0000000..4211c27 --- /dev/null +++ b/pcp-7.0.3-pmproxy-logger-meta-network.patch @@ -0,0 +1,179 @@ +From bf898b50ef Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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 </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 === diff --git a/pcp-7.0.3-pmproxy-rest-certreqd.patch b/pcp-7.0.3-pmproxy-rest-certreqd.patch new file mode 100644 index 0000000..daee0bb --- /dev/null +++ b/pcp-7.0.3-pmproxy-rest-certreqd.patch @@ -0,0 +1,41 @@ +From 81a9efe96d Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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; + } + diff --git a/pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch b/pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch new file mode 100644 index 0000000..4fe4b5c --- /dev/null +++ b/pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch @@ -0,0 +1,42 @@ +From be9ade9950 Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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: diff --git a/pcp-7.0.3-timezone-zoneinfo-validation.patch b/pcp-7.0.3-timezone-zoneinfo-validation.patch new file mode 100644 index 0000000..f562e74 --- /dev/null +++ b/pcp-7.0.3-timezone-zoneinfo-validation.patch @@ -0,0 +1,287 @@ +From f86c0f4cda Mon Sep 17 00:00:00 2001 +From: Nathan Scott +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) +--- +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 ++ ++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 + #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 || diff --git a/pcp-RHEL-132402.patch b/pcp-RHEL-132402.patch new file mode 100644 index 0000000..fa3936a --- /dev/null +++ b/pcp-RHEL-132402.patch @@ -0,0 +1,60 @@ +commit 082ff6beb14420c04af74f37d2ae8c1628182ae2 +Author: William Cohen +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 +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; diff --git a/pcp.spec b/pcp.spec index 8624d91..6d98656 100644 --- a/pcp.spec +++ b/pcp.spec @@ -1,13 +1,42 @@ Name: pcp -Version: 6.3.7 -Release: 1%{?dist} +Version: 7.0.3 +Release: 5.0.1%{?dist} Summary: System-level performance monitoring and performance management License: GPL-2.0-or-later AND LGPL-2.1-or-later AND CC-BY-3.0 URL: https://pcp.io Source0: https://github.com/performancecopilot/pcp/releases/pcp-%{version}.src.tar.gz - -Patch0: fix-pmdabpf-noarch-man-page-build-failure.patch +Source1: pdu-getpdu-overflow +Patch0: pcp2openmetrics-archive.patch +Patch1: pcp-RHEL-132402.patch +Patch2: pcp-7.0.3-CVE-2026-16529.patch +Patch3: pcp-7.0.3-CVE-2026-16527.patch +Patch4: pcp-7.0.3-CVE-2026-16526.patch +Patch5: pcp-7.0.3-CVE-2026-16524.patch +Patch6: pcp-7.0.3-CVE-2026-16530.patch +Patch7: pcp-7.0.3-CVE-2026-16531.patch +Patch8: pcp-7.0.3-pmlogmv-command-injection.patch +Patch9: pcp-7.0.3-pmieconf-command-injection.patch +Patch10: pcp-7.0.3-OOB-pmDecodeInstance.patch +Patch11: pcp-7.0.3-OOB-pmDecodeLabel.patch +Patch12: pcp-7.0.3-OOB-pmDecodeLogStatus.patch +Patch13: pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch +Patch14: pcp-7.0.3-OOB-pmLogLoadLabelSet.patch +Patch15: pcp-7.0.3-pducrash-oob-tests.patch +Patch16: pcp-7.0.3-pmdaroot-peer-credentials.patch +Patch17: pcp-7.0.3-timezone-zoneinfo-validation.patch +Patch18: pcp-7.0.3-pmproxy-rest-certreqd.patch +Patch19: pcp-7.0.3-pmproxy-logger-auth.patch +Patch20: pcp-7.0.3-pmproxy-logger-meta-network.patch +Patch21: pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch +Patch1011: 1011-orabug38724847-fix-nfsclient-per-op-parsing.patch +Patch1012: 1012-orabug38817053-Introduce-PCP-implementation-of-nfsiostat.patch +Patch1013: 1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch +Patch1014: 1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch +Patch1015: 1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch +Patch1016: 1016-pcp-system-tools-restore-backward-compatibility-with.patch +Patch1017: 1017-orabug39068870-adds-interval-option-in-nfsiostat.patch +Patch1018: 1018-orabug39096683-introduces-numa-maps-metrics-and-adds-numastat-process-option.patch %if 0%{?fedora} >= 40 || 0%{?rhel} >= 10 ExcludeArch: %{ix86} @@ -17,12 +46,6 @@ ExcludeArch: %{ix86} # https://bugzilla.redhat.com/show_bug.cgi?id=2043092 %undefine _package_note_flags -%if 0%{?fedora} >= 26 || 0%{?rhel} > 7 -%global __python2 python2 -%else -%global __python2 python -%endif - # UsrMerge was completed in EL 7, however the latest 'hostname' package in EL 7 contains "Provides: /bin/hostname". Likewise for /bin/ps from procps[-ng] packages. %if 0%{?rhel} >= 8 || 0%{?fedora} >= 17 %global _hostname_executable /usr/bin/hostname @@ -74,17 +97,6 @@ ExcludeArch: %{ix86} %global disable_gfs2 0 %endif -%if 0%{?fedora} >= 30 || 0%{?rhel} > 7 -%global _with_python2 --with-python=no -%global disable_python2 1 -%else -%global disable_python2 0 -%endif - -# Default for epel5 is python24, so use the (optional) python26 packages -%if 0%{?rhel} == 5 -%global default_python 26 -%endif # No python3 development environment before el8 %if 0%{?rhel} == 0 || 0%{?rhel} > 7 %global disable_python3 0 @@ -135,19 +147,8 @@ ExcludeArch: %{ix86} %global disable_bpftrace 1 %endif -# support for pmdajson -%if 0%{?rhel} == 0 || 0%{?rhel} > 6 -%if !%{disable_python2} || !%{disable_python3} -%global disable_json 0 -%else -%global disable_json 1 -%endif -%else -%global disable_json 1 -%endif - # support for pmdamongodb -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} %global disable_mongodb 0 %else %global disable_mongodb 1 @@ -155,7 +156,7 @@ ExcludeArch: %{ix86} # No mssql ODBC driver on non-x86 platforms %ifarch x86_64 -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} %global disable_mssql 0 %else %global disable_mssql 1 @@ -199,17 +200,6 @@ ExcludeArch: %{ix86} %global disable_systemd 1 %endif -# static probes, missing before el6 and on some architectures -%if 0%{?rhel} == 0 || 0%{?rhel} > 5 -%global disable_sdt 0 -%else -%ifnarch ppc ppc64 -%global disable_sdt 0 -%else -%global disable_sdt 1 -%endif -%endif - # libuv async event library %if 0%{?fedora} >= 28 || 0%{?rhel} > 7 %global disable_libuv 0 @@ -282,13 +272,6 @@ BuildRequires: procps autoconf bison flex BuildRequires: avahi-devel BuildRequires: xz-devel BuildRequires: zlib-devel -%if !%{disable_python2} -%if 0%{?default_python} != 3 -BuildRequires: python%{?default_python}-devel -%else -BuildRequires: %{__python2}-devel -%endif -%endif %if !%{disable_python3} BuildRequires: python3-devel BuildRequires: python3-setuptools @@ -306,9 +289,6 @@ BuildRequires: chan-devel HdrHistogram_c-devel %if !%{disable_perfevent} BuildRequires: libpfm-devel >= 4 %endif -%if !%{disable_sdt} -BuildRequires: systemtap-sdt-devel -%endif %if !%{disable_libuv} BuildRequires: libuv-devel >= 1.0 %endif @@ -339,7 +319,7 @@ BuildRequires: qt5-qtsvg-devel %endif # Utilities used indirectly e.g. by scripts we install -Requires: bash xz gawk sed grep coreutils diffutils findutils +Requires: bash xz zstd gawk sed grep coreutils diffutils findutils Requires: which %{_hostname_executable} %{_ps_executable} Requires: pcp-libs = %{version}-%{release} @@ -363,6 +343,9 @@ Requires: pcp-selinux = %{version}-%{release} %global _ieconfdir %{_localstatedir}/lib/pcp/config/pmieconf %global _selinuxdir %{_datadir}/selinux/packages/targeted +%global _with_multilib --enable-multilib=true +%global _with_nondebug --with-non-debug=yes + %if 0%{?fedora} >= 20 || 0%{?rhel} >= 8 %global _with_doc --with-docdir=%{_docdir}/%{name} %endif @@ -434,12 +417,6 @@ Requires: pcp-selinux = %{version}-%{release} %global _with_bpftrace --with-pmdabpftrace=yes %endif -%if %{disable_json} -%global _with_json --with-pmdajson=no -%else -%global _with_json --with-pmdajson=yes -%endif - %if %{disable_mongodb} %global _with_mongodb --with-pmdamongodb=no %else @@ -571,81 +548,12 @@ Requires: pcp = %{version}-%{release} Requires: pcp-libs = %{version}-%{release} Requires: pcp-libs-devel = %{version}-%{release} Requires: pcp-devel = %{version}-%{release} -Obsoletes: pcp-gui-testsuite < 3.9.5 -# The following are inherited from pcp-collector and pcp-monitor, -# both of which are now obsoleted by the base pcp package -Requires: pcp-pmda-activemq pcp-pmda-bonding pcp-pmda-dbping pcp-pmda-ds389 pcp-pmda-ds389log -Requires: pcp-pmda-elasticsearch pcp-pmda-gpfs pcp-pmda-gpsd pcp-pmda-lustre -Requires: pcp-pmda-memcache pcp-pmda-named pcp-pmda-netfilter pcp-pmda-news -Requires: pcp-pmda-nginx pcp-pmda-nfsclient pcp-pmda-pdns pcp-pmda-postfix pcp-pmda-postgresql pcp-pmda-oracle -Requires: pcp-pmda-samba pcp-pmda-slurm pcp-pmda-zimbra -Requires: pcp-pmda-dm pcp-pmda-apache -Requires: pcp-pmda-bash pcp-pmda-cisco pcp-pmda-mailq pcp-pmda-mounts -Requires: pcp-pmda-nvidia-gpu pcp-pmda-roomtemp pcp-pmda-sendmail pcp-pmda-shping pcp-pmda-smart pcp-pmda-farm -Requires: pcp-pmda-hacluster pcp-pmda-lustrecomm pcp-pmda-logger pcp-pmda-denki pcp-pmda-docker pcp-pmda-bind2 -Requires: pcp-pmda-sockets pcp-pmda-podman -%if !%{disable_gfs2} -Requires: pcp-pmda-gfs2 -%endif -%if !%{disable_statsd} -Requires: pcp-pmda-statsd -%endif -%if !%{disable_nutcracker} -Requires: pcp-pmda-nutcracker -%endif -%if !%{disable_bcc} -Requires: pcp-pmda-bcc -%endif -%if !%{disable_bpf} -Requires: pcp-pmda-bpf -%endif -%if !%{disable_bpftrace} -Requires: pcp-pmda-bpftrace -%endif -%if !%{disable_python2} || !%{disable_python3} -Requires: pcp-geolocate pcp-export-pcp2openmetrics pcp-export-pcp2json -Requires: pcp-export-pcp2spark pcp-export-pcp2xml pcp-export-pcp2zabbix -Requires: pcp-pmda-gluster pcp-pmda-zswap pcp-pmda-unbound pcp-pmda-mic -Requires: pcp-pmda-libvirt pcp-pmda-lio pcp-pmda-openmetrics pcp-pmda-haproxy -Requires: pcp-pmda-lmsensors pcp-pmda-netcheck pcp-pmda-rabbitmq pcp-pmda-uwsgi -Requires: pcp-pmda-openvswitch -%endif -%if !%{disable_mongodb} -Requires: pcp-pmda-mongodb -%endif -%if !%{disable_mssql} -Requires: pcp-pmda-mssql -%endif -%if !%{disable_mysql} -Requires: pcp-pmda-mysql -%endif -%if !%{disable_snmp} -Requires: pcp-pmda-snmp -%endif -%if !%{disable_json} -Requires: pcp-pmda-json -%endif -%if !%{disable_resctrl} -Requires: pcp-pmda-resctrl -%endif -Requires: pcp-pmda-summary pcp-pmda-trace pcp-pmda-weblog -%if !%{disable_amdgpu} -Requires: pcp-pmda-amdgpu -%endif -Requires: pcp-system-tools -%if !%{disable_qt} -Requires: pcp-gui -%endif Requires: bc gcc gzip bzip2 Requires: redhat-rpm-config %if !%{disable_selinux} Requires: selinux-policy-devel Requires: selinux-policy-targeted -%if 0%{?rhel} == 5 Requires: setools -%else -Requires: setools-console -%endif %endif %description testsuite @@ -772,6 +680,7 @@ Summary: Performance Co-Pilot tools for importing ganglia data into PCP archive URL: https://pcp.io Requires: pcp-libs = %{version}-%{release} Requires: perl-PCP-LogImport = %{version}-%{release} +BuildRequires: rrdtool-perl %description import-ganglia2pcp Performance Co-Pilot (PCP) front-end tools for importing ganglia data @@ -803,7 +712,37 @@ Requires: pcp-libs = %{version}-%{release} Performance Co-Pilot (PCP) module for exporting metrics from PCP to Zabbix via the Zabbix agent - see zbxpcp(3) for further details. -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} +# +# pcp-import-benchmarks +# +%package import-benchmarks +License: LGPL-2.1-or-later +Summary: Performance Co-Pilot tools importing benchmark results into PCP archive logs +URL: https://pcp.io +Requires: pcp-libs = %{version}-%{release} +Requires: python3-pcp = %{version}-%{release} +Obsoletes: pcp-import-guidellm2pcp <= 7.0.2 +Provides: pcp-import-guidellm2pcp = %{version}-%{release} + +%description import-benchmarks +Performance Co-Pilot (PCP) front-end tools for importing JSON benchmark +results files into PCP archives for replay with PCP analysis tools. + +# +# pcp-import-pmseries +# +%package import-pmseries +License: LGPL-2.1-or-later +Summary: Performance Co-Pilot tools importing PCP archives for pmseries queries +URL: https://pcp.io +Requires: pcp-libs >= %{version}-%{release} +Requires: python3-pcp = %{version}-%{release} + +%description import-pmseries +Performance Co-Pilot (PCP) tools for importing PCP archives into Valkey +or Redis for fast, scalable time series access via pmseries(1) queries. + # # pcp-geolocate # @@ -812,11 +751,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot geographical location metric labels URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %description geolocate Performance Co-Pilot (PCP) tools that automatically apply metric labels @@ -831,15 +766,9 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics to ElasticSearch URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} Requires: python3-requests BuildRequires: python3-requests -%else -Requires: %{__python2}-pcp = %{version}-%{release} -Requires: %{__python2}-requests -BuildRequires: %{__python2}-requests -%endif %description export-pcp2elasticsearch Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -854,11 +783,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics to Graphite URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %description export-pcp2graphite Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -871,13 +796,8 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics to InfluxDB URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} Requires: python3-requests -%else -Requires: %{__python2}-pcp = %{version}-%{release} -Requires: %{__python2}-requests -%endif %description export-pcp2influxdb Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -891,11 +811,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics in JSON format URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %description export-pcp2json Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -909,16 +825,27 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics in OpenMetrics format URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %description export-pcp2openmetrics Performance Co-Pilot (PCP) front-end tools for exporting metric values in OpenMetrics (https://openmetrics.io/) format. +# +# pcp-export-pcp2opentelemetry +# +%package export-pcp2opentelemetry +License: GPL-2.0-or-later +Summary: Performance Co-Pilot tools for exporting PCP metrics in OpenTelemetry format +URL: https://pcp.io +Requires: pcp-libs >= %{version}-%{release} +%if !%{disable_python3} +Requires: python3-pcp = %{version}-%{release} + +%description export-pcp2opentelemetry +Performance Co-Pilot (PCP) front-end tools for exporting metric values +in OpenTelemetry (https://opentelemetry.io/) format. + # # pcp-export-pcp2spark # @@ -927,11 +854,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics to Apache Spark URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %description export-pcp2spark Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -947,15 +870,9 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics to Apache Arrow URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} Requires: python3-pyarrow BuildRequires: python3-pyarrow -%else -Requires: %{__python2}-pcp = %{version}-%{release} -Requires: %{__python2}-pyarrow -BuildRequires: %{__python2}-pyarrow -%endif %description export-pcp2arrow Performance Co-Pilot (PCP) front-end tool for exporting metric values @@ -971,15 +888,9 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics to Excel URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} Requires: python3-openpyxl BuildRequires: python3-openpyxl -%else -Requires: %{__python2}-pcp = %{version}-%{release} -Requires: %{__python2}-openpyxl -BuildRequires: %{__python2}-openpyxl -%endif %description export-pcp2xlsx Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -994,11 +905,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics in XML format URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %description export-pcp2xml Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -1012,11 +919,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot tools for exporting PCP metrics to Zabbix URL: https://pcp.io Requires: pcp-libs >= %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %description export-pcp2zabbix Performance Co-Pilot (PCP) front-end tools for exporting metric values @@ -1621,7 +1524,24 @@ extracting performance metrics from bpftrace scripts. # end pcp-pmda-bpftrace %endif -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} +# +# pcp-pmda-hdb +# +%package pmda-hdb +License: GPL-3.0-or-later +Summary: Performance Co-Pilot (PCP) metrics for SAP HANA databases +URL: https://pcp.io +Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} +Requires: python3-pcp +%if 0%{?rhel} > 8 || 0%{?fedora} > 0 +Requires: python3-packaging +%endif +%description pmda-hdb +This package provides a PMDA to export metric values about a SAP HANA +database (https://www.sap.com/products/data-cloud/hana.html). +#end pcp-pmda-hdb + # # pcp-pmda-gluster # @@ -1630,11 +1550,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for the Gluster filesystem URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-gluster This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about the gluster filesystem. @@ -1648,11 +1564,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for NFS Clients URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-nfsclient This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics for NFS Clients. @@ -1666,15 +1578,9 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for PostgreSQL URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp Requires: python3-psycopg2 BuildRequires: python3-psycopg2 -%else -Requires: %{__python2}-pcp -Requires: %{__python2}-psycopg2 -BuildRequires: %{__python2}-psycopg2 -%endif %description pmda-postgresql This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about the PostgreSQL database. @@ -1688,11 +1594,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for compressed swap URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-zswap This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about compressed swap. @@ -1706,11 +1608,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for the Unbound DNS Resolver URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-unbound This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about the Unbound DNS Resolver. @@ -1724,11 +1622,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for Intel MIC cards URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-mic This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about Intel MIC cards. @@ -1742,11 +1636,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for HAProxy URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-haproxy This package contains the PCP Performance Metrics Domain Agent (PMDA) for extracting performance metrics from HAProxy over the HAProxy stats socket. @@ -1760,22 +1650,9 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics from virtual machines URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp Requires: libvirt-python3 python3-lxml BuildRequires: libvirt-python3 python3-lxml -%else -%if 0%{?rhel} == 0 || 0%{?fedora} >= 27 -Requires: %{__python2}-pcp -Requires: %{__python2}-libvirt %{__python2}-lxml -BuildRequires: %{__python2}-libvirt %{__python2}-lxml -%endif -%if 0%{?rhel} > 5 -Requires: %{__python2}-pcp -Requires: libvirt-%{__python2} %{__python2}-lxml -BuildRequires: libvirt-%{__python2} %{__python2}-lxml -%endif -%endif %description pmda-libvirt This package contains the PCP Performance Metrics Domain Agent (PMDA) for extracting virtualisation statistics from libvirt about behaviour of guest @@ -1790,11 +1667,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for Elasticsearch URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-elasticsearch This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about Elasticsearch. @@ -1808,11 +1681,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for Open vSwitch URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-openvswitch This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics from Open vSwitch. @@ -1826,11 +1695,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for RabbitMQ queues URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-rabbitmq This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics about RabbitMQ message queues. @@ -1844,11 +1709,7 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics from uWSGI servers URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-uwsgi This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics from uWSGI servers. @@ -1862,15 +1723,9 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for the LIO subsystem URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp Requires: python3-rtslib BuildRequires: python3-rtslib -%else -Requires: %{__python2}-pcp -Requires: %{__python2}-rtslib -BuildRequires: %{__python2}-rtslib -%endif %description pmda-lio This package provides a PMDA to gather performance metrics from the kernels iSCSI target interface (LIO). The metrics are stored by LIO within the Linux @@ -1887,24 +1742,31 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics from OpenMetrics endpoints URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp Requires: python3-requests BuildRequires: python3-requests -%else -Requires: %{__python2}-pcp -Requires: %{__python2}-requests -BuildRequires: %{__python2}-requests -%endif -Obsoletes: pcp-pmda-prometheus < 5.0.0 -Provides: pcp-pmda-prometheus < 5.0.0 -Obsoletes: pcp-pmda-vmware < 5.3.5 - %description pmda-openmetrics This package contains the PCP Performance Metrics Domain Agent (PMDA) for extracting metrics from OpenMetrics (https://openmetrics.io/) endpoints. #end pcp-pmda-openmetrics +# +# pcp-pmda-opentelemetry +# +%package pmda-opentelemetry +License: GPL-2.0-or-later +Summary: Performance Co-Pilot (PCP) metrics from OpenTelemetry endpoints +URL: https://pcp.io +Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} +Requires: python3-pcp +Requires: python3-requests +BuildRequires: python3-requests +Obsoletes: pcp-pmda-json <= 7.0.0 +%description pmda-opentelemetry +This package contains the PCP Performance Metrics Domain Agent (PMDA) for +extracting metrics from OpenTelemetry (https://opentelemetry.io/) endpoints. +#end pcp-pmda-opentelemetry + # # pcp-pmda-lmsensors # @@ -1914,11 +1776,7 @@ Summary: Performance Co-Pilot (PCP) metrics for hardware sensors URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} Requires: lm_sensors -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif # rewritten in python, so there is no longer a debuginfo package Obsoletes: pcp-pmda-lmsensors-debuginfo < 4.2.0 %description pmda-lmsensors @@ -1934,15 +1792,25 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for simple network checks URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp -%else -Requires: %{__python2}-pcp -%endif %description pmda-netcheck This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics from simple network checks. # end pcp-pmda-netcheck + +# +# pcp-pmda-rocestat +# +%package pmda-rocestat +License: GPL-2.0-or-later +Summary: Performance Co-Pilot (PCP) metrics for nVidia RoCE devices +URL: https://pcp.io +Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} +Requires: python3-pcp +%description pmda-rocestat +This package contains the PCP Performance Metrics Domain Agent (PMDA) for +collecting statistics for nVidia RDMA over Converged Ethernet (RoCE) devices. +# end pcp-pmda-rocestat %endif %if !%{disable_mongodb} @@ -1954,19 +1822,11 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for MongoDB URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp %if 0%{?rhel} == 0 Requires: python3-pymongo BuildRequires: python3-pymongo %endif -%else -Requires: %{__python2}-pcp -%if 0%{?rhel} == 0 -Requires: %{__python2}-pymongo -BuildRequires: %{__python2}-pymongo -%endif -%endif %description pmda-mongodb This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics from MongoDB. @@ -1982,49 +1842,17 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) metrics for Microsoft SQL Server URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} Requires: python3-pcp %if 0%{?rhel} == 0 || 0%{?rhel} >= 9 Requires: python3-pyodbc BuildRequires: python3-pyodbc %endif -%else -Requires: %{__python2}-pcp -%if 0%{?rhel} == 0 -Requires: %{__python2}-pyodbc -BuildRequires: %{__python2}-pyodbc -%endif -%endif %description pmda-mssql This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting metrics from Microsoft SQL Server. # end pcp-pmda-mssql %endif -%if !%{disable_json} -# -# pcp-pmda-json -# -%package pmda-json -License: GPL-2.0-or-later -Summary: Performance Co-Pilot (PCP) metrics for JSON data -URL: https://pcp.io -Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python3} -Requires: python3-pcp -Requires: python3-jsonpointer python3-six -BuildRequires: python3-jsonpointer python3-six -%else -Requires: %{__python2}-pcp -Requires: %{__python2}-jsonpointer %{__python2}-six -BuildRequires: %{__python2}-jsonpointer %{__python2}-six -%endif -%description pmda-json -This package contains the PCP Performance Metrics Domain Agent (PMDA) for -collecting metrics output in JSON. -# end pcp-pmda-json -%endif - # # C pmdas # pcp-pmda-apache @@ -2339,40 +2167,22 @@ Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} Requires: pcp-system-tools = %{version}-%{release} Requires: pcp-doc = %{version}-%{release} Requires: pcp-pmda-dm = %{version}-%{release} -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_bpf} +Requires: pcp-pmda-bpf = %{version}-%{release} +%endif +%if !%{disable_bpftrace} +Requires: pcp-pmda-bpftrace = %{version}-%{release} +%endif +%if !%{disable_python3} Requires: pcp-pmda-nfsclient = %{version}-%{release} Requires: pcp-pmda-openmetrics = %{version}-%{release} +Requires: pcp-pmda-opentelemetry = %{version}-%{release} %endif %description zeroconf This package contains configuration tweaks and files to increase metrics gathering frequency, several extended pmlogger configurations, as well as automated pmie diagnosis, alerting and self-healing for the localhost. -%if !%{disable_python2} -# -# python2-pcp. This is the PCP library bindings for python. -# -%package -n %{__python2}-pcp -License: GPL-2.0-or-later -Summary: Performance Co-Pilot (PCP) Python bindings and documentation -URL: https://pcp.io -Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if 0%{?fedora} >= 26 || 0%{?rhel} > 7 -# on these platforms, python2-pcp replaces python-pcp -Obsoletes: python-pcp < %{version} -%endif -%if 0%{?rhel} == 5 -Requires: python%{default_python} -%else -Requires: %{__python2} -%endif - -%description -n %{__python2}-pcp -This python PCP module contains the language bindings for -Performance Metric API (PMAPI) monitor tools and Performance -Metric Domain Agent (PMDA) collector tools written in Python. -%endif - %if !%{disable_python3} # # python3-pcp. This is the PCP library bindings for python3. @@ -2398,12 +2208,8 @@ License: GPL-2.0-or-later Summary: Performance Co-Pilot (PCP) System and Monitoring Tools URL: https://pcp.io Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} -%if !%{disable_python2} || !%{disable_python3} %if !%{disable_python3} Requires: python3-pcp = %{version}-%{release} -%else -Requires: %{__python2}-pcp = %{version}-%{release} -%endif %if !%{disable_dstat} # https://fedoraproject.org/wiki/Packaging:Guidelines "Renaming/Replacing Existing Packages" Provides: dstat = %{version}-%{release} @@ -2486,6 +2292,7 @@ updated policy package. %prep %autosetup -p1 +install -D -m 644 %{SOURCE1} qa/pdudata/pdu-getpdu-overflow %build # the buildsubdir macro gets defined in %%setup and is apparently only available in the next step (i.e. the %%build step) @@ -2495,10 +2302,7 @@ updated policy package. _build=`echo %{release} | sed -e 's/\..*$//'` sed -i "/PACKAGE_BUILD/s/=[0-9]*/=$_build/" VERSION.pcp -%if !%{disable_python2} && 0%{?default_python} != 3 -export PYTHON=python%{?default_python} -%endif -%configure %{?_with_initd} %{?_with_doc} %{?_with_dstat} %{?_with_ib} %{?_with_gfs2} %{?_with_statsd} %{?_with_perfevent} %{?_with_bcc} %{?_with_bpf} %{?_with_bpftrace} %{?_with_json} %{?_with_mongodb} %{?_with_mysql} %{?_with_snmp} %{?_with_nutcracker} %{?_with_python2} +%configure %{?_with_multilib} %{?_with_nondebug} %{?_with_initd} %{?_with_doc} %{?_with_dstat} %{?_with_ib} %{?_with_gfs2} %{?_with_statsd} %{?_with_perfevent} %{?_with_bcc} %{?_with_bpf} %{?_with_bpftrace} %{?_with_mongodb} %{?_with_mysql} %{?_with_snmp} %{?_with_nutcracker} make %{?_smp_mflags} default_pcp %install @@ -2542,8 +2346,11 @@ desktop-file-validate $RPM_BUILD_ROOT/%{_datadir}/applications/pmchart.desktop %endif %if 0%{?rhel} || 0%{?fedora} -# Fedora and RHEL default local only access for pmcd and pmlogger -sed -i -e '/^# .*_LOCAL=1/s/^# //' $RPM_BUILD_ROOT/%{_sysconfdir}/sysconfig/{pmcd,pmlogger} +# Fedora and RHEL default local only access for pmcd, pmproxy and pmlogger +if [ "$1" -eq 1 ] +then + sed -i -e '/^# .*_LOCAL=1/s/^# //' $RPM_BUILD_ROOT/%{_sysconfdir}/sysconfig/{pmcd,pmproxy,pmlogger} +fi %endif # default chkconfig off (all RPM platforms) @@ -2642,12 +2449,15 @@ basic_manifest | grep -E -e 'pmiostat|pmrep|dstat|htop|pcp2csv' \ -e 'pcp-tapestat|pcp-uptime|pcp-verify|pcp-xsos' | \ cull 'selinux|pmlogconf|pmieconf|pmrepconf' >pcp-system-tools-files basic_manifest | keep 'geolocate' >pcp-geolocate-files +basic_manifest | keep 'pmseries_import' >pcp-import-pmseries-files basic_manifest | keep 'sar2pcp' >pcp-import-sar2pcp-files basic_manifest | keep 'iostat2pcp' >pcp-import-iostat2pcp-files basic_manifest | keep 'sheet2pcp' >pcp-import-sheet2pcp-files basic_manifest | keep 'mrtg2pcp' >pcp-import-mrtg2pcp-files basic_manifest | keep 'ganglia2pcp' >pcp-import-ganglia2pcp-files basic_manifest | keep 'collectl2pcp' >pcp-import-collectl2pcp-files +basic_manifest | keep 'guidellm2pcp' >pcp-import-benchmarks-files +basic_manifest | keep 'vllmbench2pcp' >pcp-import-benchmarks-files basic_manifest | keep 'pcp2arrow' >pcp-export-pcp2arrow-files basic_manifest | keep 'pcp2elasticsearch' >pcp-export-pcp2elasticsearch-files basic_manifest | keep 'pcp2influxdb' >pcp-export-pcp2influxdb-files @@ -2655,6 +2465,7 @@ basic_manifest | keep 'pcp2xlsx' >pcp-export-pcp2xlsx-files basic_manifest | keep 'pcp2graphite' >pcp-export-pcp2graphite-files basic_manifest | keep 'pcp2json' >pcp-export-pcp2json-files basic_manifest | keep 'pcp2openmetrics' >pcp-export-pcp2openmetrics-files +basic_manifest | keep 'pcp2opentelemetry' >pcp-export-pcp2opentelemetry-files basic_manifest | keep 'pcp2spark' >pcp-export-pcp2spark-files basic_manifest | keep 'pcp2xml' >pcp-export-pcp2xml-files basic_manifest | keep 'pcp2zabbix' >pcp-export-pcp2zabbix-files @@ -2684,8 +2495,8 @@ basic_manifest | keep '(etc/pcp|pmdas)/gpfs(/|$)' >pcp-pmda-gpfs-files basic_manifest | keep '(etc/pcp|pmdas)/gpsd(/|$)' >pcp-pmda-gpsd-files basic_manifest | keep '(etc/pcp|pmdas)/hacluster(/|$)' >pcp-pmda-hacluster-files basic_manifest | keep '(etc/pcp|pmdas)/haproxy(/|$)' >pcp-pmda-haproxy-files +basic_manifest | keep '(etc/pcp|pmdas)/hdb(/|$)' >pcp-pmda-hdb-files basic_manifest | keep '(etc/pcp|pmdas)/infiniband(/|$)' >pcp-pmda-infiniband-files -basic_manifest | keep '(etc/pcp|pmdas)/json(/|$)' >pcp-pmda-json-files basic_manifest | keep '(etc/pcp|pmdas)/libvirt(/|$)' >pcp-pmda-libvirt-files basic_manifest | keep '(etc/pcp|pmdas)/lio(/|$)' >pcp-pmda-lio-files basic_manifest | keep '(etc/pcp|pmdas)/lmsensors(/|$)' >pcp-pmda-lmsensors-files @@ -2708,6 +2519,7 @@ basic_manifest | keep '(etc/pcp|pmdas)/nginx(/|$)' >pcp-pmda-nginx-files basic_manifest | keep '(etc/pcp|pmdas)/nutcracker(/|$)' >pcp-pmda-nutcracker-files basic_manifest | keep '(etc/pcp|pmdas)/nvidia(/|$)' >pcp-pmda-nvidia-files basic_manifest | keep '(etc/pcp|pmdas)/openmetrics(/|$)' >pcp-pmda-openmetrics-files +basic_manifest | keep '(etc/pcp|pmdas)/opentelemetry(/|$)' >pcp-pmda-opentelemetry-files basic_manifest | keep '(etc/pcp|pmdas|pmieconf)/openvswitch(/|$)' >pcp-pmda-openvswitch-files basic_manifest | keep '(etc/pcp|pmdas)/oracle(/|$)' >pcp-pmda-oracle-files basic_manifest | keep '(etc/pcp|pmdas)/pdns(/|$)' >pcp-pmda-pdns-files @@ -2718,6 +2530,7 @@ basic_manifest | keep '(etc/pcp|pmdas)/postgresql(/|$)' >pcp-pmda-postgresql-fil basic_manifest | keep '(etc/pcp|pmdas)/rabbitmq(/|$)' >pcp-pmda-rabbitmq-files basic_manifest | keep '(etc/pcp|pmdas)/redis(/|$)' >pcp-pmda-redis-files basic_manifest | keep '(etc/pcp|pmdas)/resctrl(/|$)|sys-fs-resctrl' >pcp-pmda-resctrl-files +basic_manifest | keep '(etc/pcp|pmdas)/rocestat(/|$)' >pcp-pmda-rocestat-files basic_manifest | keep '(etc/pcp|pmdas)/roomtemp(/|$)' >pcp-pmda-roomtemp-files basic_manifest | keep '(etc/pcp|pmdas)/rpm(/|$)' >pcp-pmda-rpm-files basic_manifest | keep '(etc/pcp|pmdas)/rsyslog(/|$)' >pcp-pmda-rsyslog-files @@ -2747,16 +2560,15 @@ for pmda_package in \ elasticsearch \ farm \ gfs2 gluster gpfs gpsd \ - hacluster haproxy \ + hacluster haproxy hdb \ infiniband \ - json \ libvirt lio lmsensors logger lustre lustrecomm \ mailq memcache mic mounts mongodb mssql mysql \ named netcheck netfilter news nfsclient nginx \ nutcracker nvidia \ - openmetrics openvswitch oracle \ + openmetrics opentelemetry openvswitch oracle \ pdns perfevent podman postfix postgresql \ - rabbitmq redis resctrl roomtemp rpm rsyslog \ + rabbitmq redis resctrl rocestat roomtemp rpm rsyslog \ samba sendmail shping slurm smart snmp \ sockets statsd summary systemd \ unbound uwsgi \ @@ -2768,6 +2580,7 @@ do \ done for import_package in \ + pmseries benchmarks \ collectl2pcp iostat2pcp ganglia2pcp mrtg2pcp sar2pcp sheet2pcp ; \ do \ import_packages="$import_packages pcp-import-$import_package"; \ @@ -2775,7 +2588,8 @@ done for export_package in \ pcp2arrow pcp2elasticsearch pcp2graphite pcp2influxdb pcp2json \ - pcp2openmetrics pcp2spark pcp2xlsx pcp2xml pcp2zabbix zabbix-agent ; \ + pcp2openmetrics pcp2spark pcp2xlsx pcp2xml pcp2opentelemetry \ + pcp2zabbix zabbix-agent ; \ do \ export_packages="$export_packages pcp-export-$export_package"; \ done @@ -2901,6 +2715,8 @@ chown -R pcpqa:pcpqa %{_testsdir} 2>/dev/null exit 0 %post testsuite +PCP_PMDAS_DIR=%{_pmdasdir} +PCP_PMCDCONF_PATH=%{_confdir}/pmcd/pmcd.conf %if !%{disable_selinux} PCP_SELINUX_DIR=%{_selinuxdir} semodule -r pcpqa >/dev/null 2>&1 || true @@ -2908,6 +2724,14 @@ semodule -r pcpqa >/dev/null 2>&1 || true %selinux_relabel_post -s targeted %endif chown -R pcpqa:pcpqa %{_testsdir} 2>/dev/null +# auto-install important PMDAs for testing (if not present already) +needinstall='sample simple' +for PMDA in $needinstall ; do + if ! grep -q "$PMDA/pmda$PMDA" "$PCP_PMCDCONF_PATH" + then + %{install_file "$PCP_PMDAS_DIR/$PMDA" .NeedInstall} + fi +done %if 0%{?rhel} %if !%{disable_systemd} systemctl restart pcp-reboot-init pmcd pmlogger >/dev/null 2>&1 @@ -2964,11 +2788,6 @@ exit 0 %{pmda_remove "$1" "statsd"} %endif -%if !%{disable_json} -%preun pmda-json -%{pmda_remove "$1" "json"} -%endif - %preun pmda-nginx %{pmda_remove "$1" "nginx"} @@ -3039,6 +2858,9 @@ exit 0 %preun pmda-openmetrics %{pmda_remove "$1" "openmetrics"} +%preun pmda-opentelemetry +%{pmda_remove "$1" "opentelemetry"} + %preun pmda-lustre %{pmda_remove "$1" "lustre"} @@ -3098,7 +2920,10 @@ exit 0 %{pmda_remove "$1" "bpftrace"} %endif -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} +%preun pmda-hdb +%{pmda_remove "$1" "hdb"} + %preun pmda-gluster %{pmda_remove "$1" "gluster"} @@ -3133,6 +2958,9 @@ exit 0 %preun pmda-netcheck %{pmda_remove "$1" "netcheck"} +%preun pmda-rocestat +%{pmda_remove "$1" "rocestat"} + %endif %preun pmda-apache @@ -3234,14 +3062,22 @@ PCP_PMDAS_DIR=%{_pmdasdir} PCP_SYSCONFIG_DIR=%{_sysconfdir}/sysconfig PCP_PMCDCONF_PATH=%{_confdir}/pmcd/pmcd.conf # auto-install important PMDAs for RH Support (if not present already) -for PMDA in dm nfsclient openmetrics ; do +needinstall='dm' +%if !%{disable_python3} +needinstall="$needinstall nfsclient openmetrics opentelemetry" +%endif +for PMDA in $needinstall ; do if ! grep -q "$PMDA/pmda$PMDA" "$PCP_PMCDCONF_PATH" then %{install_file "$PCP_PMDAS_DIR/$PMDA" .NeedInstall} fi done # managed via /usr/lib/systemd/system-preset/90-default.preset nowadays: -%if 0%{?rhel} > 0 && 0%{?rhel} < 10 +%if 0%{?fedora} > 40 || 0%{?rhel} > 9 + for s in pmcd pmlogger pmie; do + systemctl --quiet is-enabled $s && systemctl --quiet restart $s || true + done +%else # old-school methods follow %if !%{disable_systemd} systemctl restart pmcd pmlogger pmie >/dev/null 2>&1 systemctl enable pmcd pmlogger pmie >/dev/null 2>&1 @@ -3255,6 +3091,26 @@ done %endif %endif +%post pmda-rocestat +PCP_PMDAS_DIR=%{_pmdasdir} +PCP_SYSCONFIG_DIR=%{_sysconfdir}/sysconfig +PCP_PMCDCONF_PATH=%{_confdir}/pmcd/pmcd.conf + +# Auto-install rocestat PMDA if not already in pmcd.conf +if ! grep -q "rocestat/pmdarocestat" "$PCP_PMCDCONF_PATH"; then + if [ ! -d /sys/class/infiniband ]; then + if ! lsmod | grep -q '^ib_core'; then + echo "Skipping install for PMDA Rocestat (IB kernel modules are not loaded)" >&2 + else + echo "Skipping install for PMDA Rocestat (No IB devices detected)" >&2 + fi + else + cd "$PCP_PMDAS_DIR/rocestat" && \ + chmod +x Install && \ + ./Install < /dev/null + fi +fi + %post PCP_PMNS_DIR=%{_pmnsdir} PCP_LOG_DIR=%{_logsdir} @@ -3364,7 +3220,7 @@ fi %files pmda-nutcracker -f pcp-pmda-nutcracker-files.rpm %endif -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} %files pmda-elasticsearch -f pcp-pmda-elasticsearch-files.rpm %endif @@ -3420,7 +3276,7 @@ fi %files pmda-postfix -f pcp-pmda-postfix-files.rpm %endif -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} %files pmda-postgresql -f pcp-pmda-postgresql-files.rpm %endif @@ -3446,7 +3302,7 @@ fi %files pmda-bpftrace -f pcp-pmda-bpftrace-files.rpm %endif -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} %files geolocate -f pcp-geolocate-files.rpm %files pmda-gluster -f pcp-pmda-gluster-files.rpm @@ -3477,6 +3333,8 @@ fi %files pmda-rabbitmq -f pcp-pmda-rabbitmq-files.rpm +%files pmda-rocestat -f pcp-pmda-rocestat-files.rpm + %files pmda-uwsgi -f pcp-pmda-uwsgi-files.rpm %files export-pcp2graphite -f pcp-export-pcp2graphite-files.rpm @@ -3485,6 +3343,8 @@ fi %files export-pcp2openmetrics -f pcp-export-pcp2openmetrics-files.rpm +%files export-pcp2opentelemetry -f pcp-export-pcp2opentelemetry-files.rpm + %files export-pcp2spark -f pcp-export-pcp2spark-files.rpm %files export-pcp2xml -f pcp-export-pcp2xml-files.rpm @@ -3492,11 +3352,11 @@ fi %files export-pcp2zabbix -f pcp-export-pcp2zabbix-files.rpm %endif -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} %files export-pcp2elasticsearch -f pcp-export-pcp2elasticsearch-files.rpm %endif -%if !%{disable_python2} || !%{disable_python3} +%if !%{disable_python3} %files export-pcp2influxdb -f pcp-export-pcp2influxdb-files.rpm %endif @@ -3510,11 +3370,9 @@ fi %files export-zabbix-agent -f pcp-export-zabbix-agent-files.rpm -%if !%{disable_json} -%files pmda-json -f pcp-pmda-json-files.rpm -%endif +%if !%{disable_python3} +%files pmda-hdb -f pcp-pmda-hdb-files.rpm -%if !%{disable_python2} || !%{disable_python3} %files pmda-libvirt -f pcp-pmda-libvirt-files.rpm %files pmda-lio -f pcp-pmda-lio-files.rpm @@ -3522,6 +3380,9 @@ fi %files pmda-openmetrics -f pcp-pmda-openmetrics-files.rpm %endif +%files pmda-opentelemetry -f pcp-pmda-opentelemetry-files.rpm +%endif + %if !%{disable_amdgpu} %files pmda-amdgpu -f pcp-pmda-amdgpu-files.rpm %endif @@ -3574,6 +3435,12 @@ fi %files pmda-weblog -f pcp-pmda-weblog-files.rpm +%if !%{disable_python3} +%files import-pmseries -f pcp-import-pmseries-files.rpm + +%files import-benchmarks -f pcp-import-benchmarks-files.rpm +%endif + %if !%{disable_perl} %files import-sar2pcp -f pcp-import-sar2pcp-files.rpm @@ -3599,10 +3466,6 @@ fi %files -n perl-PCP-LogSummary -f perl-pcp-logsummary.list %endif -%if !%{disable_python2} -%files -n %{__python2}-pcp -f python-pcp.list.rpm -%endif - %if !%{disable_python3} %files -n python3-pcp -f python3-pcp.list.rpm %endif @@ -3612,6 +3475,61 @@ fi %files zeroconf -f pcp-zeroconf-files.rpm %changelog +* Mon Aug 17 2026 EL Errata - 7.0.3-5.0.1 +- Fix unterminated backtick in pmlogger_janitor fix not to terminate unauthorized process patch + [Orabug: 39270658] +- Adds interval option support in PCP nfsiostat tool [Orabug: 39068870] +- Introduces proc.numa_maps metrics in linux_proc PMDA [Orabug: 39096683] +- adds numastat process option in the tool +- Added support for size based cleanup for pcp archives [Orabug: 38757778] +- Implement sorting option in pcp ps based on %cpu, %mem [Orabug: 38719615] +- Fixed pmlogger incorrectly attempts to terminate +- unauthorized process [Orabug: 38598244] +- Merges new PCP nfsiostat parser in OL [Orabug: 38817053] +- pmdanfsclient: fix regex to correctly parse NFS op stats [Orabug: 38724847] +- pmda/rocestat: pmda/rocestat: skip installation when IB is absent [Orabug: 38595797] + +* Fri Aug 14 2026 Jan Kuřík - 7.0.3-5 +- Fix CVE-2026-16530: __pmLogLoadInDom OOB pointer dereference (RHEL-213746) +- Fix CVE-2026-16531: pmproxy logger servlet path traversal (RHEL-213756) +- Backport remaining PCP security hardening fixes from private-pcp +- pmieconf and pmlogmv command injection hardening (CWE-78) +- libpcp PDU decode OOB read and overflow guards (CWE-125/190/195) +- timezone and zoneinfo string validation +- pmdaroot peer credential verification (CWE-403) +- pmproxy REST CERT_REQD enforcement and logger servlet authentication + +* Fri Aug 14 2026 Jan Kuřík - 7.0.3-4 +- Fix CVE-2026-16524: linux_sockets PMDA command injection (RHEL-213659) +- Fix CVE-2026-16526: FD_CLOEXEC privilege escalation via pmdaroot (RHEL-213695) +- Fix CVE-2026-16527: pmproxy unauthenticated /store access (RHEL-213721) +- Fix CVE-2026-16529: __pmGetPDU signed integer overflow DoS (RHEL-213732) + +* Thu Feb 19 2026 William Cohen - 7.0.3-3 +- Update selinux polices for rocestat and nvidia pmdas (RHEL-132402, RHEL-134388, RHEL-133519) + +* Wed Feb 4 2026 Lauren Chilton - 7.0.3-2 +- Fix pcp2openmetrics archive metric querying (RHEL-114006) + +* Wed Nov 12 2025 William Cohen - 7.0.3-1 +- Update to latest stable version of PCP (RHEL-124897) + +* Fri Jun 27 2025 Nathan Scott - 6.3.7-5 +- Make pcp-zeroconf start enabled services once again (RHEL-93182) +- Fix python PMDA wrapper handling of list objects +- Improve tmpfiles.d handling of empty directories +- Backport some more fixes to the OpenMetrics PMDA +- Fix a multilib regression in PCP header files + +* Wed Apr 30 2025 Lauren Chilton - 6.3.7-4 +- Backport metric removal for pmdaopenmetrics + +* Tue Apr 22 2025 William Cohen - 6.3.7-3 +- Backport the webapi filtering fix to allow the use of exact matching. (RHEL-88201) + +* Tue Apr 15 2025 Nathan Scott - 6.3.7-2 +- Add selinux policy for new proc_psi_t-induced failure + * Mon Mar 31 2025 Nathan Scott - 6.3.7-1 - Update to latest stable version of PCP (RHEL-83468) diff --git a/pcp2openmetrics-archive.patch b/pcp2openmetrics-archive.patch new file mode 100644 index 0000000..b51b8b7 --- /dev/null +++ b/pcp2openmetrics-archive.patch @@ -0,0 +1,99 @@ +From 634789506f10c0156b13bfa6a3f07705ed40b5f1 Mon Sep 17 00:00:00 2001 +From: lmchilton +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 + diff --git a/sources b/sources index 241d379..0406cc7 100644 --- a/sources +++ b/sources @@ -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