import Oracle_OSS pcp-7.0.3-5.0.1.el10_2
This commit is contained in:
parent
f5949a2578
commit
5f0a3da4bf
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
pcp-6.3.7.src.tar.gz
|
||||
pcp-7.0.3.src.tar.gz
|
||||
pdu-getpdu-overflow
|
||||
|
||||
38
1011-orabug38724847-fix-nfsclient-per-op-parsing.patch
Normal file
38
1011-orabug38724847-fix-nfsclient-per-op-parsing.patch
Normal file
@ -0,0 +1,38 @@
|
||||
From c80dc758eb0eb27fafa0b594d6c2aa1f4c3803fa Mon Sep 17 00:00:00 2001
|
||||
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
Date: Wed, 3 Dec 2025 20:40:52 +0530
|
||||
Subject: [PATCH] pmdanfsclient: fix regex to correctly parse NFS op stats
|
||||
|
||||
The regex used to match NFS operation statistics in /proc/self/mountstats was
|
||||
missing a capture group, causing parsing failures when an additional field
|
||||
was present in newer kernel formats. Updated the regex to include the extra
|
||||
numeric field so that all opstats lines are parsed correctly.
|
||||
|
||||
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
|
||||
Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/85671f8874d7d4b4e57eb45bfe295de92b95415c
|
||||
|
||||
Orabug: 38724847
|
||||
|
||||
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
|
||||
---
|
||||
src/pmdas/nfsclient/pmdanfsclient.python | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/src/pmdas/nfsclient/pmdanfsclient.python b/src/pmdas/nfsclient/pmdanfsclient.python
|
||||
index 786d958..f08b93a 100644
|
||||
--- a/src/pmdas/nfsclient/pmdanfsclient.python
|
||||
+++ b/src/pmdas/nfsclient/pmdanfsclient.python
|
||||
@@ -636,7 +636,7 @@ class NFSCLIENTPMDA(PMDA):
|
||||
line = STATS.readline()
|
||||
if line == '':
|
||||
break
|
||||
- m = re.match(r'\s*([A-Z_]*): (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*)$', line)
|
||||
+ m = re.match(r'\s*([A-Z_]*): (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*) (\d*)$', line)
|
||||
if not m:
|
||||
break
|
||||
opname = m.group(1).lower()
|
||||
--
|
||||
2.43.7
|
||||
|
||||
@ -0,0 +1,575 @@
|
||||
From ca8fbd2a827e3e7caa72331e003f93523c9f5241 Mon Sep 17 00:00:00 2001
|
||||
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
Date: Tue, 6 Jan 2026 05:53:08 +0000
|
||||
Subject: [PATCH] pcp-nfsiostat: Introduce PCP implementation of nfsiostat
|
||||
parser
|
||||
|
||||
Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/c0a0c53b57774dfac0b727e62472fb1ae6065540
|
||||
|
||||
Orabug: 38817053
|
||||
|
||||
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
---
|
||||
src/pcp/GNUmakefile | 1 +
|
||||
src/pcp/nfsiostat/GNUmakefile | 43 ++++
|
||||
src/pcp/nfsiostat/pcp-nfsiostat.1 | 132 +++++++++++
|
||||
src/pcp/nfsiostat/pcp-nfsiostat.py | 346 +++++++++++++++++++++++++++++
|
||||
4 files changed, 522 insertions(+)
|
||||
create mode 100644 src/pcp/nfsiostat/GNUmakefile
|
||||
create mode 100644 src/pcp/nfsiostat/pcp-nfsiostat.1
|
||||
create mode 100644 src/pcp/nfsiostat/pcp-nfsiostat.py
|
||||
|
||||
diff --git a/src/pcp/GNUmakefile b/src/pcp/GNUmakefile
|
||||
index da29269..2b74fc0 100644
|
||||
--- a/src/pcp/GNUmakefile
|
||||
+++ b/src/pcp/GNUmakefile
|
||||
@@ -30,6 +30,7 @@ SUBDIRS = \
|
||||
mpstat \
|
||||
netstat \
|
||||
numastat \
|
||||
+ nfsiostat \
|
||||
pidstat \
|
||||
ps \
|
||||
python \
|
||||
diff --git a/src/pcp/nfsiostat/GNUmakefile b/src/pcp/nfsiostat/GNUmakefile
|
||||
new file mode 100644
|
||||
index 0000000..d8c5356
|
||||
--- /dev/null
|
||||
+++ b/src/pcp/nfsiostat/GNUmakefile
|
||||
@@ -0,0 +1,43 @@
|
||||
+#
|
||||
+# Copyright (c) 2023 Oracle and/or its affiliates.
|
||||
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
+#
|
||||
+# This program is free software; you can redistribute it and/or modify it
|
||||
+# under the terms of the GNU General Public License as published by the
|
||||
+# Free Software Foundation; either version 2 of the License, or (at your
|
||||
+# option) any later version.
|
||||
+#
|
||||
+# This program is distributed in the hope that it will be useful, but
|
||||
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
+# for more details.
|
||||
+#
|
||||
+
|
||||
+TOPDIR = ../../..
|
||||
+include $(TOPDIR)/src/include/builddefs
|
||||
+
|
||||
+TARGET = pcp-nfsiostat
|
||||
+SCRIPT = $(TARGET).py
|
||||
+MAN_SECTION = 1
|
||||
+MAN_PAGES = $(TARGET).$(MAN_SECTION)
|
||||
+MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTION)
|
||||
+
|
||||
+default: $(SCRIPT) $(MAN_PAGES)
|
||||
+
|
||||
+include $(BUILDRULES)
|
||||
+
|
||||
+install: default
|
||||
+ifeq "$(HAVE_PYTHON)" "true"
|
||||
+ $(INSTALL) -m 755 $(SCRIPT) $(PCP_BINADM_DIR)/$(TARGET)
|
||||
+ @$(INSTALL_MAN)
|
||||
+endif
|
||||
+
|
||||
+default_pcp : default
|
||||
+
|
||||
+install_pcp : install
|
||||
+
|
||||
+check:: $(SCRIPT)
|
||||
+ $(PYLINT) $^
|
||||
+
|
||||
+check :: $(MAN_PAGES)
|
||||
+ $(MANLINT) $^
|
||||
diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.1 b/src/pcp/nfsiostat/pcp-nfsiostat.1
|
||||
new file mode 100644
|
||||
index 0000000..b297285
|
||||
--- /dev/null
|
||||
+++ b/src/pcp/nfsiostat/pcp-nfsiostat.1
|
||||
@@ -0,0 +1,132 @@
|
||||
+'\"! tbl | mmdoc
|
||||
+'\"macro stdmacro
|
||||
+.\"
|
||||
+.\" Man page for pcp-nfsiostat
|
||||
+.\" Copyright (c) 2023 Oracle and/or its affiliates.
|
||||
+.\" DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
+.\"
|
||||
+.\" This program is free software; you can redistribute it and/or modify it
|
||||
+.\" under the terms of the GNU General Public License as published by the
|
||||
+.\" Free Software Foundation; either version 2 of the License, or (at your
|
||||
+.\" option) any later version.
|
||||
+.\"
|
||||
+.\" This program is distributed in the hope that it will be useful, but
|
||||
+.\" WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
+.\" or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
+.\" for more details.
|
||||
+.\"
|
||||
+
|
||||
+.TH PCP-NFSIOSTAT 1 "PCP" "Performance Co-Pilot"
|
||||
+
|
||||
+.SH NAME
|
||||
+\f3pcp-nfsiostat\f1 \- Emulate iostat for NFS mount points using /proc/self/mountstats
|
||||
+
|
||||
+.SH SYNOPSIS
|
||||
+\fBpcp\fP [\fBpcp options\fP] \fBnfsiostat\fP [\fB-s\fP \fBsamples\fP] [\fB-a\fP \fBarchive\fP] [\fB-Z\fP \fB--timezone\fP] [\fB-z\fP \fB--hostzone\fP] [\fB-V\fP \fBVersion\fP]
|
||||
+
|
||||
+.SH DESCRIPTION
|
||||
+The\fB pcp-nfsiostat \fPcommand reports client-side\fB Network File System (NFS) \fPI/O statistics for each mounted NFS filesystem. For every NFS mountpoint, the output consists of a summary section followed by detailed read and write statistics blocks. These statistics include operation rates, data throughput, latency, retransmissions, queueing delays, and error counts. By default,\fB pcp-nfsiostat \fPmonitors NFS mountpoints on the local host and reports live metrics collected via the \fB PCP NFS PMDA\fP. When an archive is specified, historical metrics are reported instead.
|
||||
+
|
||||
+.SH OUTPUT FORMAT
|
||||
+Statistics are reported per mounted NFS filesystem using the following layout:
|
||||
+
|
||||
+.nf
|
||||
+server:/export mounted on /mount/point:
|
||||
+
|
||||
+ ops/s rpc bklog
|
||||
+ 0.017 0.000
|
||||
+
|
||||
+read: ops/s kB/s kB/op retrans avg RTT (ms) avg exe (ms) avg queue (ms) errors
|
||||
+ ...
|
||||
+
|
||||
+write: ops/s kB/s kB/op retrans avg RTT (ms) avg exe (ms) avg queue (ms) errors
|
||||
+ ...
|
||||
+.fi
|
||||
+
|
||||
+.SH FIELD DESCRIPTIONS
|
||||
+.TP
|
||||
+.B ops/s
|
||||
+Number of NFS operations performed per second.
|
||||
+
|
||||
+.TP
|
||||
+.B rpc bklog
|
||||
+Average number of RPC requests waiting to be transmitted.
|
||||
+
|
||||
+.TP
|
||||
+.B kB/s
|
||||
+Kilobytes transferred per second.
|
||||
+
|
||||
+.TP
|
||||
+.B kB/op
|
||||
+Average number of kilobytes transferred per operation.
|
||||
+
|
||||
+.TP
|
||||
+.B retrans
|
||||
+Number of RPC retransmissions and the retransmission percentage.
|
||||
+
|
||||
+.TP
|
||||
+.B avg RTT (ms)
|
||||
+Average round-trip time in milliseconds for RPC requests.
|
||||
+
|
||||
+.TP
|
||||
+.B avg exe (ms)
|
||||
+Average execution time in milliseconds spent servicing requests on the server.
|
||||
+
|
||||
+.TP
|
||||
+.B avg queue (ms)
|
||||
+Average time in milliseconds spent waiting in the RPC transmission queue.
|
||||
+
|
||||
+.TP
|
||||
+.B errors
|
||||
+Number of failed operations and failure percentage.
|
||||
+
|
||||
+.SH OPTIONS
|
||||
+.TP
|
||||
+.BR \-a ", " \-\-archive " " I archive
|
||||
+Fetch NFS I/O statistics from the specified PCP archive.
|
||||
+
|
||||
+.TP
|
||||
+.BR \-s ", " \-\-samples " " I samples
|
||||
+Number of samples to collect before exiting.
|
||||
+
|
||||
+.TP
|
||||
+.BR \-z ", " \-\-hostzone
|
||||
+Set the reporting timezone to the local timezone of the metrics source.
|
||||
+
|
||||
+.TP
|
||||
+.BR \-Z ", " \-\-timezone " " I tz
|
||||
+Set the reporting timezone.
|
||||
+
|
||||
+.TP
|
||||
+.BR \-V ", " \-\-version
|
||||
+Display version information and exit.
|
||||
+
|
||||
+.TP
|
||||
+.BR \-? ", " \-\-help
|
||||
+Display usage information and exit.
|
||||
+
|
||||
+.SH NOTES
|
||||
+.B pcp-nfsiostat
|
||||
+reports client-side NFS statistics collected from the kernel and exposed
|
||||
+via the PCP NFS PMDA. The output format and metrics are similar to those
|
||||
+reported by the
|
||||
+.BR nfsiostat (1)
|
||||
+tool from the nfs-utils package.
|
||||
+
|
||||
+.SH PCP ENVIRONMENT
|
||||
+Environment variables with the prefix \fBPCP_\fP are used to parameterize
|
||||
+the file and directory names used by PCP.
|
||||
+On each installation, the
|
||||
+file \fI/etc/pcp.conf\fP contains the local values for these variables.
|
||||
+The \fB$PCP_CONF\fP variable may be used to specify an alternative
|
||||
+configuration file, as described in
|
||||
+.BR pcp.conf (5).
|
||||
+
|
||||
+For environment variables affecting PCP tools, see
|
||||
+.BR pmGetOptions (3).
|
||||
+
|
||||
+.SH SEE ALSO
|
||||
+.BR PCPIntro (1),
|
||||
+.BR pcp (1),
|
||||
+.BR nfsiostat (1),
|
||||
+.BR environ (7).
|
||||
diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.py b/src/pcp/nfsiostat/pcp-nfsiostat.py
|
||||
new file mode 100644
|
||||
index 0000000..0b0376d
|
||||
--- /dev/null
|
||||
+++ b/src/pcp/nfsiostat/pcp-nfsiostat.py
|
||||
@@ -0,0 +1,346 @@
|
||||
+#!/usr/bin/pmpython
|
||||
+#
|
||||
+# Copyright (c) 2023 Oracle and/or its affiliates.
|
||||
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
+#
|
||||
+# This program is free software; you can redistribute it and/or modify it
|
||||
+# under the terms of the GNU General Public License as published by the
|
||||
+# Free Software Foundation; either version 2 of the License, or (at your
|
||||
+# option) any later version.
|
||||
+#
|
||||
+# This program is distributed in the hope that it will be useful, but
|
||||
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
+# for more details.
|
||||
+#
|
||||
+# pylint: disable=bad-whitespace,too-many-lines,bad-continuation
|
||||
+# pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
+# pylint: disable=redefined-outer-name,unnecessary-lambda
|
||||
+#
|
||||
+
|
||||
+import signal
|
||||
+import sys
|
||||
+import time
|
||||
+from pcp import pmapi, pmcc
|
||||
+from cpmapi import PM_CONTEXT_ARCHIVE
|
||||
+
|
||||
+SYS_METRICS= ["kernel.uname.sysname","kernel.uname.release",
|
||||
+ "kernel.uname.nodename","kernel.uname.machine","hinv.ncpu"]
|
||||
+NFSIOSTAT_METRICS = ["nfsclient.mountpoint","nfsclient.export","nfsclient.age",
|
||||
+ "nfsclient.xprt.sends","nfsclient.xprt.backlog_u","nfsclient.ops.read.ops",
|
||||
+ "nfsclient.ops.read.errors","nfsclient.ops.read.execute","nfsclient.ops.read.rtt",
|
||||
+ "nfsclient.ops.read.queue","nfsclient.ops.read.bytes_recv","nfsclient.ops.read.bytes_sent",
|
||||
+ "nfsclient.ops.read.ntrans","nfsclient.ops.write.ops","nfsclient.ops.write.errors",
|
||||
+ "nfsclient.ops.write.execute","nfsclient.ops.write.rtt","nfsclient.ops.write.queue",
|
||||
+ "nfsclient.ops.write.bytes_recv","nfsclient.ops.write.bytes_sent","nfsclient.ops.write.ntrans"]
|
||||
+ALL_METRICS = NFSIOSTAT_METRICS + SYS_METRICS
|
||||
+
|
||||
+def adjust_length(name):
|
||||
+ return name.ljust(25)
|
||||
+class ReportingMetricRepository:
|
||||
+
|
||||
+ def __init__(self,group):
|
||||
+ self.group=group
|
||||
+ self.current_cached_values = {}
|
||||
+
|
||||
+ def __sorted(self,data):
|
||||
+ return dict(sorted(data.items(), key=lambda item: item[0].lower()))
|
||||
+
|
||||
+ def __fetch_current_value(self,metric):
|
||||
+ val=dict(map(lambda x: (x[1], x[2]), self.group[metric].netValues))
|
||||
+ val=self.__sorted(val)
|
||||
+ return dict(val)
|
||||
+
|
||||
+ def current_value(self,metric):
|
||||
+ if not metric in self.group:
|
||||
+ return None
|
||||
+ if self.current_cached_values.get(metric) is None:
|
||||
+ first_value=self.__fetch_current_value(metric)
|
||||
+ self.current_cached_values[metric]=first_value
|
||||
+ return self.current_cached_values[metric]
|
||||
+
|
||||
+class NfsioStatUtil:
|
||||
+ def __init__(self,metrics_repository):
|
||||
+ self.__metric_repository=metrics_repository
|
||||
+ self.report=ReportingMetricRepository(self.__metric_repository)
|
||||
+
|
||||
+ def mount_point(self):
|
||||
+ return self.report.current_value('nfsclient.mountpoint')
|
||||
+
|
||||
+ def mount_share(self):
|
||||
+ return self.report.current_value('nfsclient.export')
|
||||
+
|
||||
+ def mount_share_keys(self):
|
||||
+ data = self.report.current_value('nfsclient.export')
|
||||
+ return data.keys()
|
||||
+
|
||||
+ def sample_time(self):
|
||||
+ return self.report.current_value('nfsclient.age')
|
||||
+
|
||||
+ def xprt_sends(self):
|
||||
+ return self.report.current_value('nfsclient.xprt.sends')
|
||||
+
|
||||
+ def xprt_backlog(self):
|
||||
+ return self.report.current_value('nfsclient.xprt.backlog_u')
|
||||
+
|
||||
+ def readops(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.ops')
|
||||
+
|
||||
+ def readerrors(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.errors')
|
||||
+
|
||||
+ def readexecute(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.execute')
|
||||
+
|
||||
+ def readrtt(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.rtt')
|
||||
+
|
||||
+ def readqueue(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.queue')
|
||||
+
|
||||
+ def readbytesrecv(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.bytes_recv')
|
||||
+
|
||||
+ def readbytessent(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.bytes_sent')
|
||||
+
|
||||
+ def readntrans(self):
|
||||
+ return self.report.current_value('nfsclient.ops.read.ntrans')
|
||||
+
|
||||
+ def writeops(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.ops')
|
||||
+
|
||||
+ def writeerrors(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.errors')
|
||||
+
|
||||
+ def writeexecute(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.execute')
|
||||
+
|
||||
+ def writertt(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.rtt')
|
||||
+
|
||||
+ def writequeue(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.queue')
|
||||
+
|
||||
+ def writebytesrecv(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.bytes_recv')
|
||||
+
|
||||
+ def writebytessent(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.bytes_sent')
|
||||
+
|
||||
+ def writentrans(self):
|
||||
+ return self.report.current_value('nfsclient.ops.write.ntrans')
|
||||
+
|
||||
+class NfsiostatReport(pmcc.MetricGroupPrinter):
|
||||
+ def __init__(self,opts,group):
|
||||
+ self.opts = opts
|
||||
+ self.group = group
|
||||
+ self.samples = opts.samples
|
||||
+ self.context = opts.context
|
||||
+
|
||||
+ def __get_ncpu(self, group):
|
||||
+ return group['hinv.ncpu'].netValues[0][2]
|
||||
+
|
||||
+ def __print_machine_info(self, context):
|
||||
+ timestamp = self.group.pmLocaltime(context.timestamp.tv_sec)
|
||||
+ # Please check strftime(3) for different formatting options.
|
||||
+ # Also check TZ and LC_TIME environment variables for more
|
||||
+ # information on how to override the default formatting of
|
||||
+ # the date display in the header
|
||||
+ time_string = time.strftime("%m/%d/%Y %H:%M:%S", timestamp.struct_time())
|
||||
+ header_string = ''
|
||||
+ header_string += context['kernel.uname.sysname'].netValues[0][2] + ' '
|
||||
+ header_string += context['kernel.uname.release'].netValues[0][2] + ' '
|
||||
+ header_string += '(' + context['kernel.uname.nodename'].netValues[0][2] + ') '
|
||||
+ header_string += time_string + ' '
|
||||
+ header_string += context['kernel.uname.machine'].netValues[0][2] + ' '
|
||||
+ print("%s (%s CPU)" % (header_string, self.__get_ncpu(context)))
|
||||
+
|
||||
+ def __print_values(self,timestamp, nfsstatus):
|
||||
+ n_shares = nfsstatus.mount_share_keys()
|
||||
+ mountshare = nfsstatus.mount_share()
|
||||
+ mountpoint = nfsstatus.mount_point()
|
||||
+ sampletime = nfsstatus.sample_time()
|
||||
+ sends = nfsstatus.xprt_sends()
|
||||
+ backlog = nfsstatus.xprt_backlog()
|
||||
+ readops = nfsstatus.readops()
|
||||
+ readerrors = nfsstatus.readerrors()
|
||||
+ readexecute = nfsstatus.readexecute()
|
||||
+ readrtt = nfsstatus.readrtt()
|
||||
+ readqueue = nfsstatus.readqueue()
|
||||
+ readbytesrecv = nfsstatus.readbytesrecv()
|
||||
+ readbytessent = nfsstatus.readbytessent()
|
||||
+ readntrans = nfsstatus.readntrans()
|
||||
+ writeops = nfsstatus.writeops()
|
||||
+ writeerrors = nfsstatus.writeerrors()
|
||||
+ writeexecute = nfsstatus.writeexecute()
|
||||
+ writertt = nfsstatus.writertt()
|
||||
+ writequeue = nfsstatus.writequeue()
|
||||
+ writebytesrecv = nfsstatus.writebytesrecv()
|
||||
+ writebytessent = nfsstatus.writebytessent()
|
||||
+ writentrans = nfsstatus.writentrans()
|
||||
+
|
||||
+ print("%-18s:%s"%("Timestamp", timestamp))
|
||||
+ print()
|
||||
+
|
||||
+ for name in n_shares:
|
||||
+ # read
|
||||
+ r_kilobytes = (readbytessent[name] + readbytesrecv[name]) / 1024
|
||||
+ if sampletime[name] > 0:
|
||||
+ ops_per_sample = sends[name] / sampletime[name]
|
||||
+ ops_per_sample_read = readops[name] / sampletime[name]
|
||||
+ r_kilobytes_per_sample = r_kilobytes / sampletime[name]
|
||||
+ else:
|
||||
+ ops_per_sample = 0.0
|
||||
+ ops_per_sample_read = 0.0
|
||||
+ r_kilobytes_per_sample = 0.0
|
||||
+
|
||||
+ r_retrans = readntrans[name] - readops[name]
|
||||
+ if readops[name] > 0:
|
||||
+ r_kilobytes_per_op = r_kilobytes / readops[name]
|
||||
+ r_retrans_percent = (r_retrans * 100) / readops[name]
|
||||
+ r_rtt_per_op = readrtt[name] / readops[name]
|
||||
+ r_exe_per_op = readexecute[name] / readops[name]
|
||||
+ r_queued_for_per_op = readqueue[name] / readops[name]
|
||||
+ r_errs_percent = (readerrors[name] * 100) / readops[name]
|
||||
+ else:
|
||||
+ r_kilobytes_per_op = 0.0
|
||||
+ r_retrans_percent = 0.0
|
||||
+ r_rtt_per_op = 0.0
|
||||
+ r_exe_per_op = 0.0
|
||||
+ r_queued_for_per_op = 0.0
|
||||
+ r_errs_percent = 0.0
|
||||
+
|
||||
+ # write
|
||||
+ w_kilobytes = (writebytessent[name] + writebytesrecv[name]) / 1024
|
||||
+ if sampletime[name] > 0:
|
||||
+ ops_per_sample_write = writeops[name] / sampletime[name]
|
||||
+ w_kilobytes_per_sample = w_kilobytes / sampletime[name]
|
||||
+ else:
|
||||
+ ops_per_sample_write = 0.0
|
||||
+ w_kilobytes_per_sample = 0.0
|
||||
+
|
||||
+ w_retrans = writentrans[name] - writeops[name]
|
||||
+ if writeops[name] > 0:
|
||||
+ w_kilobytes_per_op = w_kilobytes / writeops[name]
|
||||
+ w_retrans_percent = (w_retrans * 100) / writeops[name]
|
||||
+ w_rtt_per_op = writertt[name] / writeops[name]
|
||||
+ w_exe_per_op = writeexecute[name] / writeops[name]
|
||||
+ w_queued_for_per_op = writequeue[name] / writeops[name]
|
||||
+ w_errs_percent = (writeerrors[name] * 100) / writeops[name]
|
||||
+ else:
|
||||
+ w_kilobytes_per_op = 0.0
|
||||
+ w_retrans_percent = 0.0
|
||||
+ w_rtt_per_op = 0.0
|
||||
+ w_exe_per_op = 0.0
|
||||
+ w_queued_for_per_op = 0.0
|
||||
+ w_errs_percent = 0.0
|
||||
+
|
||||
+ print(f"{mountshare[name]} mounted on {mountpoint[name]}:")
|
||||
+
|
||||
+ print(f"{'':14}ops/s{'':7}rpc bklog")
|
||||
+ print(f"{ops_per_sample:19.3f}{backlog[name]:16.3f}")
|
||||
+ print()
|
||||
+ print(
|
||||
+ "read: "
|
||||
+ "ops/s kB/s kB/op retrans "
|
||||
+ "avg RTT (ms) avg exe (ms) avg queue (ms) errors"
|
||||
+ )
|
||||
+ print(
|
||||
+ f"{'':19}"
|
||||
+ f"{ops_per_sample_read:5.3f}"
|
||||
+ f"{r_kilobytes_per_sample:12.3f}"
|
||||
+ f"{r_kilobytes_per_op:13.3f} "
|
||||
+ f"{int(r_retrans):2d} ({r_retrans_percent:2.1f}%)"
|
||||
+ f"{r_rtt_per_op:15.3f}"
|
||||
+ f"{r_exe_per_op:15.3f}"
|
||||
+ f"{r_queued_for_per_op:17.3f} "
|
||||
+ f"{int(readerrors[name]):4d} ({r_errs_percent:2.1f}%)"
|
||||
+ )
|
||||
+
|
||||
+ print(
|
||||
+ "write: "
|
||||
+ "ops/s kB/s kB/op retrans "
|
||||
+ "avg RTT (ms) avg exe (ms) avg queue (ms) errors"
|
||||
+ )
|
||||
+
|
||||
+ print(
|
||||
+ f"{'':19}"
|
||||
+ f"{ops_per_sample_write:5.3f}"
|
||||
+ f"{w_kilobytes_per_sample:12.3f}"
|
||||
+ f"{w_kilobytes_per_op:13.3f} "
|
||||
+ f"{int(w_retrans):2d} ({w_retrans_percent:2.1f}%)"
|
||||
+ f"{w_rtt_per_op:15.3f}"
|
||||
+ f"{w_exe_per_op:15.3f}"
|
||||
+ f"{w_queued_for_per_op:17.3f} "
|
||||
+ f"{int(writeerrors[name]):4d} ({w_errs_percent:2.1f}%)"
|
||||
+ )
|
||||
+ print()
|
||||
+
|
||||
+ def print_report(self,group,timestamp, manager_nfsiostat):
|
||||
+ def __print_nfs_status():
|
||||
+ nfsstatus = NfsioStatUtil(manager_nfsiostat)
|
||||
+ if nfsstatus.mount_share():
|
||||
+ try:
|
||||
+ self.__print_machine_info(group)
|
||||
+ self.__print_values(timestamp, nfsstatus)
|
||||
+ except IndexError:
|
||||
+ print("Incorrect machine info due to some missing metrics")
|
||||
+ return
|
||||
+ else:
|
||||
+ pass
|
||||
+
|
||||
+ if self.context != PM_CONTEXT_ARCHIVE and self.samples is None:
|
||||
+ __print_nfs_status()
|
||||
+ sys.exit(0)
|
||||
+ elif self.context == PM_CONTEXT_ARCHIVE and self.samples is None:
|
||||
+ __print_nfs_status()
|
||||
+ elif self.samples >=1:
|
||||
+ __print_nfs_status()
|
||||
+ self.samples-=1
|
||||
+ else:
|
||||
+ pass
|
||||
+
|
||||
+ def report(self, manager):
|
||||
+ group = manager["sysinfo"]
|
||||
+ self.samples = self.opts.pmGetOptionSamples()
|
||||
+ t_s = group.contextCache.pmLocaltime(int(group.timestamp))
|
||||
+ timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time())
|
||||
+ self.print_report(group,timestamp,manager['nfsiostat'])
|
||||
+
|
||||
+class NfsiostatOptions(pmapi.pmOptions):
|
||||
+ timefmt = "%m/%d/%Y %H:%M:%S"
|
||||
+ def __init__(self):
|
||||
+ pmapi.pmOptions.__init__(self, "a:s:Z:zV?")
|
||||
+ self.pmSetLongOptionHeader("General options")
|
||||
+ self.pmSetLongOptionHostZone()
|
||||
+ self.pmSetLongOptionTimeZone()
|
||||
+ self.pmSetLongOptionHelp()
|
||||
+ self.pmSetLongOptionSamples()
|
||||
+ self.pmSetLongOptionVersion()
|
||||
+ self.samples=None
|
||||
+ self.context=None
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ try:
|
||||
+ opts = NfsiostatOptions()
|
||||
+ mngr = pmcc.MetricGroupManager.builder(opts,sys.argv)
|
||||
+ opts.context=mngr.type
|
||||
+ missing = mngr.checkMissingMetrics(ALL_METRICS)
|
||||
+ if missing is not None:
|
||||
+ sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing)
|
||||
+ sys.exit(1)
|
||||
+ mngr["nfsiostat"] = ALL_METRICS
|
||||
+ mngr["sysinfo"] = SYS_METRICS
|
||||
+ mngr.printer = NfsiostatReport(opts,mngr)
|
||||
+ sts = mngr.run()
|
||||
+ sys.exit(sts)
|
||||
+ except pmapi.pmErr as error:
|
||||
+ sys.stderr.write('%s\n' % (error.message()))
|
||||
+ except pmapi.pmUsageErr as usage:
|
||||
+ usage.message()
|
||||
+ sys.exit(1)
|
||||
+ except IOError:
|
||||
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
||||
+ except KeyboardInterrupt:
|
||||
+ pass
|
||||
--
|
||||
2.43.7
|
||||
|
||||
178
1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch
Normal file
178
1013-pmlogger_janitor-fix-not-to-terminate-unauthorized-p.patch
Normal file
@ -0,0 +1,178 @@
|
||||
From 7bc4a59bfe06f9b988ea21ad6a2decc86c942097 Mon Sep 17 00:00:00 2001
|
||||
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
Date: Thu, 26 Mar 2026 14:16:48 +0000
|
||||
Subject: [PATCH OL9 1013/1016] pmlogger_janitor fix not to terminate
|
||||
unauthorized process
|
||||
|
||||
- Fix pmlogger_janitor.sh to verify pid for active pmlogger before kill/removing map files
|
||||
- Update process type detection to support *BSD ps output syntax
|
||||
- Refactor pmlogger_check.sh to load janitor env vars more robustly
|
||||
|
||||
upstream :- 358b684619d786acc93092824bc24ba69c5d5dbc
|
||||
f305a22730a614535098c4f85c05de37eadc790e
|
||||
|
||||
[Orabug:38598244]
|
||||
Signed-off-by: sagar sagar <sagar.sagar@oracle.com>
|
||||
|
||||
---
|
||||
src/pmlogger/pmlogger_check.sh | 27 +++++++++++---
|
||||
src/pmlogger/pmlogger_farm.defaults | 8 ----
|
||||
src/pmlogger/pmlogger_janitor.sh | 57 ++++++++++++++++++++++++++++-
|
||||
3 files changed, 76 insertions(+), 16 deletions(-)
|
||||
|
||||
diff --git a/src/pmlogger/pmlogger_check.sh b/src/pmlogger/pmlogger_check.sh
|
||||
index c941df0..27a0a1e 100755
|
||||
--- a/src/pmlogger/pmlogger_check.sh
|
||||
+++ b/src/pmlogger/pmlogger_check.sh
|
||||
@@ -23,9 +23,6 @@
|
||||
|
||||
PMLOGGER="$PCP_BINADM_DIR/pmlogger"
|
||||
PMLOGCONF="$PCP_BINADM_DIR/pmlogconf"
|
||||
-PMLOGGERENVS="$PCP_SYSCONFIG_DIR/pmlogger"
|
||||
-PMLOGGERFARMENVS="$PCP_SYSCONFIG_DIR/pmlogger_farm"
|
||||
-PMLOGGERZEROCONFENVS="$PCP_SHARE_DIR/zeroconf/pmlogger"
|
||||
|
||||
# error messages should go to stderr, not the GUI notifiers
|
||||
#
|
||||
@@ -703,7 +700,7 @@ _callback_log_control()
|
||||
if [ "X$primary" = Xy ]
|
||||
then
|
||||
# User configuration takes precedence over pcp-zeroconf
|
||||
- envs=`grep -h ^PMLOGGER "$PMLOGGERZEROCONFENVS" "$PMLOGGERENVS" 2>/dev/null`
|
||||
+ envs=`grep -h ^PMLOGGER "$PCP_SHARE_DIR/zeroconf/pmlogger" "$PCP_SYSCONFIG_DIR/pmlogger" 2>/dev/null`
|
||||
args="-P $args"
|
||||
iam=" primary"
|
||||
# clean up port-map, just in case
|
||||
@@ -721,7 +718,7 @@ _callback_log_control()
|
||||
return
|
||||
fi
|
||||
else
|
||||
- envs=`grep -h ^PMLOGGER "$PMLOGGERFARMENVS" 2>/dev/null`
|
||||
+ envs=`grep -h ^PMLOGGER "$PCP_SYSCONFIG_DIR/pmlogger_farm" 2>/dev/null`
|
||||
args="-h $host $args"
|
||||
iam=""
|
||||
fi
|
||||
@@ -896,10 +893,28 @@ fi
|
||||
# because the legitimate pmloggers, like the primary pmlogger, may
|
||||
# not be included in the "test" control file(s).
|
||||
#
|
||||
+if [ -z "${PMLOGGER_CHECK_SKIP_JANITOR+is_set}" ]
|
||||
+then
|
||||
+ check=`grep '^PMLOGGER_CHECK_SKIP_JANITOR=' $PCP_SYSCONFIG_DIR/pmlogger`
|
||||
+ if [ -n "$check" ]
|
||||
+ then
|
||||
+ eval $check
|
||||
+ fi
|
||||
+fi
|
||||
+if [ -z "${PMLOGGER_JANITOR_ARGS+is_set}" ]
|
||||
+then
|
||||
+ check=`grep '^PMLOGGER_JANITOR_ARGS=' $PCP_SYSCONFIG_DIR/pmlogger`
|
||||
+ if [ -n "$check" ]
|
||||
+ then
|
||||
+ eval $check
|
||||
+ fi
|
||||
+fi
|
||||
+
|
||||
if [ "$CONTROL" = "$PCP_PMLOGGERCONTROL_PATH" -a "$PMLOGGER_CHECK_SKIP_JANITOR" != "yes" ]
|
||||
then
|
||||
$VERY_VERBOSE && echo "Running: pmlogger_janitor $daily_args"
|
||||
- $PCP_BINADM_DIR/pmlogger_janitor $daily_args
|
||||
+ args="$daily_args"
|
||||
+ $PCP_BINADM_DIR/pmlogger_janitor $args
|
||||
fi
|
||||
|
||||
if [ -f $tmp/err ]
|
||||
diff --git a/src/pmlogger/pmlogger_farm.defaults b/src/pmlogger/pmlogger_farm.defaults
|
||||
index 10a0132..af06446 100644
|
||||
--- a/src/pmlogger/pmlogger_farm.defaults
|
||||
+++ b/src/pmlogger/pmlogger_farm.defaults
|
||||
@@ -25,11 +25,3 @@
|
||||
# setting PMLOGGER_CHECK_SKIP_LOGCONF to yes disables the regeneration
|
||||
# and checking.
|
||||
# PMLOGGER_CHECK_SKIP_LOGCONF=yes
|
||||
-
|
||||
-# By default pmlogger_check(1) will run pmlogger_janitor to check for
|
||||
-# pmlogger(1) badness caused by processes and/or files that were once
|
||||
-# managed from the control files but have become detached from those
|
||||
-# control files.
|
||||
-# Setting PMLOGGER_CHECK_SKIP_JANITOR to yes disables pmlogger_janitor
|
||||
-# and maybe useful for QA or special testing
|
||||
-# PMLOGGER_CHECK_SKIP_JANITOR=yes
|
||||
diff --git a/src/pmlogger/pmlogger_janitor.sh b/src/pmlogger/pmlogger_janitor.sh
|
||||
index bc0360a..7c8ec0c 100755
|
||||
--- a/src/pmlogger/pmlogger_janitor.sh
|
||||
+++ b/src/pmlogger/pmlogger_janitor.sh
|
||||
@@ -348,8 +348,15 @@ _callback_log_control()
|
||||
|
||||
if [ -n "$pid" ]
|
||||
then
|
||||
- # found matching pmlogger ... cull this one from
|
||||
- $VERY_VERBOSE && echo "[$filename:$line] match PID $pid, nothing to be done"
|
||||
+ # found matching pmlogger ... cull this one from $tmp/loggers
|
||||
+ #
|
||||
+ if $VERY_VERBOSE
|
||||
+ then
|
||||
+ echo "[$filename:$line] match PID $pid, nothing to be done"
|
||||
+ elif $VERBOSE
|
||||
+ then
|
||||
+ echo "Pass 3: PID $pid matches control [$filename:$line], nothing to be done"
|
||||
+ fi
|
||||
sed <$tmp/loggers >$tmp/tmp -e "/^$pid /d"
|
||||
mv $tmp/tmp $tmp/loggers
|
||||
fi
|
||||
@@ -365,6 +372,52 @@ then
|
||||
| while read file
|
||||
do
|
||||
pid=`echo "$file" | sed -e "s@$PCP_TMP_DIR/pmlogger/@@"`
|
||||
+ # sanity checks
|
||||
+ # 1. does this process exist?
|
||||
+ # 2. is it really pmlogger?
|
||||
+ # if "no" to either case, remove this (stale) mapfile
|
||||
+ # and move on ...
|
||||
+ #
|
||||
+ if $PCP_PS_PROG -p "$pid" >$tmp/tmp 2>&1
|
||||
+ then
|
||||
+ # ps(1) -p output should be something like this ...
|
||||
+ # PID TTY TIME CMD
|
||||
+ # 14298 ? 00:00:00 pmlogger
|
||||
+ # or this (for *BSD)
|
||||
+ # PID TT STAT TIME COMMAND
|
||||
+ # 22839 1 S 0:00.04 /usr/libexec/pcp/bin/pmlogger -N -P ...
|
||||
+ #
|
||||
+ if sed -n -e 2p <$tmp/tmp | grep -E -q '( pmlogger$)|(/bin/pmlogger )'
|
||||
+ then
|
||||
+ : OK
|
||||
+ else
|
||||
+ if $VERBOSE
|
||||
+ then
|
||||
+ cat $tmp/tmp
|
||||
+ echo "Warning: PID $pid is not a pmlogger process, removing $file"
|
||||
+ fi
|
||||
+ if $SHOWME
|
||||
+ then
|
||||
+ echo "+ rm $file"
|
||||
+ else
|
||||
+ rm -f "$file"
|
||||
+ fi
|
||||
+ continue
|
||||
+ fi
|
||||
+ else
|
||||
+ if $VERBOSE
|
||||
+ then
|
||||
+ echo "Warning: PID $pid has vanished, removing $file"
|
||||
+ fi
|
||||
+ if $SHOWME
|
||||
+ then
|
||||
+ echo "+ rm $file"
|
||||
+ else
|
||||
+ rm -f "$file"
|
||||
+ fi
|
||||
+ continue
|
||||
+ fi
|
||||
+
|
||||
# timing window here, file may have gone away between
|
||||
# find(1) and awk(1), so just ignore any errors ...
|
||||
#
|
||||
--
|
||||
2.43.7
|
||||
|
||||
1277
1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch
Normal file
1277
1014-pcp-ps-implement-sort-option-to-allow-sorting-by-cpu.patch
Normal file
File diff suppressed because it is too large
Load Diff
604
1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch
Normal file
604
1015-pmlogger_daily-d-disk-option-for-archive-space-limit.patch
Normal file
@ -0,0 +1,604 @@
|
||||
From 09f39fb28a3171300afa5ed15e7f75ac0fbfdabc Mon Sep 17 00:00:00 2001
|
||||
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
Date: Thu, 26 Mar 2026 16:10:37 +0000
|
||||
Subject: [PATCH OL9 1015/1016] pmlogger_daily -d/--disk option for archive
|
||||
space limit
|
||||
|
||||
Added -d/--disk option in pmlogger_daily for archive size limit
|
||||
Doc: Explain $PCP_SPACELIMIT env var for disk usage in PCP archives
|
||||
Clarify archive retention/purging with space limits and env/CLI opts
|
||||
utilproc.sh: Refactor _convert_to_kb(), unit checks, env
|
||||
|
||||
upstream ref:- 7b7c38c16c6904960ede400efa5225732935b830
|
||||
|
||||
[orabug:38757778]
|
||||
Signed-off-by: sagar sagar <sagar.sagar@oracle.com>
|
||||
|
||||
---
|
||||
man/man1/pmlogger_daily.1 | 85 +++++++++++++++-
|
||||
src/pmlogger/pmlogger_daily.sh | 176 +++++++++++++++++++++++++++++++--
|
||||
src/pmlogger/utilproc.sh | 142 +++++++++++++++++++++++++-
|
||||
3 files changed, 391 insertions(+), 12 deletions(-)
|
||||
|
||||
diff --git a/man/man1/pmlogger_daily.1 b/man/man1/pmlogger_daily.1
|
||||
index c7b30ae..0371f63 100644
|
||||
--- a/man/man1/pmlogger_daily.1
|
||||
+++ b/man/man1/pmlogger_daily.1
|
||||
@@ -20,6 +20,7 @@
|
||||
.B $PCP_BINADM_DIR/pmlogger_daily
|
||||
[\f3\-DEfKMNnoPpQrRVzZ?\f1]
|
||||
[\f3\-c\f1 \f2control\f1]
|
||||
+[\f3\-d\f1 \f2fssize\f1]
|
||||
[\f3\-k\f1 \f2time\f1]
|
||||
[\f3\-l\f1 \f2logfile\f1]
|
||||
[\f3\-m\f1 \f2addresses\f1]
|
||||
@@ -120,6 +121,72 @@ Do not perform the conditional
|
||||
.BR pmlogger_daily_report (1)
|
||||
processing as described below.
|
||||
.TP 5
|
||||
+\fB\-d\fR \fIfssize\fR, \fB\-\-disk\fR=\fIfssize\fR
|
||||
+This option enforces a maximum total file system space usage per host directory for PCP archive files stored under each
|
||||
+.IR $PCP_ARCHIVE_DIR/<hostname>
|
||||
+and
|
||||
+.IR $PCP_REMOTE_ARCHIVE_DIR/<hostname>
|
||||
+location. The
|
||||
+.I fssize
|
||||
+must be specified as an integer with a suffix
|
||||
+.B K
|
||||
+or
|
||||
+.B k
|
||||
+for kilobytes,
|
||||
+.B M
|
||||
+or
|
||||
+.B m
|
||||
+for megabytes,
|
||||
+.B G
|
||||
+or
|
||||
+.B g
|
||||
+for gigabytes (e.g. 500m or 10G).
|
||||
+As a special case,
|
||||
+.I fssize
|
||||
+may be the keyword
|
||||
+.B unlimited
|
||||
+to prevent any file system space limit enforcement (this is the default
|
||||
+behaviour).
|
||||
+.RS
|
||||
+.PP
|
||||
+Alternatively, the file system space limit may be set by exporting the
|
||||
+.B $PCP_SPACELIMIT
|
||||
+environment variable, which is handled analogously to
|
||||
+.BR $PCP_CULLAFTER .
|
||||
+If both
|
||||
+.B $PCP_SPACELIMIT
|
||||
+and
|
||||
+.B \-d
|
||||
+are provided and specify different values then the value from
|
||||
+the environment variable is used and a warning is issued.
|
||||
+.PP
|
||||
+After normal daily log processing (compression, merging, culling by age, etc.)
|
||||
+completes,
|
||||
+if the total file system space consumed by any
|
||||
+.BR pmlogger (1)
|
||||
+instance exceeds
|
||||
+.IR fssize ,
|
||||
+then additional archive files will be purged (i.e. removed),
|
||||
+oldest first, until
|
||||
+the file system usage is reduced to be not more than
|
||||
+.IR fssize .
|
||||
+This purging operation never deletes archive files with today's date,
|
||||
+and so always preserves the most recent archives required by active
|
||||
+.BR pmlogger (1)
|
||||
+processes.
|
||||
+.PP
|
||||
+Enforcement is only performed when
|
||||
+.B pmlogger_daily
|
||||
+runs (typically once a day), not continuously.
|
||||
+As a result, the total file system usage for any
|
||||
+.BR pmlogger (1)
|
||||
+instance may exceed a specified maximum between
|
||||
+.B pmlogger_daily
|
||||
+executions (for example, as new archives are created
|
||||
+or when archives are uncompressed for merging or
|
||||
+prior to asynchronous archive compression).
|
||||
+.RE
|
||||
+.TP 5
|
||||
\fB\-E\fR, \fB\-\-expunge\fR
|
||||
This option causes
|
||||
.B pmlogger_daily
|
||||
@@ -524,7 +591,7 @@ maximizes the diagnostic capabilities for debugging.
|
||||
.TP 5
|
||||
\fB\-x\fR \fItime\fR, \fB\-\-compress\-after\fR=\fItime\fR
|
||||
Archive data files can optionally be compressed after some period
|
||||
-to conserve disk space.
|
||||
+to conserve file system space.
|
||||
This is particularly useful for large numbers of
|
||||
.BR pmlogger (1)
|
||||
processes under the control of
|
||||
@@ -796,7 +863,7 @@ may appear literally in
|
||||
and will be substituted at execution time to generate the destination
|
||||
directory name. For example:
|
||||
.ft CR
|
||||
-.in +6n
|
||||
+.in +2n
|
||||
$PCP_AUTOSAVE_DIR=/gpfs/LOCALHOSTNAME/DATEYYYY/DATEMM-DATEDD
|
||||
.br
|
||||
.PP
|
||||
@@ -957,6 +1024,20 @@ if this file exists, then this is treated as equivalent to using
|
||||
on the command line and the file will be removed once all rewriting
|
||||
has been done.
|
||||
.SH PCP ENVIRONMENT
|
||||
+.TP 5
|
||||
+.B $PCP_SPACELIMIT
|
||||
+If set, specifies a maximum allowed total file system space (in kilobytes, or with optional K/M/G suffix as for the \-d option) for each per-host archive directory under
|
||||
+.I $PCP_ARCHIVE_DIR
|
||||
+or
|
||||
+.I $PCP_REMOTE_ARCHIVE_DIR .
|
||||
+This value is used by
|
||||
+.B pmlogger_daily
|
||||
+when enforcing archive retention limits for file system space, unless the
|
||||
+.B \-d
|
||||
+option is provided in which case the command line flag overrides the environment variable and a warning is issued. Enforcement occurs after each run of
|
||||
+.B pmlogger_daily
|
||||
+and only applies at that time—not continuously.
|
||||
+.PP
|
||||
Environment variables with the prefix \fBPCP_\fP are used to parameterize
|
||||
the file and directory names used by PCP.
|
||||
On each installation, the
|
||||
diff --git a/src/pmlogger/pmlogger_daily.sh b/src/pmlogger/pmlogger_daily.sh
|
||||
index 3ebd17c..be915d6 100755
|
||||
--- a/src/pmlogger/pmlogger_daily.sh
|
||||
+++ b/src/pmlogger/pmlogger_daily.sh
|
||||
@@ -37,6 +37,7 @@ prog=`basename $0`
|
||||
PROGLOG=$PCP_LOG_DIR/pmlogger/$prog.log
|
||||
MYPROGLOG=$PROGLOG.$$
|
||||
USE_SYSLOG=true
|
||||
+localhost=`hostname || echo localhost`
|
||||
|
||||
# optional begin logging to $PCP_LOG_DIR/NOTICES
|
||||
#
|
||||
@@ -353,6 +354,7 @@ Options:
|
||||
-E,--expunge expunge metrics with metadata inconsistencies when merging archives
|
||||
-f,--force force actions (intended for QA, not production)
|
||||
-k=TIME,--discard=TIME remove archives after TIME (format DD[:HH[:MM]])
|
||||
+ -d=fssize,--disk=fssize set maximum disk usage for archives for each pmlogger instance
|
||||
-K compress, but no other changes
|
||||
-l=FILE,--logfile=FILE send important diagnostic messages to FILE
|
||||
-m=ADDRs,--mail=ADDRs send daily NOTICES entries to email addresses
|
||||
@@ -404,6 +406,8 @@ DO_DAILY_REPORT=true
|
||||
NOPROXY=false
|
||||
PROXYONLY=false
|
||||
NOERROR=false
|
||||
+SPACELIMIT_CMDLINE=""
|
||||
+SPACELIMIT_DEFAULT="unlimited"
|
||||
|
||||
ARGS=`pmgetopt --progname=$prog --config=$tmp/usage -- "$@"`
|
||||
[ $? != 0 ] && exit 1
|
||||
@@ -419,6 +423,24 @@ do
|
||||
;;
|
||||
-D) DO_DAILY_REPORT=false
|
||||
;;
|
||||
+ -d) SPACELIMIT_CMDLINE="$2"
|
||||
+ shift
|
||||
+ if [ -n "$PCP_SPACELIMIT" -a "$PCP_SPACELIMIT" != "$SPACELIMIT_CMDLINE" ]
|
||||
+ then
|
||||
+ echo "Warning: -d value ($SPACELIMIT_CMDLINE) ignored because \$PCP_SPACELIMIT ($PCP_SPACELIMIT) set in environment"
|
||||
+ SPACELIMIT_CMDLINE=""
|
||||
+ continue
|
||||
+ fi
|
||||
+ if [ "$SPACELIMIT_CMDLINE" != unlimited ]
|
||||
+ then
|
||||
+ if ! _convert_to_kb "$SPACELIMIT_CMDLINE" >/dev/null
|
||||
+ then
|
||||
+ echo "Error: -d value ($SPACELIMIT_CMDLINE) not valid"
|
||||
+ $NOERROR || status=1
|
||||
+ exit
|
||||
+ fi
|
||||
+ fi
|
||||
+ ;;
|
||||
-E) EXPUNGE="-E"
|
||||
;;
|
||||
-f) FORCE=true
|
||||
@@ -781,10 +803,49 @@ fi
|
||||
if [ ! -f "$CONTROL" ]
|
||||
then
|
||||
echo "$prog: Error: cannot find control file ($CONTROL)"
|
||||
+ echo "... I am here ... `pwd` ... and these files are here ..."
|
||||
+ ls -l
|
||||
$NOERROR || status=1
|
||||
exit
|
||||
fi
|
||||
|
||||
+# Given a list of candidate directories, calculate the total
|
||||
+# size below them in Kbytes
|
||||
+#
|
||||
+_calculate_total_size()
|
||||
+{
|
||||
+ du -sk "$@" 2>$tmp/cts_err \
|
||||
+ | awk >$tmp/cts_out '
|
||||
+BEGIN { kb = 0 }
|
||||
+ { kb += $1 }
|
||||
+END { print kb }' >$tmp/cts_out
|
||||
+ if [ -s $tmp/cts_err ] && $VERY_VERBOSE
|
||||
+ then
|
||||
+ echo >&2 "Warning: _calculate_total_size: du -sk $@ produced errors ..."
|
||||
+ cat >&2 $tmp/cts_err
|
||||
+ fi
|
||||
+ $VERY_VERBOSE && echo >&2 "Info: _calculate_total_size -> `cat $tmp/cts_out` Kbytes"
|
||||
+ cat $tmp/cts_out
|
||||
+}
|
||||
+
|
||||
+# Calculate the total size of the archive with basename $1
|
||||
+#
|
||||
+_calculate_archive_size()
|
||||
+{
|
||||
+ du -sk "$1".* 2>$tmp/cts_err \
|
||||
+ | awk >$tmp/cts_out '
|
||||
+BEGIN { kb = 0 }
|
||||
+ { kb += $1 }
|
||||
+END { print kb }' >$tmp/cts_out
|
||||
+ if [ -s $tmp/cts_err ] && $VERY_VERBOSE
|
||||
+ then
|
||||
+ echo >&2 "Warning: _calculate_archive_size: du -sk $1.* produced errors ..."
|
||||
+ cat >&2 $tmp/cts_err
|
||||
+ fi
|
||||
+ $VERY_VERBOSE && echo >&2 "Info: _calculate_archive_size $1 -> `cat $tmp/cts_out` Kbytes"
|
||||
+ cat $tmp/cts_out
|
||||
+}
|
||||
+
|
||||
_skipping()
|
||||
{
|
||||
echo "$prog: Warning: $@"
|
||||
@@ -1062,15 +1123,93 @@ BEGIN { seenslash = 0; inshell = 0; out = "" }
|
||||
END { print out }'
|
||||
}
|
||||
|
||||
+
|
||||
+
|
||||
+# Check disk space allocation for one pmlogger instance, and if more
|
||||
+# than $SPACELIMIT Kbytes, purge files to try and get the space
|
||||
+# allocated to be not more than $SPACELIMIT Kbytes.
|
||||
+#
|
||||
+# Oldest files are purged first and the most recent archives for today
|
||||
+# (including those required by active pmlogger (1) processes are
|
||||
+# never deleted.
|
||||
+#
|
||||
+# On entry, $find_dirs is a list of one or more directories holding
|
||||
+# archives for a pmlogger instance, set in _callback_log_control(),
|
||||
+# and $host is the host name field from the control line, set in
|
||||
+# _parse_log_control().
|
||||
+#
|
||||
+_do_purge()
|
||||
+{
|
||||
+ SPACELIMIT="$PCP_SPACELIMIT"
|
||||
+ [ -z "$SPACELIMIT" ] && SPACELIMIT="$SPACELIMIT_CMDLINE"
|
||||
+ [ -z "$SPACELIMIT" ] && SPACELIMIT="$SPACELIMIT_DEFAULT"
|
||||
+ [ "$SPACELIMIT" = unlimited ] && return
|
||||
+ if ! _convert_to_kb "$SPACELIMIT" >$tmp/tmp
|
||||
+ then
|
||||
+ _warning "skipping purging because of invalid space limit"
|
||||
+ return
|
||||
+ fi
|
||||
+ SPACELIMIT=`cat $tmp/tmp`
|
||||
+ $VERY_VERBOSE && echo >&2 "SPACELIMIT=$SPACELIMIT"
|
||||
+ __total_size=`_calculate_total_size $find_dirs`
|
||||
+ if [ "$__total_size" -le "$SPACELIMIT" ]
|
||||
+ then
|
||||
+ $VERBOSE && echo "Info: No purging required for $host, archives ($__total_size Kbytes) <= size limit ($SPACELIMIT Kbytes)"
|
||||
+ return
|
||||
+ fi
|
||||
+ $VERBOSE && echo "Info: archives for $host ($__total_size Kbytes) >= size limit ($SPACELIMIT Kbytes)"
|
||||
+
|
||||
+ # algorithm to find archive basenames borrowed from _do_merge()
|
||||
+ # output is in this format ...
|
||||
+ # <directory>|<archive>
|
||||
+ #
|
||||
+ TODAY=`date +%Y%m%d`
|
||||
+ find $find_dirs -maxdepth 1 -type f \
|
||||
+ | sed -n \
|
||||
+ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.meta.*\)/s//\1|\2/p' \
|
||||
+ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
|
||||
+ -e '/\(.*\)\/\([12][0-9][0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]-[0-9][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
|
||||
+ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.meta.*\)/s//\1|\2/p' \
|
||||
+ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
|
||||
+ -e '/\(.*\)\/\([0-9][0-9][0-1][0-9][0-3][0-9]\)\(\.[0-2][0-9].[0-5][0-9]-[0-9][0-9]\)\(\.meta.*\)/s//\1|\2\3/p' \
|
||||
+ | sort -t'|' -n -k2,2 \
|
||||
+ | $PCP_AWK_PROG -F'|' '
|
||||
+$2 == "'$TODAY'" { next }
|
||||
+$2 ~ /^'$TODAY'/ { next }
|
||||
+ { print }' >$tmp/purge_candidates
|
||||
+ if [ ! -s $tmp/purge_candidates ]
|
||||
+ then
|
||||
+ $VERBOSE && echo "Info: No candidates to purge."
|
||||
+ return
|
||||
+ fi
|
||||
+
|
||||
+ sed -e 's/|/ /' <$tmp/purge_candidates \
|
||||
+ | while read __dir __file
|
||||
+ do
|
||||
+ [ "$__dir" != "." ] && __file="$__dir/$__file"
|
||||
+ __arch_size=`_calculate_archive_size "$__file"`
|
||||
+ if $SHOWME
|
||||
+ then
|
||||
+ echo "+ rm `echo $__file.**`"
|
||||
+ __total_size=`expr $__total_size - $__arch_size`
|
||||
+ else
|
||||
+ if rm "$__file".*
|
||||
+ then
|
||||
+ $VERBOSE && echo "Info: purge $__file, reclaims $__arch_size Kbytes"
|
||||
+ __total_size=`expr $__total_size - $__arch_size`
|
||||
+ else
|
||||
+ $VERBOSE && echo "Warning: rm $__file.* failed"
|
||||
+ fi
|
||||
+ fi
|
||||
+ [ "$__total_size" -le "$SPACELIMIT" ] && break
|
||||
+ done
|
||||
+}
|
||||
+
|
||||
# come here from _parse_log_control() once per valid line in a control
|
||||
# file ... see utilproc.sh for interface definitions
|
||||
#
|
||||
_callback_log_control()
|
||||
{
|
||||
- # nothing to do for pmlogger pushing to a remote pmproxy
|
||||
- #
|
||||
- $logpush && return
|
||||
-
|
||||
if $VERBOSE
|
||||
then
|
||||
echo
|
||||
@@ -1130,6 +1269,10 @@ _callback_log_control()
|
||||
find_dirs=`echo $orig_dir | _unbackquote`
|
||||
$VERBOSE && echo "Embedded \`...\`: find_dirs=$find_dirs"
|
||||
fi
|
||||
+ # and rewrite LOCALHOSTNAME if it is included
|
||||
+ #
|
||||
+ find_dirs=`echo "$find_dirs" | sed -e "s;LOCALHOSTNAME;$localhost;g"`
|
||||
+ $VERY_VERBOSE && echo "Info: find_dirs=$find_dirs"
|
||||
|
||||
# For archive rewriting (to make metadata consistent across
|
||||
# archives) find the rules as follows:
|
||||
@@ -1424,6 +1567,14 @@ _callback_log_control()
|
||||
$VERY_VERBOSE && echo >&2 "Warning: no trace files found to cull"
|
||||
fi
|
||||
fi
|
||||
+
|
||||
+ # if space limit specified, potentially purge old archives if
|
||||
+ # space limit exceeded
|
||||
+ #
|
||||
+ if [ -n "$PCP_SPACELIMIT" -o -n "$SPACELIMIT_CMDLINE" ]
|
||||
+ then
|
||||
+ _do_purge
|
||||
+ fi
|
||||
}
|
||||
|
||||
# Paranoid archive saving
|
||||
@@ -1779,7 +1930,10 @@ _do_compress()
|
||||
do
|
||||
# pmlc may race with pmlogger starting up here - a timeout is required
|
||||
# to avoid pmlc blocking forever and hanging pmlogger_daily. RHBZ#1892326
|
||||
- [ -z "$PMLOGGER_REQUEST_TIMEOUT" ] && export PMLOGGER_REQUEST_TIMEOUT=2
|
||||
+ if [ -z "$PMLOGGER_REQUEST_TIMEOUT" ]
|
||||
+ then
|
||||
+ PMLOGGER_REQUEST_TIMEOUT=2; export PMLOGGER_REQUEST_TIMEOUT
|
||||
+ fi
|
||||
if pmlc "$pid" </dev/null 2>&1 | tee $tmp/out \
|
||||
| grep "^Connected to .*pmlogger" >/dev/null
|
||||
then
|
||||
@@ -2017,6 +2171,7 @@ else
|
||||
#
|
||||
if cd "$PCP_REMOTE_ARCHIVE_DIR"
|
||||
then
|
||||
+ $VERY_VERBOSE && echo "Info: do_compress: cd to $PCP_REMOTE_ARCHIVE_DIR OK"
|
||||
# one-trip guard if there is something to be done
|
||||
#
|
||||
rm -f $tmp/proxy_sighup
|
||||
@@ -2077,7 +2232,16 @@ else
|
||||
_parse_log_control $tmp/control
|
||||
fi
|
||||
done
|
||||
- cd $here
|
||||
+ if cd $here
|
||||
+ then
|
||||
+ $VERY_VERBOSE && echo "Info: do_compress: cd back to $here OK"
|
||||
+ else
|
||||
+ echo >&2 "Error: do_compress: failed to cd back to $here"
|
||||
+ $NOERROR || status=1
|
||||
+ fi
|
||||
+ else
|
||||
+ echo >&2 "Error: do_compress: faild to cd to $PCP_REMOTE_ARCHIVE_DIR"
|
||||
+ $NOERROR || status=1
|
||||
fi
|
||||
fi
|
||||
|
||||
diff --git a/src/pmlogger/utilproc.sh b/src/pmlogger/utilproc.sh
|
||||
index ae9c72f..9e5582c 100644
|
||||
--- a/src/pmlogger/utilproc.sh
|
||||
+++ b/src/pmlogger/utilproc.sh
|
||||
@@ -256,6 +256,7 @@ BEGIN { i = 0 }
|
||||
[ $? -eq 0 ] && _PWDCMD="$_PWDCMD -P"
|
||||
fi
|
||||
_here=`$_PWDCMD`
|
||||
+ $VERY_VERBOSE && echo >&2 "_parse_log_control: initial pwd=$_here"
|
||||
|
||||
if echo "$1" | grep -q -e '\.rpmsave$' -e '\.rpmnew$' -e '\.rpmorig$' -e '\.dpkg-dist$' -e '\.dpkg-old$' -e '\.dpkg-new$'
|
||||
then
|
||||
@@ -270,7 +271,10 @@ BEGIN { i = 0 }
|
||||
| while read host primary socks dir args
|
||||
do
|
||||
# start in one place for each iteration (beware of relative paths)
|
||||
- cd "$_here"
|
||||
+ if ! cd "$_here"
|
||||
+ then
|
||||
+ $VERY_VERBOSE && echo >&2 "_parse_log_control: failed to cd back to $_here"
|
||||
+ fi
|
||||
line=`expr $line + 1`
|
||||
|
||||
if $VERY_VERBOSE
|
||||
@@ -350,6 +354,51 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p
|
||||
fi
|
||||
;;
|
||||
|
||||
+ 'export PCP_SPACELIMIT;'*)
|
||||
+ _old_value="$PCP_SPACELIMIT"
|
||||
+ _check=`echo "$_cmd" | sed -e 's/.*=//' -e 's/ *$//'`
|
||||
+ if [ -n "$_check" ]
|
||||
+ then
|
||||
+ if [ "$_check" = unlimited ]
|
||||
+ then
|
||||
+ # no conversion
|
||||
+ :
|
||||
+ else
|
||||
+ # check syntax & convert to canonical Kbytes
|
||||
+ #
|
||||
+ _kb=`_convert_to_kb "$_check"`
|
||||
+ if [ $? != 0 ]
|
||||
+ then
|
||||
+ _warning "\$PCP_SPACELIMIT value ($_check) is invalid. Must be a positive integer and a unit (e.g. 100M)"
|
||||
+ _cmd=''
|
||||
+ else
|
||||
+ $SHOWME && echo "+ $_cmd (normalized to $_kb Kbytes)"
|
||||
+ # need to put back the "K" units here
|
||||
+ # # because it will get re-processed by
|
||||
+ # _convert_to_kb() later
|
||||
+ #
|
||||
+ _cmd=`echo "$_cmd" | sed -e "s/=.*/=${_kb}K/"`
|
||||
+ fi
|
||||
+ fi
|
||||
+ if [ -n "$_cmd" ]
|
||||
+ then
|
||||
+ echo eval $_cmd >>$tmp/_cmd
|
||||
+ eval $_cmd
|
||||
+ if [ -n "$_old_value" -a "$_old_value" != "$PCP_SPACELIMIT" ]
|
||||
+ then
|
||||
+ _warning "\$PCP_SPACELIMIT ($PCP_SPACELIMIT) reset from control file, previous value ($_old_value) ignored"
|
||||
+ fi
|
||||
+ if [ -n "$PCP_SPACELIMIT" -a -n "$SPACELIMIT_CMDLINE" -a "$PCP_SPACELIMIT" != "$SPACELIMIT_CMDLINE" ]
|
||||
+ then
|
||||
+ _warning "\$PCP_SPACELIMIT ($PCP_SPACELIMIT) reset from control file, -d value ($SPACELIMIT_CMDLINE) ignored"
|
||||
+ SPACELIMIT_CMDLINE=""
|
||||
+ fi
|
||||
+ fi
|
||||
+ else
|
||||
+ _warning "\$PCP_SPACELIMIT from control file missing a value, will be ignored"
|
||||
+ fi
|
||||
+ ;;
|
||||
+
|
||||
'export PCP_COMPRESS;'*)
|
||||
_old_value="$PCP_COMPRESS"
|
||||
$SHOWME && echo "+ $_cmd"
|
||||
@@ -537,10 +586,8 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p
|
||||
# check $dir, cd there, acquire lock
|
||||
#
|
||||
$SHOWME && echo "+ cd $dir"
|
||||
- if cd "$dir"
|
||||
+ if ! cd "$dir"
|
||||
then
|
||||
- :
|
||||
- else
|
||||
if $SHOWME
|
||||
then
|
||||
echo "+ ... cannot show any more for this control line"
|
||||
@@ -659,6 +706,10 @@ s/^\([A-Za-z][A-Za-z0-9_]*\)=/export \1; \1=/p
|
||||
fi
|
||||
|
||||
done
|
||||
+ if ! cd "$_here"
|
||||
+ then
|
||||
+ $VERY_VERBOSE && echo >&2 "_parse_log_control: failed to cd back to $_here at return"
|
||||
+ fi
|
||||
}
|
||||
|
||||
# Called from _callback_log_control() [in pmlogger_check and pmlogger_janitor]
|
||||
@@ -859,6 +910,89 @@ END { exit sts }'
|
||||
fi
|
||||
}
|
||||
|
||||
+# Converts a size string like 10G, 100M, 100K to integer KBytes for limit
|
||||
+# checks.
|
||||
+# Usage: _convert_to_kb size_string
|
||||
+# Outputs: integer KBytes on stdout, returns 0 if OK, 1 if error
|
||||
+_convert_to_kb()
|
||||
+{
|
||||
+ __input="$1"
|
||||
+ __num=
|
||||
+ __unit=
|
||||
+ __kb=
|
||||
+ if [ -z "$__input" ]
|
||||
+ then
|
||||
+ echo "Error: _convert_to_kb(): missing argument" >&2
|
||||
+ return 1
|
||||
+ fi
|
||||
+ __num=`echo "$__input" | sed -E 's/^([0-9]+)\s*([a-zA-Z]*)$/\1/'`
|
||||
+ case "$__num"
|
||||
+ in
|
||||
+ "" )
|
||||
+ echo "Error: _convert_to_kb(): argument '$__input' does not start with a number" >&2
|
||||
+ return 1
|
||||
+ ;;
|
||||
+ *[!0-9]* )
|
||||
+ echo "Error: _convert_to_kb(): argument '$__input' has a non-numeric value" >&2
|
||||
+ return 1
|
||||
+ ;;
|
||||
+ 0 )
|
||||
+ echo "Error: _convert_to_kb(): argument '$__input' resolves to zero (not allowed)" >&2
|
||||
+ return 1
|
||||
+ ;;
|
||||
+ esac
|
||||
+ __unit=`echo "$__input" | sed -E 's/^([0-9]+)\s*([a-zA-Z]*)$/\2/'`
|
||||
+ case "$__unit"
|
||||
+ in
|
||||
+ G|g|M|m|K|k)
|
||||
+ ;;
|
||||
+ '')
|
||||
+ echo "Error: _convert_to_kb(): missing unit after '$__num'" >&2
|
||||
+ return 1
|
||||
+ ;;
|
||||
+ *)
|
||||
+ echo "Error: _convert_to_kb(): invalid unit '$__unit'" >&2
|
||||
+ return 1
|
||||
+ ;;
|
||||
+ esac
|
||||
+
|
||||
+ MAX_INT32=2147483647
|
||||
+
|
||||
+ case "$__unit"
|
||||
+ in
|
||||
+ G|g)
|
||||
+ # Check before multiplying
|
||||
+ __max=`expr $MAX_INT32 / \( 1024 \* 1024 \)`
|
||||
+ if [ $__num -gt $__max ]
|
||||
+ then
|
||||
+ echo "Error: overflow, $__num Gbytes too large for Kbytes in a 32-bit signed int" >&2
|
||||
+ return 1
|
||||
+ fi
|
||||
+ __kb=`expr $__num \* 1024 \* 1024`
|
||||
+ ;;
|
||||
+ M|m)
|
||||
+ # Check before multiplying
|
||||
+ __max=`expr $MAX_INT32 / 1024`
|
||||
+ if [ $__num -gt $__max ]
|
||||
+ then
|
||||
+ echo "Error: overflow, $__num Mbytes too large for Kbytes in a 32-bit signed int" >&2
|
||||
+ return 1
|
||||
+ fi
|
||||
+ __kb=`expr $__num \* 1024`
|
||||
+ ;;
|
||||
+ K|k)
|
||||
+ if [ $__num -gt $MAX_INT32 ]
|
||||
+ then
|
||||
+ echo "Error: overflow, $__num Kbytes too large for 32-bit signed int" >&2
|
||||
+ return 1
|
||||
+ fi
|
||||
+ __kb=$__num
|
||||
+ ;;
|
||||
+ esac
|
||||
+ echo "$__kb"
|
||||
+ return 0
|
||||
+}
|
||||
+
|
||||
# current time to the highest precision available from date(1) and
|
||||
# strftime(3)
|
||||
#
|
||||
--
|
||||
2.43.7
|
||||
|
||||
332
1016-pcp-system-tools-restore-backward-compatibility-with.patch
Normal file
332
1016-pcp-system-tools-restore-backward-compatibility-with.patch
Normal file
@ -0,0 +1,332 @@
|
||||
From a6bb22bf407522ef95557de0cebde717de347bde Mon Sep 17 00:00:00 2001
|
||||
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
Date: Thu, 26 Mar 2026 16:31:22 +0000
|
||||
Subject: [PATCH OL9 1016/1016] pcp-system-tools: restore backward
|
||||
compatibility with older pmapi versions
|
||||
|
||||
Commit 0a37ed0 introduced support for pmapi version 4, but upgrading to the latest pmapi is not currently desired.
|
||||
This change updates pcp-iostat, mpstat, pidstat, and ps to fall back to tv_usec when tv_nsec is unavailable
|
||||
during timestamp delta calculation, ensuring compatibility with older pmapi versions and collectors that only expose tv_usec
|
||||
|
||||
- optimized code in pcp-ps for dynamic sorting
|
||||
- fixed sorting colum indexes in -u case
|
||||
|
||||
[Orabug: 38719615]
|
||||
Signed-off-by: Sagar Sagar <sagar.sagar@oracle.com>
|
||||
|
||||
---
|
||||
src/pcp/iostat/pcp-iostat.py | 15 +++-
|
||||
src/pcp/mpstat/pcp-mpstat.py | 13 +++-
|
||||
src/pcp/pidstat/pcp-pidstat.py | 13 +++-
|
||||
src/pcp/ps/pcp-ps.py | 125 +++++++++++++------------------
|
||||
src/pcp/tapestat/pcp-tapestat.py | 16 +++-
|
||||
5 files changed, 97 insertions(+), 85 deletions(-)
|
||||
|
||||
diff --git a/src/pcp/iostat/pcp-iostat.py b/src/pcp/iostat/pcp-iostat.py
|
||||
index fd049a6..79d8c9b 100755
|
||||
--- a/src/pcp/iostat/pcp-iostat.py
|
||||
+++ b/src/pcp/iostat/pcp-iostat.py
|
||||
@@ -56,9 +56,18 @@ class IostatReport(pmcc.MetricGroupPrinter):
|
||||
Hcount = 0
|
||||
def timeStampDelta(self, group):
|
||||
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
|
||||
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
- # n may be negative here, calculation is still correct.
|
||||
- return s + n / 1000000000.0
|
||||
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
|
||||
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
|
||||
+ # when available, but gracefully fall back to microseconds to avoid
|
||||
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
|
||||
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
+ # n may be negative here, calculation is still correct.
|
||||
+ return s + n / 1000000000.0
|
||||
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
|
||||
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
|
||||
+ return s + u / 1000000.0
|
||||
+ # it should not reach here
|
||||
+ return s
|
||||
|
||||
def instlist(self, group, name):
|
||||
return dict(map(lambda x: (x[1], x[2]), group[name].netValues)).keys()
|
||||
diff --git a/src/pcp/mpstat/pcp-mpstat.py b/src/pcp/mpstat/pcp-mpstat.py
|
||||
index 2b0bd59..4e0cdf6 100755
|
||||
--- a/src/pcp/mpstat/pcp-mpstat.py
|
||||
+++ b/src/pcp/mpstat/pcp-mpstat.py
|
||||
@@ -499,8 +499,17 @@ class MpstatReport(pmcc.MetricGroupPrinter):
|
||||
|
||||
def timeStampDelta(self, group):
|
||||
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
|
||||
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
- return s + n / 1000000000.0
|
||||
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
|
||||
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
|
||||
+ # when available, but gracefully fall back to microseconds to avoid
|
||||
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
|
||||
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
+ return s + n / 1000000000.0
|
||||
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
|
||||
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
|
||||
+ return s + u / 1000000.0
|
||||
+ # it should not reach here
|
||||
+ return s
|
||||
|
||||
def print_machine_info(self,group, context):
|
||||
self.get_summary_metrics(group)
|
||||
diff --git a/src/pcp/pidstat/pcp-pidstat.py b/src/pcp/pidstat/pcp-pidstat.py
|
||||
index 32bf925..b7613e1 100755
|
||||
--- a/src/pcp/pidstat/pcp-pidstat.py
|
||||
+++ b/src/pcp/pidstat/pcp-pidstat.py
|
||||
@@ -925,8 +925,17 @@ class PidstatReport(pmcc.MetricGroupPrinter):
|
||||
|
||||
def timeStampDelta(self, group):
|
||||
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
|
||||
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
- return s + n / 1000000000.0
|
||||
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
|
||||
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
|
||||
+ # when available, but gracefully fall back to microseconds to avoid
|
||||
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
|
||||
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
+ return s + n / 1000000000.0
|
||||
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
|
||||
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
|
||||
+ return s + u / 1000000.0
|
||||
+ # it should not reach here
|
||||
+ return s
|
||||
|
||||
def print_machine_info(self,group, context):
|
||||
timestamp = context.pmLocaltime(group.timestamp.tv_sec)
|
||||
diff --git a/src/pcp/ps/pcp-ps.py b/src/pcp/ps/pcp-ps.py
|
||||
index e2992ea..e5abf34 100755
|
||||
--- a/src/pcp/ps/pcp-ps.py
|
||||
+++ b/src/pcp/ps/pcp-ps.py
|
||||
@@ -252,7 +252,7 @@ class ProcessStatusUtil:
|
||||
if self.user_percent() is not None and self.guest_percent() is not None and self.system_percent() is not None:
|
||||
return float("%.2f" % (self.user_percent() + self.guest_percent() + self.system_percent()))
|
||||
else:
|
||||
- return None
|
||||
+ return 0.0
|
||||
|
||||
def stime(self):
|
||||
c_systime = self.__get_value('proc.psinfo.stime', self.instance)
|
||||
@@ -366,6 +366,20 @@ class DynamicProcessReporter:
|
||||
self.printer = printer
|
||||
self.processStatOptions = processStatOptions
|
||||
|
||||
+ def __sort_by_idx(self, output_list, sorting_idx, reverse=True):
|
||||
+ # Rows are tab-delimited; splitting on whitespace breaks when
|
||||
+ # command/args contain spaces and shifts sortable column indexes.
|
||||
+ return sorted(
|
||||
+ output_list,
|
||||
+ key=lambda row: (
|
||||
+ float(row.split('\t')[sorting_idx].strip())
|
||||
+ if sorting_idx < len(row.split('\t'))
|
||||
+ and row.split('\t')[sorting_idx].strip().replace('.', '', 1).replace('-', '', 1).isdigit()
|
||||
+ else float('-inf')
|
||||
+ ),
|
||||
+ reverse=reverse
|
||||
+
|
||||
+
|
||||
def _is_last_and_args(self, key):
|
||||
return (key == "args") and \
|
||||
self.processStatOptions.colum_list.index(key) == len(self.processStatOptions.colum_list) - 1
|
||||
@@ -378,19 +392,15 @@ class DynamicProcessReporter:
|
||||
# Sorting validations
|
||||
sorting_idx = None
|
||||
if self.processStatOptions.sorting_flag:
|
||||
- if self.processStatOptions.filterstate == "ALL":
|
||||
- if self.processStatOptions.sorting_order == '%mem':
|
||||
- sorting_idx = 7
|
||||
- elif self.processStatOptions.sorting_order == '%cpu':
|
||||
- sorting_idx = 8
|
||||
- else:
|
||||
- sorting_idx = next((idx for idx, key in enumerate(self.processStatOptions.colum_list)
|
||||
- if key == self.processStatOptions.sorting_order),
|
||||
- None)
|
||||
- if sorting_idx is None:
|
||||
- raise ValueError("Sorting order not found in output columns")
|
||||
- # Adjust for timestamp column
|
||||
- sorting_idx += 1
|
||||
+ # For dynamic output, sorting key must be present in selected columns.
|
||||
+ if self.processStatOptions.sorting_order not in self.processStatOptions.colum_list:
|
||||
+ raise ValueError("Sorting order not found in output columns")
|
||||
+
|
||||
+ # Find sorting column index and adjust for Timestamp at position 0.
|
||||
+ sorting_idx = next((idx for idx, key in enumerate(self.processStatOptions.colum_list)
|
||||
+ if key == self.processStatOptions.sorting_order), None)
|
||||
+ # to account for Timestamp colum
|
||||
+ sorting_idx += 1
|
||||
|
||||
# Always compute process list ONCE
|
||||
processes = self.process_filter.filter_processes(
|
||||
@@ -400,48 +410,8 @@ class DynamicProcessReporter:
|
||||
output_list = []
|
||||
header = None
|
||||
|
||||
- # -------- PATH 1: With filterstate -------- #
|
||||
- if self.processStatOptions.filterstate == "ALL":
|
||||
- header = (
|
||||
- "Timestamp\tUSER\t\tPID\t\tPPID\t\tPRI\t%CPU\t%MEM\tVSZ"
|
||||
- "\tRSS\tS\tSTARTED\t\tTIME\t\t"
|
||||
- "WCHAN\t\t\t\tCommand"
|
||||
- )
|
||||
-
|
||||
- # Precompute format string
|
||||
- # fmt = (
|
||||
- # "{ts}{indent}{user}\t{pid}\t{ppid}\t{pri}\t{cpu}\t{mem}\t"
|
||||
- # "{vsz}\t{rss}\t{s}\t{started}\t{time}\t{wchan}\t{cmd}"
|
||||
- # )
|
||||
-
|
||||
- for process in processes:
|
||||
- # Maintain state info
|
||||
- key = (process.s_name(), process.pid())
|
||||
- process_state_info[key] = process_state_info.get(key, 0) + self.delta_time
|
||||
- row = [timestamp]
|
||||
- row.extend([
|
||||
- process.user_name(),
|
||||
- process.pid(),
|
||||
- process.ppid(),
|
||||
- process.priority(),
|
||||
- process.total_percent(),
|
||||
- process.system_percent(),
|
||||
- process.vsize(),
|
||||
- process.rss(),
|
||||
- process.s_name(),
|
||||
- process.start(),
|
||||
- process.total_time(),
|
||||
- process.wchan_s(),
|
||||
- process.process_name_with_args_last()[:45]
|
||||
- ])
|
||||
- output_list.append(
|
||||
- "\t".join(
|
||||
- str(x) if x is not None else '' for x in row
|
||||
- )
|
||||
- )
|
||||
-
|
||||
- # -------- PATH 2: Customized column list -------- #
|
||||
- elif self.processStatOptions.colum_list is not None:
|
||||
+ # -------- Dynamic column list path -------- #
|
||||
+ if self.processStatOptions.colum_list is not None:
|
||||
|
||||
header = "Timestamp\t"
|
||||
for key in self.processStatOptions.colum_list:
|
||||
@@ -457,23 +427,14 @@ class DynamicProcessReporter:
|
||||
# print(row)
|
||||
output_list.append("\t".join(str(x) if x is not None else '' for x in row))
|
||||
|
||||
- # -------- PATH 3: Invalid filterstate or column list -------- #
|
||||
+ # -------- Invalid column list -------- #
|
||||
# This should never happen, but just in case
|
||||
else:
|
||||
raise ValueError("No valid filterstate or column list provided")
|
||||
|
||||
# Sorting logic
|
||||
if self.processStatOptions.sorting_flag and sorting_idx is not None:
|
||||
- output_list.sort(
|
||||
- key=lambda x: (
|
||||
- sorting_idx,
|
||||
- float('inf') if (
|
||||
- len(x.split()) <= sorting_idx or
|
||||
- not x.split()[sorting_idx].replace('.', '', 1).isdigit()
|
||||
- ) else float(x.split()[sorting_idx])
|
||||
- ),
|
||||
- reverse=True
|
||||
- )
|
||||
+ output_list = self.__sort_by_idx(output_list, sorting_idx, reverse=True)
|
||||
|
||||
# --------- Print output --------- #
|
||||
self.printer(header)
|
||||
@@ -536,25 +497,25 @@ class ProcessStatusReporter:
|
||||
for process in processes:
|
||||
output_rows.append("%s%s%s\t\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (
|
||||
safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()),
|
||||
- safe_str(process.pid()),safe_str(process.system_percent()), safe_str(process.total_percent()),
|
||||
+ safe_str(process.pid()),safe_str(process.system_percent()), safe_str(process.mem()),
|
||||
safe_str(process.vsize()), safe_str(process.rss()),
|
||||
safe_str(process.tty_name()), safe_str(process.ppid()), safe_str(process.total_time()),
|
||||
safe_str(process.start()),
|
||||
safe_str(process.process_name())))
|
||||
- cpu_idx = 5
|
||||
- mem_idx = 6
|
||||
+ cpu_idx = 3
|
||||
+ mem_idx = 4
|
||||
elif selected_flag == "user":
|
||||
for process in processes:
|
||||
output_rows.append("%s%s%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (
|
||||
safe_str(timestamp), safe_str(value_indentation), safe_str(process.user_name()),
|
||||
safe_str(process.pid()),
|
||||
- safe_str(process.system_percent()), safe_str(process.total_percent()),
|
||||
+ safe_str(process.system_percent()), safe_str(process.mem()),
|
||||
safe_str(process.vsize()), safe_str(process.rss()),
|
||||
safe_str(process.tty_name()), safe_str(process.s_name()),
|
||||
safe_str(process.total_time()), safe_str(process.start()),
|
||||
safe_str(process.process_name())))
|
||||
- cpu_idx = 5
|
||||
- mem_idx = 6
|
||||
+ cpu_idx = 3
|
||||
+ mem_idx = 4
|
||||
elif selected_flag == "command":
|
||||
for process in processes:
|
||||
output_rows.append("%s%s%s\t%s\t%s\t%s\t%s" % (
|
||||
@@ -590,8 +551,17 @@ class ProcessStatReport(pmcc.MetricGroupPrinter):
|
||||
|
||||
def timeStampDelta(self):
|
||||
s = self.group.timestamp.tv_sec - self.group.prevTimestamp.tv_sec
|
||||
- n = self.group.timestamp.tv_nsec - self.group.prevTimestamp.tv_nsec
|
||||
- return s + n / 1000000000.0
|
||||
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
|
||||
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
|
||||
+ # when available, but gracefully fall back to microseconds to avoid
|
||||
+ if hasattr(self.group.timestamp, 'tv_nsec') and hasattr(self.group.prevTimestamp, 'tv_nsec'):
|
||||
+ n = self.group.timestamp.tv_nsec - self.group.prevTimestamp.tv_nsec
|
||||
+ return s + n / 1000000000.0
|
||||
+ elif hasattr(self.group.timestamp, 'tv_usec') and hasattr(self.group.prevTimestamp, 'tv_usec'):
|
||||
+ u = self.group.timestamp.tv_usec - self.group.prevTimestamp.tv_usec
|
||||
+ return s + u / 1000000.0
|
||||
+ # it should not reach here
|
||||
+ return s
|
||||
|
||||
def print_machine_info(self,context):
|
||||
timestamp = context.pmLocaltime(self.group.timestamp.tv_sec)
|
||||
@@ -868,6 +838,11 @@ class ProcessStatOptions(pmapi.pmOptions):
|
||||
try:
|
||||
if optarg.upper() == "ALL":
|
||||
self.filterstate = optarg.upper()
|
||||
+ self.colum_list = [
|
||||
+ "uname", "pid", "ppid", "pri", "%cpu", "%mem",
|
||||
+ "vsize", "rss", "state", "start", "time", "wchan", "args"
|
||||
+ ]
|
||||
+
|
||||
else:
|
||||
dummy_list = optarg.replace(',', ' ').split(' ')
|
||||
if self.debug_mode:
|
||||
diff --git a/src/pcp/tapestat/pcp-tapestat.py b/src/pcp/tapestat/pcp-tapestat.py
|
||||
index dd54c70..ac76c68 100755
|
||||
--- a/src/pcp/tapestat/pcp-tapestat.py
|
||||
+++ b/src/pcp/tapestat/pcp-tapestat.py
|
||||
@@ -68,9 +68,19 @@ class TapestatReport(pmcc.MetricGroupPrinter):
|
||||
Hcount = 0
|
||||
def timeStampDelta(self, group):
|
||||
s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec
|
||||
- n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
- # n may be negative here, calculation is still correct.
|
||||
- return s + n / 1000000000.0
|
||||
+ # pmapi timestamps may provide sub-second resolution via tv_nsec (nanoseconds)
|
||||
+ # or tv_usec (microseconds) depending on the collector. Prefer nanoseconds
|
||||
+ # when available, but gracefully fall back to microseconds to avoid
|
||||
+ if hasattr(group.timestamp, 'tv_nsec') and hasattr(group.prevTimestamp, 'tv_nsec'):
|
||||
+ n = group.timestamp.tv_nsec - group.prevTimestamp.tv_nsec
|
||||
+ # n may be negative here, calculation is still correct.
|
||||
+ return s + n / 1000000000.0
|
||||
+ elif hasattr(group.timestamp, 'tv_usec') and hasattr(group.prevTimestamp, 'tv_usec'):
|
||||
+ u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec
|
||||
+ return s + u / 1000000.0
|
||||
+ # it should not reach here
|
||||
+ return s
|
||||
+
|
||||
def instlist(self, group, name):
|
||||
return dict(map(lambda x: (x[1], x[2]), group[name].netValues)).keys()
|
||||
|
||||
--
|
||||
2.43.7
|
||||
|
||||
364
1017-orabug39068870-adds-interval-option-in-nfsiostat.patch
Normal file
364
1017-orabug39068870-adds-interval-option-in-nfsiostat.patch
Normal file
@ -0,0 +1,364 @@
|
||||
From c79976edb317272dcc8ae9352778d5d335763f10 Mon Sep 17 00:00:00 2001
|
||||
From: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
Date: Wed, 25 Feb 2026 12:45:43 +0000
|
||||
Subject: [PATCH] Add interval and count support for nfsiostat tool
|
||||
|
||||
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
|
||||
Cherry-pick-commit: https://github.com/performancecopilot/pcp/commit/02d9b5a7b3c2ea16de2665c1f8cd6b81843fee15
|
||||
|
||||
Orabug: 39068870
|
||||
|
||||
Signed-off-by: Sourav Sharma <sourav.ss.sharma@oracle.com>
|
||||
|
||||
---
|
||||
src/pcp/nfsiostat/pcp-nfsiostat.py | 269 ++++++++++++++++-------------
|
||||
1 file changed, 145 insertions(+), 124 deletions(-)
|
||||
|
||||
diff --git a/src/pcp/nfsiostat/pcp-nfsiostat.py b/src/pcp/nfsiostat/pcp-nfsiostat.py
|
||||
index 0b0376d..e19fa41 100644
|
||||
--- a/src/pcp/nfsiostat/pcp-nfsiostat.py
|
||||
+++ b/src/pcp/nfsiostat/pcp-nfsiostat.py
|
||||
@@ -22,7 +22,7 @@ import signal
|
||||
import sys
|
||||
import time
|
||||
from pcp import pmapi, pmcc
|
||||
-from cpmapi import PM_CONTEXT_ARCHIVE
|
||||
+from cpmapi import PM_CONTEXT_ARCHIVE, PM_MODE_FORW
|
||||
|
||||
SYS_METRICS= ["kernel.uname.sysname","kernel.uname.release",
|
||||
"kernel.uname.nodename","kernel.uname.machine","hinv.ncpu"]
|
||||
@@ -40,98 +40,54 @@ def adjust_length(name):
|
||||
class ReportingMetricRepository:
|
||||
|
||||
def __init__(self,group):
|
||||
- self.group=group
|
||||
- self.current_cached_values = {}
|
||||
-
|
||||
- def __sorted(self,data):
|
||||
- return dict(sorted(data.items(), key=lambda item: item[0].lower()))
|
||||
-
|
||||
- def __fetch_current_value(self,metric):
|
||||
- val=dict(map(lambda x: (x[1], x[2]), self.group[metric].netValues))
|
||||
- val=self.__sorted(val)
|
||||
- return dict(val)
|
||||
-
|
||||
- def current_value(self,metric):
|
||||
- if not metric in self.group:
|
||||
- return None
|
||||
- if self.current_cached_values.get(metric) is None:
|
||||
- first_value=self.__fetch_current_value(metric)
|
||||
- self.current_cached_values[metric]=first_value
|
||||
- return self.current_cached_values[metric]
|
||||
+ self.group = group
|
||||
+ self._current_cache = {}
|
||||
+ self._previous_cache = {}
|
||||
+
|
||||
+ def _fetch_values(self, metric, use_previous=False):
|
||||
+ """Fetch values - always returns a dictionary."""
|
||||
+ if metric not in self.group:
|
||||
+ return {}
|
||||
+ attr = "netPrevValues" if use_previous else "netValues"
|
||||
+ values = getattr(self.group[metric], attr, [])
|
||||
+ return {x[0].inst: x[2] for x in values} if values else {}
|
||||
+
|
||||
+ def _get_values_dict(self, metric, use_previous=False):
|
||||
+ """Get cached dictionary of all values for a metric."""
|
||||
+ cache = self._previous_cache if use_previous else self._current_cache
|
||||
+ if metric not in cache:
|
||||
+ cache[metric] = self._fetch_values(metric, use_previous)
|
||||
+ return cache[metric]
|
||||
+
|
||||
+ def previous_value(self, metric, instance=None):
|
||||
+ """Get previous value. Returns single value if instance given, else returns dict."""
|
||||
+ values_dict = self._get_values_dict(metric, use_previous=True)
|
||||
+ if instance is not None:
|
||||
+ return values_dict.get(instance)
|
||||
+ return values_dict
|
||||
+
|
||||
+ def current_value(self, metric, instance=None):
|
||||
+ """Get current value. Returns single value if instance given, else returns dict."""
|
||||
+ values_dict = self._get_values_dict(metric, use_previous=False)
|
||||
+ if instance is not None:
|
||||
+ return values_dict.get(instance)
|
||||
+ return values_dict
|
||||
+
|
||||
+ def previous_value(self, metric, instance=None):
|
||||
+ """Get previous value. Returns single value if instance given, else returns dict."""
|
||||
+ values_dict = self._get_values_dict(metric, use_previous=True)
|
||||
+ if instance is not None:
|
||||
+ return values_dict.get(instance)
|
||||
+ return values_dict
|
||||
|
||||
class NfsioStatUtil:
|
||||
def __init__(self,metrics_repository):
|
||||
self.__metric_repository=metrics_repository
|
||||
self.report=ReportingMetricRepository(self.__metric_repository)
|
||||
|
||||
- def mount_point(self):
|
||||
- return self.report.current_value('nfsclient.mountpoint')
|
||||
-
|
||||
- def mount_share(self):
|
||||
- return self.report.current_value('nfsclient.export')
|
||||
-
|
||||
- def mount_share_keys(self):
|
||||
- data = self.report.current_value('nfsclient.export')
|
||||
- return data.keys()
|
||||
-
|
||||
- def sample_time(self):
|
||||
- return self.report.current_value('nfsclient.age')
|
||||
-
|
||||
- def xprt_sends(self):
|
||||
- return self.report.current_value('nfsclient.xprt.sends')
|
||||
-
|
||||
- def xprt_backlog(self):
|
||||
- return self.report.current_value('nfsclient.xprt.backlog_u')
|
||||
-
|
||||
- def readops(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.ops')
|
||||
-
|
||||
- def readerrors(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.errors')
|
||||
-
|
||||
- def readexecute(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.execute')
|
||||
-
|
||||
- def readrtt(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.rtt')
|
||||
-
|
||||
- def readqueue(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.queue')
|
||||
-
|
||||
- def readbytesrecv(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.bytes_recv')
|
||||
-
|
||||
- def readbytessent(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.bytes_sent')
|
||||
-
|
||||
- def readntrans(self):
|
||||
- return self.report.current_value('nfsclient.ops.read.ntrans')
|
||||
-
|
||||
- def writeops(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.ops')
|
||||
-
|
||||
- def writeerrors(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.errors')
|
||||
-
|
||||
- def writeexecute(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.execute')
|
||||
-
|
||||
- def writertt(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.rtt')
|
||||
-
|
||||
- def writequeue(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.queue')
|
||||
-
|
||||
- def writebytesrecv(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.bytes_recv')
|
||||
-
|
||||
- def writebytessent(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.bytes_sent')
|
||||
-
|
||||
- def writentrans(self):
|
||||
- return self.report.current_value('nfsclient.ops.write.ntrans')
|
||||
-
|
||||
class NfsiostatReport(pmcc.MetricGroupPrinter):
|
||||
+ machine_info_count = 0
|
||||
+
|
||||
def __init__(self,opts,group):
|
||||
self.opts = opts
|
||||
self.group = group
|
||||
@@ -156,34 +112,67 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
|
||||
header_string += context['kernel.uname.machine'].netValues[0][2] + ' '
|
||||
print("%s (%s CPU)" % (header_string, self.__get_ncpu(context)))
|
||||
|
||||
- def __print_values(self,timestamp, nfsstatus):
|
||||
- n_shares = nfsstatus.mount_share_keys()
|
||||
- mountshare = nfsstatus.mount_share()
|
||||
- mountpoint = nfsstatus.mount_point()
|
||||
- sampletime = nfsstatus.sample_time()
|
||||
- sends = nfsstatus.xprt_sends()
|
||||
- backlog = nfsstatus.xprt_backlog()
|
||||
- readops = nfsstatus.readops()
|
||||
- readerrors = nfsstatus.readerrors()
|
||||
- readexecute = nfsstatus.readexecute()
|
||||
- readrtt = nfsstatus.readrtt()
|
||||
- readqueue = nfsstatus.readqueue()
|
||||
- readbytesrecv = nfsstatus.readbytesrecv()
|
||||
- readbytessent = nfsstatus.readbytessent()
|
||||
- readntrans = nfsstatus.readntrans()
|
||||
- writeops = nfsstatus.writeops()
|
||||
- writeerrors = nfsstatus.writeerrors()
|
||||
- writeexecute = nfsstatus.writeexecute()
|
||||
- writertt = nfsstatus.writertt()
|
||||
- writequeue = nfsstatus.writequeue()
|
||||
- writebytesrecv = nfsstatus.writebytesrecv()
|
||||
- writebytessent = nfsstatus.writebytessent()
|
||||
- writentrans = nfsstatus.writentrans()
|
||||
+ # --------------------------------------------------------
|
||||
+
|
||||
+ def __collect(self, nfs):
|
||||
+ return {
|
||||
+ metric: nfs.report.current_value(metric)
|
||||
+ for metric in NFSIOSTAT_METRICS
|
||||
+ }
|
||||
+
|
||||
+ # --------------------------------------------------------
|
||||
+
|
||||
+ def __delta(self, new: dict, nfs):
|
||||
+ delta = {}
|
||||
+ old = {
|
||||
+ metric: nfs.report.previous_value(metric)
|
||||
+ for metric in NFSIOSTAT_METRICS
|
||||
+ }
|
||||
+
|
||||
+ for metric in new:
|
||||
+ delta[metric] = {}
|
||||
+
|
||||
+ for inst in new[metric]:
|
||||
+ new_val = new[metric][inst]
|
||||
+ old_val = old.get(metric, {}).get(inst, 0)
|
||||
+
|
||||
+ # If value is numeric → subtract
|
||||
+ if isinstance(new_val, (int, float)):
|
||||
+ delta[metric][inst] = new_val - old_val
|
||||
+ else:
|
||||
+ # If string → just copy (no subtraction)
|
||||
+ delta[metric][inst] = new_val
|
||||
+
|
||||
+ return delta
|
||||
+
|
||||
+ def __print_values(self,timestamp, delta):
|
||||
+
|
||||
+ sampletime = delta["nfsclient.age"]
|
||||
+ readops = delta["nfsclient.ops.read.ops"]
|
||||
+ writeops = delta["nfsclient.ops.write.ops"]
|
||||
+ readbytesrecv = delta["nfsclient.ops.read.bytes_recv"]
|
||||
+ writebytesrecv = delta["nfsclient.ops.write.bytes_recv"]
|
||||
+ mountpoint = delta["nfsclient.mountpoint"]
|
||||
+ mountshare = delta["nfsclient.export"]
|
||||
+ sends = delta["nfsclient.xprt.sends"]
|
||||
+ backlog = delta["nfsclient.xprt.backlog_u"]
|
||||
+ readerrors = delta["nfsclient.ops.read.errors"]
|
||||
+ readexecute = delta["nfsclient.ops.read.execute"]
|
||||
+ readrtt = delta["nfsclient.ops.read.rtt"]
|
||||
+ readqueue = delta["nfsclient.ops.read.queue"]
|
||||
+ readbytessent = delta["nfsclient.ops.read.bytes_sent"]
|
||||
+ readntrans = delta["nfsclient.ops.read.ntrans"]
|
||||
+ writeerrors = delta["nfsclient.ops.write.errors"]
|
||||
+ writeexecute = delta["nfsclient.ops.write.execute"]
|
||||
+ writertt = delta["nfsclient.ops.write.rtt"]
|
||||
+ writequeue = delta["nfsclient.ops.write.queue"]
|
||||
+ writebytessent = delta["nfsclient.ops.write.bytes_sent"]
|
||||
+ writentrans = delta["nfsclient.ops.write.ntrans"]
|
||||
|
||||
print("%-18s:%s"%("Timestamp", timestamp))
|
||||
print()
|
||||
|
||||
- for name in n_shares:
|
||||
+ for name in mountshare:
|
||||
# read
|
||||
r_kilobytes = (readbytessent[name] + readbytesrecv[name]) / 1024
|
||||
if sampletime[name] > 0:
|
||||
@@ -277,13 +266,24 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
|
||||
)
|
||||
print()
|
||||
|
||||
- def print_report(self,group,timestamp, manager_nfsiostat):
|
||||
+ def get_timestamp(self, group):
|
||||
+ t_s = group.contextCache.pmLocaltime(int(group.timestamp))
|
||||
+ timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time())
|
||||
+ return timestamp
|
||||
+
|
||||
+ def print_report(self,group, manager_nfsiostat, mgr):
|
||||
def __print_nfs_status():
|
||||
- nfsstatus = NfsioStatUtil(manager_nfsiostat)
|
||||
- if nfsstatus.mount_share():
|
||||
+ timestamp = self.get_timestamp(group)
|
||||
+ nfs = NfsioStatUtil(manager_nfsiostat)
|
||||
+ if nfs.report.current_value("nfsclient.export"):
|
||||
try:
|
||||
- self.__print_machine_info(group)
|
||||
- self.__print_values(timestamp, nfsstatus)
|
||||
+ if self.machine_info_count == 0:
|
||||
+ self.__print_machine_info(group)
|
||||
+ self.machine_info_count = 1
|
||||
+ current = self.__collect(nfs)
|
||||
+ diff_dict = self.__delta(current, nfs)
|
||||
+ self. __print_values(timestamp, diff_dict)
|
||||
+
|
||||
except IndexError:
|
||||
print("Incorrect machine info due to some missing metrics")
|
||||
return
|
||||
@@ -292,7 +292,6 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
|
||||
|
||||
if self.context != PM_CONTEXT_ARCHIVE and self.samples is None:
|
||||
__print_nfs_status()
|
||||
- sys.exit(0)
|
||||
elif self.context == PM_CONTEXT_ARCHIVE and self.samples is None:
|
||||
__print_nfs_status()
|
||||
elif self.samples >=1:
|
||||
@@ -302,30 +301,52 @@ class NfsiostatReport(pmcc.MetricGroupPrinter):
|
||||
pass
|
||||
|
||||
def report(self, manager):
|
||||
- group = manager["sysinfo"]
|
||||
self.samples = self.opts.pmGetOptionSamples()
|
||||
- t_s = group.contextCache.pmLocaltime(int(group.timestamp))
|
||||
- timestamp = time.strftime(NfsiostatOptions.timefmt, t_s.struct_time())
|
||||
- self.print_report(group,timestamp,manager['nfsiostat'])
|
||||
+ self.print_report(manager["sysinfo"] ,manager['nfsiostat'], manager)
|
||||
|
||||
class NfsiostatOptions(pmapi.pmOptions):
|
||||
timefmt = "%m/%d/%Y %H:%M:%S"
|
||||
+ uflag = False
|
||||
+ def checkOptions(self, manager):
|
||||
+ if NfsiostatOptions.uflag:
|
||||
+ if manager._options.pmGetOptionInterval():
|
||||
+ print("Error: -t incompatible with -u")
|
||||
+ return False
|
||||
+ if manager.type != PM_CONTEXT_ARCHIVE:
|
||||
+ print("Error: -u can only be specified with -a archive")
|
||||
+ return False
|
||||
+ return True
|
||||
+
|
||||
+ def extraOptions(self, opt, optarg, index):
|
||||
+ if opt == "u":
|
||||
+ NfsiostatOptions.uflag = True
|
||||
+
|
||||
def __init__(self):
|
||||
- pmapi.pmOptions.__init__(self, "a:s:Z:zV?")
|
||||
+ pmapi.pmOptions.__init__(self, "a:s:Z:t:uzV?")
|
||||
+ self.pmSetOptionCallback(self.extraOptions)
|
||||
self.pmSetLongOptionHeader("General options")
|
||||
self.pmSetLongOptionHostZone()
|
||||
self.pmSetLongOptionTimeZone()
|
||||
- self.pmSetLongOptionHelp()
|
||||
+ self.pmSetLongOptionArchive()
|
||||
self.pmSetLongOptionSamples()
|
||||
+ self.pmSetLongOptionInterval()
|
||||
+ self.pmSetLongOption("no-interpolation", 0, "u", "", "disable interpolation mode with archives")
|
||||
+ self.pmSetLongOptionHelp()
|
||||
self.pmSetLongOptionVersion()
|
||||
- self.samples=None
|
||||
- self.context=None
|
||||
+ self.context = None
|
||||
+ self.samples = None
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
opts = NfsiostatOptions()
|
||||
mngr = pmcc.MetricGroupManager.builder(opts,sys.argv)
|
||||
opts.context=mngr.type
|
||||
+ if not opts.checkOptions(mngr):
|
||||
+ raise pmapi.pmUsageErr
|
||||
+
|
||||
+ if NfsiostatOptions.uflag:
|
||||
+ # -u turns off interpolation
|
||||
+ mngr.pmSetMode(PM_MODE_FORW, mngr._options.pmGetOptionOrigin(), None)
|
||||
missing = mngr.checkMissingMetrics(ALL_METRICS)
|
||||
if missing is not None:
|
||||
sys.stderr.write('Error: not all required metrics are available\nMissing %s\n' % missing)
|
||||
--
|
||||
2.43.7
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
260
pcp-7.0.3-CVE-2026-16524.patch
Normal file
260
pcp-7.0.3-CVE-2026-16524.patch
Normal file
@ -0,0 +1,260 @@
|
||||
From c5cbeceb7d Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] linux_sockets pmda: fix command injection via network.persocket.filter (CWE-78)
|
||||
|
||||
The sockets_check_filter() validation helper returns 1 for safe input
|
||||
and 0 for unsafe input. The guard in sockets_store() tested
|
||||
if (sockets_check_filter(av.cp)) — rejecting safe input and accepting
|
||||
malicious input containing shell metacharacters. The accepted filter
|
||||
was later passed to popen() via shell interpretation, enabling arbitrary
|
||||
command execution as the PMDA process user.
|
||||
|
||||
Fix:
|
||||
- Invert the guard: if (!sockets_check_filter(av.cp))
|
||||
- Replace popen()/pclose() in ss_open_stream() with the libpcp
|
||||
__pmProcessAddArg()/__pmProcessPipe()/__pmProcessPipeClose() API
|
||||
which uses execvp() internally, eliminating shell interpretation
|
||||
of the filter string entirely
|
||||
- Add qa/2101 verifying that valid filters are accepted and shell
|
||||
metacharacters (semicolons, backticks, pipes) are rejected
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
|
||||
Resolves: RHEL-213659
|
||||
---
|
||||
diff --git a/qa/2101 b/qa/2101
|
||||
new file mode 100755
|
||||
index 0000000000..733b7f70c9
|
||||
--- /dev/null
|
||||
+++ b/qa/2101
|
||||
@@ -0,0 +1,70 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2101
|
||||
+# Verify linux_sockets PMDA filter validation rejects shell metacharacters
|
||||
+# and accepts valid filter expressions (CWE-78 fix verification)
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+[ $PCP_PLATFORM = linux ] || _notrun "Linux-specific sockets testing"
|
||||
+[ -f $PCP_PMDAS_DIR/sockets/pmdasockets ] || _notrun "sockets PMDA not installed"
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ _cleanup_pmda sockets
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+_prepare_pmda sockets
|
||||
+_stop_auto_restart pmcd
|
||||
+
|
||||
+# install the sockets PMDA
|
||||
+cd $PCP_PMDAS_DIR/sockets
|
||||
+$sudo ./Remove >/dev/null 2>&1
|
||||
+$sudo ./Install </dev/null >$tmp.out 2>&1
|
||||
+cat $tmp.out >>$seq_full
|
||||
+
|
||||
+# check the PMDA is alive
|
||||
+pmprobe -v network.persocket.filter >$tmp.probe 2>&1
|
||||
+grep -q 'No PMCD agent' $tmp.probe && _notrun "sockets PMDA failed to install"
|
||||
+
|
||||
+# real QA test starts here
|
||||
+
|
||||
+echo "=== valid filter should be accepted ==="
|
||||
+pmstore network.persocket.filter "sport == 22" 2>&1 \
|
||||
+| grep -q 'Bad input' && echo "FAIL: valid filter rejected" || echo "valid filter accepted"
|
||||
+
|
||||
+echo
|
||||
+echo "=== shell metacharacter semicolon should be rejected ==="
|
||||
+pmstore network.persocket.filter ';id' 2>&1 \
|
||||
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
|
||||
+
|
||||
+echo
|
||||
+echo "=== shell metacharacter backtick should be rejected ==="
|
||||
+pmstore network.persocket.filter '`id`' 2>&1 \
|
||||
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
|
||||
+
|
||||
+echo
|
||||
+echo "=== shell metacharacter pipe should be rejected ==="
|
||||
+pmstore network.persocket.filter '|cat /etc/passwd' 2>&1 \
|
||||
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
|
||||
+
|
||||
+echo
|
||||
+echo "=== shell metacharacter dollar should be rejected ==="
|
||||
+pmstore network.persocket.filter '${IFS}id' 2>&1 \
|
||||
+| grep -q 'Bad input' && echo "metacharacter rejected" || echo "FAIL: metacharacter not rejected"
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2101.out b/qa/2101.out
|
||||
new file mode 100644
|
||||
index 0000000000..02f9ac5655
|
||||
--- /dev/null
|
||||
+++ b/qa/2101.out
|
||||
@@ -0,0 +1,15 @@
|
||||
+QA output created by 2101
|
||||
+=== valid filter should be accepted ===
|
||||
+valid filter accepted
|
||||
+
|
||||
+=== shell metacharacter semicolon should be rejected ===
|
||||
+metacharacter rejected
|
||||
+
|
||||
+=== shell metacharacter backtick should be rejected ===
|
||||
+metacharacter rejected
|
||||
+
|
||||
+=== shell metacharacter pipe should be rejected ===
|
||||
+metacharacter rejected
|
||||
+
|
||||
+=== shell metacharacter dollar should be rejected ===
|
||||
+metacharacter rejected
|
||||
diff --git a/qa/group b/qa/group
|
||||
index 1533ad5720..2c6529b718 100644
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2300,8 +2300,9 @@
|
||||
1994 pcp rocestat python local
|
||||
1995 pmda.linux local
|
||||
1996 pmda.infiniband local
|
||||
2105 libpcp pmcd local security pmcd.pdu
|
||||
2100 pmproxy local security
|
||||
2104 libpcp local security
|
||||
+2101 pmda.sockets local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/src/pmdas/linux_sockets/pmda.c b/src/pmdas/linux_sockets/pmda.c
|
||||
index 4d51207275..2cb46d8e89 100644
|
||||
--- a/src/pmdas/linux_sockets/pmda.c
|
||||
+++ b/src/pmdas/linux_sockets/pmda.c
|
||||
@@ -162,11 +162,9 @@ sockets_check_filter(const char *string)
|
||||
const char *p;
|
||||
|
||||
for (p = string; *p; p++) {
|
||||
- if (isspace(*p))
|
||||
+ if (isspace(*p) || isalnum(*p))
|
||||
continue;
|
||||
- if (isalnum(*p))
|
||||
- continue;
|
||||
- if (*p == '(' || *p == ')')
|
||||
+ if (strchr("()=!<>:.*/-,", *p) != NULL)
|
||||
continue;
|
||||
return 0; /* disallow */
|
||||
}
|
||||
@@ -191,7 +189,7 @@ sockets_store(pmdaResult *result, pmdaExt *pmda)
|
||||
case 0: /* network.persocket.filter */
|
||||
if ((sts = pmExtractValue(vsp->valfmt, &vsp->vlist[0],
|
||||
PM_TYPE_STRING, &av, PM_TYPE_STRING)) >= 0) {
|
||||
- if (sockets_check_filter(av.cp)) {
|
||||
+ if (!sockets_check_filter(av.cp)) {
|
||||
sts = PM_ERR_BADSTORE;
|
||||
free(av.cp);
|
||||
break;
|
||||
diff --git a/src/pmdas/linux_sockets/ss_stream.c b/src/pmdas/linux_sockets/ss_stream.c
|
||||
index 421c65fd16..833fc275a0 100644
|
||||
--- a/src/pmdas/linux_sockets/ss_stream.c
|
||||
+++ b/src/pmdas/linux_sockets/ss_stream.c
|
||||
@@ -14,18 +14,19 @@
|
||||
|
||||
#include <pcp/pmapi.h>
|
||||
#include <pcp/pmda.h>
|
||||
+#include <pcp/libpcp.h>
|
||||
#include "ss_stats.h"
|
||||
|
||||
#define SS_OPTIONS "-noemitauO"
|
||||
|
||||
-char *ss_filter = NULL; /* storable: network.persocket.filter */
|
||||
+char *ss_filter; /* storable: network.persocket.filter */
|
||||
+static int using_pipe; /* pipe is normal operation, QA uses files */
|
||||
|
||||
FILE *
|
||||
ss_open_stream()
|
||||
{
|
||||
- FILE *fp;
|
||||
+ FILE *fp = NULL;
|
||||
char *path;
|
||||
- char cmd[MAXPATHLEN];
|
||||
|
||||
if (ss_filter == NULL) {
|
||||
/* pmstore to network.persocket.filter frees this if changing */
|
||||
@@ -38,17 +39,51 @@ ss_open_stream()
|
||||
fp = fopen(path, "r");
|
||||
if (pmDebugOptions.appl0)
|
||||
fprintf(stderr, "ss_open_stream: open PCPQA_PMDA_SOCKETS=%s\n", path);
|
||||
+ using_pipe = 0;
|
||||
} else {
|
||||
+ __pmExecCtl_t *argp = NULL;
|
||||
+ int sts;
|
||||
+
|
||||
if (access((path = "/usr/sbin/ss"), X_OK) != 0) {
|
||||
if (access((path = "/usr/bin/ss"), X_OK) != 0) {
|
||||
fprintf(stderr, "Error: no \"ss\" binary found\n");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
- pmsprintf(cmd, sizeof(cmd), "%s %s %s", path, SS_OPTIONS, ss_filter);
|
||||
- fp = popen(cmd, "r");
|
||||
+ if ((sts = __pmProcessAddArg(&argp, path)) < 0 ||
|
||||
+ (sts = __pmProcessAddArg(&argp, SS_OPTIONS)) < 0) {
|
||||
+ if (pmDebugOptions.appl0)
|
||||
+ fprintf(stderr, "ss_open_stream: __pmProcessAddArg failed: %s\n",
|
||||
+ pmErrStr(sts));
|
||||
+ return NULL;
|
||||
+ }
|
||||
+ if (ss_filter[0] != '\0') {
|
||||
+ char *s, *tok, *saveptr;
|
||||
+
|
||||
+ if ((s = strdup(ss_filter)) == NULL)
|
||||
+ return NULL;
|
||||
+ for (tok = strtok_r(s, " \t", &saveptr); tok != NULL;
|
||||
+ tok = strtok_r(NULL, " \t", &saveptr)) {
|
||||
+ if ((sts = __pmProcessAddArg(&argp, tok)) < 0) {
|
||||
+ free(s);
|
||||
+ if (pmDebugOptions.appl0)
|
||||
+ fprintf(stderr, "ss_open_stream: __pmProcessAddArg failed: %s\n",
|
||||
+ pmErrStr(sts));
|
||||
+ return NULL;
|
||||
+ }
|
||||
+ }
|
||||
+ free(s);
|
||||
+ }
|
||||
+ if ((sts = __pmProcessPipe(&argp, "r", PM_EXEC_TOSS_NONE, &fp)) < 0) {
|
||||
+ if (pmDebugOptions.appl0)
|
||||
+ fprintf(stderr, "ss_open_stream: __pmProcessPipe failed: %s\n",
|
||||
+ pmErrStr(sts));
|
||||
+ return NULL;
|
||||
+ }
|
||||
if (pmDebugOptions.appl0)
|
||||
- fprintf(stderr, "ss_open_stream: popen %s\n", cmd);
|
||||
+ fprintf(stderr, "ss_open_stream: exec %s %s %s\n",
|
||||
+ path, SS_OPTIONS, ss_filter);
|
||||
+ using_pipe = 1;
|
||||
}
|
||||
|
||||
return fp;
|
||||
@@ -57,8 +92,8 @@ ss_open_stream()
|
||||
void
|
||||
ss_close_stream(FILE *fp)
|
||||
{
|
||||
- if (getenv("PCPQA_PMDA_SOCKETS") != NULL)
|
||||
- fclose(fp);
|
||||
+ if (using_pipe)
|
||||
+ __pmProcessPipeClose(fp);
|
||||
else
|
||||
- pclose(fp);
|
||||
+ fclose(fp);
|
||||
}
|
||||
228
pcp-7.0.3-CVE-2026-16526.patch
Normal file
228
pcp-7.0.3-CVE-2026-16526.patch
Normal file
@ -0,0 +1,228 @@
|
||||
From 7e27614006 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp, libpcp_pmda: set FD_CLOEXEC on AF_UNIX sockets (CWE-403)
|
||||
|
||||
The __pmInitSocket() function returns early for AF_UNIX sockets,
|
||||
skipping all subsequent socket hardening including FD_CLOEXEC.
|
||||
This causes the pmdaroot Unix socket fd to be inherited by child
|
||||
processes spawned via popen()/fork(), enabling privilege escalation
|
||||
when combined with the linux_sockets command injection (vuln 3):
|
||||
an attacker's popen() child inherits the pmdaroot fd and can send
|
||||
a PDUROOT_STARTPMDA_REQ to execute commands as root.
|
||||
|
||||
Fix:
|
||||
- Set FD_CLOEXEC on AF_UNIX sockets in __pmInitSocket() before the
|
||||
early return, matching the behavior TCP sockets get via
|
||||
__pmConnectRestoreFlags()
|
||||
- Set FD_CLOEXEC on pmdarootfd in pmdaRootConnect() after connect()
|
||||
succeeds, as belt-and-suspenders for this critical fd
|
||||
- Add qa/src/check_cloexec.c and qa/2104 verifying FD_CLOEXEC is set
|
||||
on sockets created by __pmCreateUnixSocket()
|
||||
|
||||
Note: SO_PEERCRED peer credential verification on the pmdaroot server
|
||||
side is a separate hardening measure to be addressed as a follow-up.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
|
||||
Resolves: RHEL-213695
|
||||
---
|
||||
diff --git a/qa/2104 b/qa/2104
|
||||
new file mode 100755
|
||||
index 0000000000..0b2b0b602e
|
||||
--- /dev/null
|
||||
+++ b/qa/2104
|
||||
@@ -0,0 +1,31 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2104
|
||||
+# Verify AF_UNIX sockets have FD_CLOEXEC set (CWE-403 fix)
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+[ -f src/check_cloexec ] || _notrun "check_cloexec not built"
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+# real QA test starts here
|
||||
+src/check_cloexec
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2104.out b/qa/2104.out
|
||||
new file mode 100644
|
||||
index 0000000000..900a95ec75
|
||||
--- /dev/null
|
||||
+++ b/qa/2104.out
|
||||
@@ -0,0 +1,2 @@
|
||||
+QA output created by 2104
|
||||
+FD_CLOEXEC is set
|
||||
diff --git a/qa/group b/qa/group
|
||||
index 92c27dc49f..1533ad5720 100644
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2300,7 +2300,8 @@
|
||||
1994 pcp rocestat python local
|
||||
1995 pmda.linux local
|
||||
1996 pmda.infiniband local
|
||||
2105 libpcp pmcd local security pmcd.pdu
|
||||
2100 pmproxy local security
|
||||
+2104 libpcp local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/qa/src/GNUlocaldefs b/qa/src/GNUlocaldefs
|
||||
index dd152a6412..937d263893 100644
|
||||
--- a/qa/src/GNUlocaldefs
|
||||
+++ b/qa/src/GNUlocaldefs
|
||||
@@ -56,6 +56,7 @@
|
||||
throttle.c throttle_timeout.c y2038.c bigpmcdpmids.c pdu-gadget.c \
|
||||
strnfoo.c mmv_ondisk.c newcontext.c api_abi.c interp_bug3.c \
|
||||
- pmsetmode.c scanindex.c localtime.c httpcache.c unregister.c
|
||||
+ pmsetmode.c scanindex.c localtime.c httpcache.c unregister.c \
|
||||
+ check_cloexec.c
|
||||
|
||||
ifeq ($(shell test -f ../localconfig && echo 1), 1)
|
||||
include ../localconfig
|
||||
@@ -637,6 +638,11 @@
|
||||
# --- need libpcp_import
|
||||
#
|
||||
|
||||
+check_cloexec: check_cloexec.c
|
||||
+ rm -f $@
|
||||
+ $(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS)
|
||||
+ $(LINKER_MAKERULE)
|
||||
+
|
||||
check_import: check_import.c
|
||||
rm -f $@
|
||||
$(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS) -lpcp_import
|
||||
@@ -953,6 +959,8 @@ xmktime.o: libpcp.h
|
||||
xxx.o: libpcp.h
|
||||
y2038.o: libpcp.h
|
||||
|
||||
+check_cloexec.o: libpcp.h
|
||||
+
|
||||
bozo:
|
||||
@echo CFILES_TARGETS=$(CFILES_TARGETS)
|
||||
@echo "patsubst ->" $(patsubst %.c,%,$(CFILES_TARGETS))
|
||||
|
||||
diff --git a/qa/src/check_cloexec.c b/qa/src/check_cloexec.c
|
||||
new file mode 100644
|
||||
index 0000000000..ebc438d301
|
||||
--- /dev/null
|
||||
+++ b/qa/src/check_cloexec.c
|
||||
@@ -0,0 +1,38 @@
|
||||
+/*
|
||||
+ * Verify that AF_UNIX sockets created by __pmCreateUnixSocket()
|
||||
+ * have FD_CLOEXEC set.
|
||||
+ */
|
||||
+
|
||||
+#include <pcp/pmapi.h>
|
||||
+#include "libpcp.h"
|
||||
+#include <fcntl.h>
|
||||
+
|
||||
+int
|
||||
+main(int argc, char **argv)
|
||||
+{
|
||||
+ int fd, flags;
|
||||
+
|
||||
+ pmSetProgname(argv[0]);
|
||||
+
|
||||
+ fd = __pmCreateUnixSocket();
|
||||
+ if (fd < 0) {
|
||||
+ fprintf(stderr, "Error: __pmCreateUnixSocket failed: %s\n",
|
||||
+ pmErrStr(fd));
|
||||
+ return 1;
|
||||
+ }
|
||||
+
|
||||
+ flags = fcntl(fd, F_GETFD);
|
||||
+ if (flags < 0) {
|
||||
+ fprintf(stderr, "Error: fcntl F_GETFD failed\n");
|
||||
+ close(fd);
|
||||
+ return 1;
|
||||
+ }
|
||||
+
|
||||
+ if (flags & FD_CLOEXEC)
|
||||
+ printf("FD_CLOEXEC is set\n");
|
||||
+ else
|
||||
+ printf("FAIL: FD_CLOEXEC is NOT set\n");
|
||||
+
|
||||
+ close(fd);
|
||||
+ return 0;
|
||||
+}
|
||||
diff --git a/src/libpcp/src/auxconnect.c b/src/libpcp/src/auxconnect.c
|
||||
index 4cac85c147..6e29976b2a 100644
|
||||
--- a/src/libpcp/src/auxconnect.c
|
||||
+++ b/src/libpcp/src/auxconnect.c
|
||||
@@ -514,8 +514,12 @@ __pmInitSocket(int fd, int family)
|
||||
}
|
||||
|
||||
#if defined(HAVE_STRUCT_SOCKADDR_UN)
|
||||
- if (family == AF_UNIX)
|
||||
+ if (family == AF_UNIX) {
|
||||
+ int fdFlags;
|
||||
+ if ((fdFlags = __pmGetFileDescriptorFlags(fd)) >= 0)
|
||||
+ __pmSetFileDescriptorFlags(fd, fdFlags | FD_CLOEXEC);
|
||||
return fd;
|
||||
+ }
|
||||
#endif
|
||||
|
||||
/* Avoid 200 ms delay. This option is not supported for unix domain sockets. */
|
||||
diff --git a/src/libpcp3/src/auxconnect.c b/src/libpcp3/src/auxconnect.c
|
||||
index 9d82ae539b..3ac64e79aa 100644
|
||||
--- a/src/libpcp3/src/auxconnect.c
|
||||
+++ b/src/libpcp3/src/auxconnect.c
|
||||
@@ -516,8 +516,12 @@ __pmInitSocket(int fd, int family)
|
||||
}
|
||||
|
||||
#if defined(HAVE_STRUCT_SOCKADDR_UN)
|
||||
- if (family == AF_UNIX)
|
||||
+ if (family == AF_UNIX) {
|
||||
+ int fdFlags;
|
||||
+ if ((fdFlags = __pmGetFileDescriptorFlags(fd)) >= 0)
|
||||
+ __pmSetFileDescriptorFlags(fd, fdFlags | FD_CLOEXEC);
|
||||
return fd;
|
||||
+ }
|
||||
#endif
|
||||
|
||||
/* Avoid 200 ms delay. This option is not supported for unix domain sockets. */
|
||||
diff --git a/src/libpcp_pmda/src/root.c b/src/libpcp_pmda/src/root.c
|
||||
index 06f52b1265..a2897730eb 100644
|
||||
--- a/src/libpcp_pmda/src/root.c
|
||||
+++ b/src/libpcp_pmda/src/root.c
|
||||
@@ -32,7 +32,7 @@ pmdaRootConnect(const char *path)
|
||||
char *tmpdir;
|
||||
char socketpath[MAXPATHLEN];
|
||||
char errmsg[PM_MAXERRMSGLEN];
|
||||
- int fd, sts, version, features;
|
||||
+ int fd, sts, version, features, fdFlags;
|
||||
|
||||
/* Initialize the socket address. */
|
||||
if ((addr = __pmSockAddrAlloc()) == NULL)
|
||||
@@ -71,6 +71,9 @@ pmdaRootConnect(const char *path)
|
||||
return sts;
|
||||
}
|
||||
|
||||
+ if ((fdFlags = __pmGetFileDescriptorFlags(fd)) >= 0)
|
||||
+ __pmSetFileDescriptorFlags(fd, fdFlags | FD_CLOEXEC);
|
||||
+
|
||||
/* Check server connection information */
|
||||
if ((sts = __pmdaRecvRootPDUInfo(fd, &version, &features)) < 0) {
|
||||
pmNotifyErr(LOG_ERR,
|
||||
198
pcp-7.0.3-CVE-2026-16527.patch
Normal file
198
pcp-7.0.3-CVE-2026-16527.patch
Normal file
@ -0,0 +1,198 @@
|
||||
From d96ba5a716 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] pmproxy: fix missing -Q and -S authentication flags (CWE-306)
|
||||
|
||||
The pmproxy -Q (require client certificate) and -S (require
|
||||
authenticated clients) flags existed as case blocks in the option
|
||||
parser but were absent from the short_options string and the longopts
|
||||
table, making them permanently unreachable. An unauthenticated HTTP
|
||||
client could access all REST API endpoints including /store and /derive.
|
||||
|
||||
Fix:
|
||||
- Add Q and S to short_options so pmgetopt_r() delivers them
|
||||
- Add --certreqd and --reqauth entries to the longopts table
|
||||
- Document both flags in the pmproxy(1) man page
|
||||
- Add qa/2100 verifying the flags are accepted and that -S correctly
|
||||
rejects unauthenticated REST API requests with HTTP 403
|
||||
|
||||
Note: -S enforcement in the REST API path already exists in http.c and
|
||||
webapi.c. -Q (CERT_REQD) enforcement is only implemented for the
|
||||
legacy PCP wire protocol path, not the REST API; this is a pre-existing
|
||||
limitation to be addressed separately.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
|
||||
Resolves: RHEL-213721
|
||||
---
|
||||
diff --git a/man/man1/pmproxy.1 b/man/man1/pmproxy.1
|
||||
index 6fa0ba94c1..af173edb91 100644
|
||||
--- a/man/man1/pmproxy.1
|
||||
+++ b/man/man1/pmproxy.1
|
||||
@@ -234,6 +234,9 @@ Specify an alternate
|
||||
number to listen on for client connections.
|
||||
The default value is 44322.
|
||||
.TP
|
||||
+\fB\-Q\f1, \fB\-\-certreqd\f1
|
||||
+Require that all client connections provide a trusted client certificate.
|
||||
+.TP
|
||||
\f3\-r\f1 \f2port\f1, \f3\-\-keyport\f1=\f2port\f1
|
||||
Specify an alternate key-value server
|
||||
.I port
|
||||
@@ -248,6 +251,9 @@ The default value is
|
||||
.IR $PCP_RUN_DIR/pmproxy.socket .
|
||||
This option implies \f3pmproxy\f1 is running in \f3timeseries\f1 mode.
|
||||
.TP
|
||||
+\fB\-S\f1, \fB\-\-reqauth\f1
|
||||
+Require that all client connections be authenticated.
|
||||
+.TP
|
||||
\fB\-t\f1, \fB\-\-timeseries\f1
|
||||
Operate in automatic archive timeseries discovery mode.
|
||||
This mode of operation will enable the
|
||||
diff --git a/qa/2100 b/qa/2100
|
||||
new file mode 100755
|
||||
index 0000000000..1de957a2f1
|
||||
--- /dev/null
|
||||
+++ b/qa/2100
|
||||
@@ -0,0 +1,85 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2100
|
||||
+# Verify pmproxy -Q and -S authentication flags are accepted
|
||||
+# and that -S (reqauth) enforces authentication on REST API
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+which curl >/dev/null 2>&1 || _notrun "no curl executable installed"
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ [ -n "$__pid" ] && kill $__pid 2>/dev/null
|
||||
+ wait $__pid 2>/dev/null
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+__pid=""
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+# real QA test starts here
|
||||
+
|
||||
+echo "=== checking -Q and -S appear in usage ==="
|
||||
+pmproxy --help 2>&1 | grep -E '\-[QS]' | sed -e 's/^ *//'
|
||||
+
|
||||
+echo
|
||||
+echo "=== checking -S enforces authentication on REST API ==="
|
||||
+__port=`_find_free_port`
|
||||
+$PCP_BINADM_DIR/pmproxy -S -f -p $__port -l $tmp.log &
|
||||
+__pid=$!
|
||||
+sleep 1
|
||||
+if kill -0 $__pid 2>/dev/null; then
|
||||
+ echo "pmproxy with -S started"
|
||||
+
|
||||
+ # unauthenticated request should be rejected
|
||||
+ __code=`curl -s -o /dev/null -w '%{http_code}' "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null`
|
||||
+ if [ "$__code" = "403" ]; then
|
||||
+ echo "unauthenticated request correctly rejected (HTTP $__code)"
|
||||
+ else
|
||||
+ echo "FAIL: expected HTTP 403, got HTTP $__code"
|
||||
+ fi
|
||||
+
|
||||
+ kill $__pid
|
||||
+ wait $__pid 2>/dev/null
|
||||
+ __pid=""
|
||||
+else
|
||||
+ echo "FAIL: pmproxy with -S did not start"
|
||||
+fi
|
||||
+
|
||||
+echo
|
||||
+echo "=== checking without -S allows unauthenticated access ==="
|
||||
+__port=`_find_free_port`
|
||||
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log2 &
|
||||
+__pid=$!
|
||||
+sleep 1
|
||||
+if kill -0 $__pid 2>/dev/null; then
|
||||
+ echo "pmproxy without -S started"
|
||||
+
|
||||
+ # unauthenticated request should succeed
|
||||
+ __code=`curl -s -o /dev/null -w '%{http_code}' "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null`
|
||||
+ if [ "$__code" = "200" ]; then
|
||||
+ echo "unauthenticated request correctly allowed (HTTP $__code)"
|
||||
+ else
|
||||
+ echo "FAIL: expected HTTP 200, got HTTP $__code"
|
||||
+ fi
|
||||
+
|
||||
+ kill $__pid
|
||||
+ wait $__pid 2>/dev/null
|
||||
+ __pid=""
|
||||
+else
|
||||
+ echo "FAIL: pmproxy without -S did not start"
|
||||
+fi
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2100.out b/qa/2100.out
|
||||
new file mode 100644
|
||||
index 0000000000..5302aea77a
|
||||
--- /dev/null
|
||||
+++ b/qa/2100.out
|
||||
@@ -0,0 +1,12 @@
|
||||
+QA output created by 2100
|
||||
+=== checking -Q and -S appear in usage ===
|
||||
+-Q, --certreqd require client certificate authentication
|
||||
+-S, --reqauth require all client connections to be authenticated
|
||||
+
|
||||
+=== checking -S enforces authentication on REST API ===
|
||||
+pmproxy with -S started
|
||||
+unauthenticated request correctly rejected (HTTP 403)
|
||||
+
|
||||
+=== checking without -S allows unauthenticated access ===
|
||||
+pmproxy without -S started
|
||||
+unauthenticated request correctly allowed (HTTP 200)
|
||||
diff --git a/qa/group b/qa/group
|
||||
index 3590d90dc2..92c27dc49f 100644
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2300,6 +2300,7 @@
|
||||
1994 pcp rocestat python local
|
||||
1995 pmda.linux local
|
||||
1996 pmda.infiniband local
|
||||
2105 libpcp pmcd local security pmcd.pdu
|
||||
+2100 pmproxy local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/src/pmproxy/src/pmproxy.c b/src/pmproxy/src/pmproxy.c
|
||||
index ce2a9ebf53..5db0d5fdb9 100644
|
||||
--- a/src/pmproxy/src/pmproxy.c
|
||||
+++ b/src/pmproxy/src/pmproxy.c
|
||||
@@ -82,7 +82,9 @@ static pmLongOptions longopts[] = {
|
||||
PMAPI_OPTIONS_HEADER("Connection options"),
|
||||
{ "interface", 1, 'i', "ADDR", "accept connections on this IP address" },
|
||||
{ "port", 1, 'p', "PORT", "accept connections on this port" },
|
||||
+ { "certreqd", 0, 'Q', 0, "require client certificate authentication" },
|
||||
{ "socket", 1, 's', "PATH", "Unix domain socket file [default $PCP_RUN_DIR/pmproxy.socket]" },
|
||||
+ { "reqauth", 0, 'S', 0, "require all client connections to be authenticated" },
|
||||
{ "keyport", 1, 'r', "PORT", "Connect to key server on this TCP/IP port (implies --timeseries)" },
|
||||
{ "keyhost", 1, 'h', "HOST", "Connect to key server on this host name (implies --timeseries)" },
|
||||
{ "redisport", 1, 'r', "PORT", "Backwards-compatibility option, do not use" },
|
||||
@@ -95,7 +97,7 @@ static pmLongOptions longopts[] = {
|
||||
};
|
||||
|
||||
static pmOptions opts = {
|
||||
- .short_options = "Ac:dD:Ffh:i:l:L:p:r:s:tT:U:x:?",
|
||||
+ .short_options = "Ac:dD:Ffh:i:l:L:p:Qr:s:StT:U:x:?",
|
||||
.long_options = longopts,
|
||||
};
|
||||
|
||||
145
pcp-7.0.3-CVE-2026-16529.patch
Normal file
145
pcp-7.0.3-CVE-2026-16529.patch
Normal file
@ -0,0 +1,145 @@
|
||||
From ef848fb978 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp: fix integer overflow in __pmGetPDU() (CWE-190)
|
||||
|
||||
When php->len is near INT_MAX (e.g. 0x7FFFFFFF), the buffer size
|
||||
computation PDU_CHUNK * (1 + php->len / PDU_CHUNK) overflows signed
|
||||
int, producing a negative value that permanently corrupts the static
|
||||
maxsize variable. Every subsequent __pmFindPDUBuf() call returns NULL,
|
||||
rendering the affected daemon (pmlogger, pmcd) unable to process any
|
||||
further PDUs for the remainder of its lifetime — a persistent denial
|
||||
of service requiring a restart.
|
||||
|
||||
Fix: add an overflow guard (php->len > INT_MAX - PDU_CHUNK) before
|
||||
the multiplication, returning PM_ERR_TOOBIG for absurdly large PDU
|
||||
lengths. This protects the NO_LIMIT code path used by pmcd and
|
||||
pmlogger that is not covered by the existing ceiling check.
|
||||
|
||||
Also add _filter_pmcd() to qa/common.pmcd.pdu to normalize fd=N in
|
||||
pmcd log output, and qa/2105 with a crafted PDU exercising the
|
||||
overflow.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
|
||||
Resolves: RHEL-213732
|
||||
---
|
||||
diff --git a/qa/2105 b/qa/2105
|
||||
new file mode 100755
|
||||
index 0000000000..44b849ad39
|
||||
--- /dev/null
|
||||
+++ b/qa/2105
|
||||
@@ -0,0 +1,17 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2105
|
||||
+# Verify __pmGetPDU rejects PDU with len near INT_MAX
|
||||
+# (integer overflow in buffer size computation, CWE-190)
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+pdu_data=pdudata/pdu-getpdu-overflow
|
||||
+grep_pattern="bad PDU len=.*exceeds maximum|PDU len=.*too large"
|
||||
+
|
||||
+# this is one of the generic pmcd PDU exerciser tests ...
|
||||
+#
|
||||
+. ./common.pmcd.pdu
|
||||
diff --git a/qa/2105.out b/qa/2105.out
|
||||
new file mode 100644
|
||||
index 0000000000..a304f9d913
|
||||
--- /dev/null
|
||||
+++ b/qa/2105.out
|
||||
@@ -0,0 +1,10 @@
|
||||
+QA output created by 2105
|
||||
+expect error(s) to be logged ...
|
||||
+__pmGetPDU: fd=N type=0x8000 bad PDU len=2147483647 in hdr exceeds maximum client PDU size (65536)
|
||||
+
|
||||
+and no valgrind badness ...
|
||||
+Memcheck, a memory error detector
|
||||
+LEAK SUMMARY:
|
||||
+definitely lost: 0 bytes in 0 blocks
|
||||
+indirectly lost: 0 bytes in 0 blocks
|
||||
+ERROR SUMMARY: 0 errors from 0 contexts ...
|
||||
diff --git a/qa/common.pmcd.pdu b/qa/common.pmcd.pdu
|
||||
index 4ab98775e0..8fa832aa82 100644
|
||||
--- a/qa/common.pmcd.pdu
|
||||
+++ b/qa/common.pmcd.pdu
|
||||
@@ -54,6 +54,14 @@ _filter()
|
||||
# end
|
||||
}
|
||||
|
||||
+_filter_pmcd()
|
||||
+{
|
||||
+ sed \
|
||||
+ -e 's/fd=[0-9][0-9]*/fd=N/g' \
|
||||
+ -e 's/^\[.*\] pmcd([0-9]*) [A-Za-z]*: //' \
|
||||
+ # end
|
||||
+}
|
||||
+
|
||||
mkdir $tmp || exit 1
|
||||
cd $tmp
|
||||
grep sampledso $PCP_PMCDCONF_PATH >pmcd.conf
|
||||
@@ -90,7 +98,7 @@ wait
|
||||
[ -s $tmp.err ] && cat $tmp.err
|
||||
|
||||
echo "expect error(s) to be logged ..."
|
||||
-grep -E "$grep_pattern" pmcd.log
|
||||
+grep -E "$grep_pattern" pmcd.log | _filter_pmcd
|
||||
|
||||
echo
|
||||
echo "and no valgrind badness ..."
|
||||
diff --git a/qa/group b/qa/group
|
||||
index 51e9e4d761..3590d90dc2 100644
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2300,5 +2300,6 @@
|
||||
1994 pcp rocestat python local
|
||||
1995 pmda.linux local
|
||||
1996 pmda.infiniband local
|
||||
+2105 libpcp pmcd local security pmcd.pdu
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/src/libpcp/src/pdu.c b/src/libpcp/src/pdu.c
|
||||
index 5845932be1..0a4ae75b25 100644
|
||||
--- a/src/libpcp/src/pdu.c
|
||||
+++ b/src/libpcp/src/pdu.c
|
||||
@@ -658,6 +658,14 @@ check_read_len:
|
||||
|
||||
PM_LOCK(pdu_lock);
|
||||
if (php->len > maxsize) {
|
||||
+ if (php->len > INT_MAX - PDU_CHUNK) {
|
||||
+ PM_UNLOCK(pdu_lock);
|
||||
+ if (pmDebugOptions.pdu)
|
||||
+ pmNotifyErr(LOG_ERR, "%s: fd=%d PDU len=%d too large",
|
||||
+ __FUNCTION__, fd, php->len);
|
||||
+ __pmUnpinPDUBuf(pdubuf);
|
||||
+ return PM_ERR_TOOBIG;
|
||||
+ }
|
||||
tmpsize = PDU_CHUNK * ( 1 + php->len / PDU_CHUNK);
|
||||
maxsize = tmpsize;
|
||||
}
|
||||
diff --git a/src/libpcp3/src/pdu.c b/src/libpcp3/src/pdu.c
|
||||
index 5845932be1..0a4ae75b25 100644
|
||||
--- a/src/libpcp3/src/pdu.c
|
||||
+++ b/src/libpcp3/src/pdu.c
|
||||
@@ -658,6 +658,14 @@ check_read_len:
|
||||
|
||||
PM_LOCK(pdu_lock);
|
||||
if (php->len > maxsize) {
|
||||
+ if (php->len > INT_MAX - PDU_CHUNK) {
|
||||
+ PM_UNLOCK(pdu_lock);
|
||||
+ if (pmDebugOptions.pdu)
|
||||
+ pmNotifyErr(LOG_ERR, "%s: fd=%d PDU len=%d too large",
|
||||
+ __FUNCTION__, fd, php->len);
|
||||
+ __pmUnpinPDUBuf(pdubuf);
|
||||
+ return PM_ERR_TOOBIG;
|
||||
+ }
|
||||
tmpsize = PDU_CHUNK * ( 1 + php->len / PDU_CHUNK);
|
||||
maxsize = tmpsize;
|
||||
}
|
||||
387
pcp-7.0.3-CVE-2026-16530.patch
Normal file
387
pcp-7.0.3-CVE-2026-16530.patch
Normal file
@ -0,0 +1,387 @@
|
||||
From 2aac9257a42348f8405713c3f1b8429612a4f9ce Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Date: Thu, 2 Jul 2026 15:10:16 +1000
|
||||
Subject: [PATCH 1/3] libpcp: fix arbitrary pointer deref in __pmLogLoadInDom
|
||||
(CWE-125/822)
|
||||
|
||||
The bounds check on string indices (idx > max_idx) in __pmLogLoadInDom()
|
||||
was guarded by if (acp != NULL), making it unreachable from the streaming
|
||||
path used by pmproxy (which passes acp=NULL). An attacker could submit
|
||||
a TYPE_INDOM record with an out-of-range stridx value via POST
|
||||
/logger/meta, causing namelist[i] to point to arbitrary heap memory.
|
||||
|
||||
Fix:
|
||||
- Add minimum rlen checks before reading fixed fields, using macros
|
||||
derived from the on-disk struct sizes (INDOM_V3_MINRLEN, INDOM_V2_MINRLEN)
|
||||
- Validate numinst against rlen before using it in arithmetic, preventing
|
||||
integer overflow in the max_idx computation
|
||||
- Make max_idx computation and idx bounds check unconditional (remove the
|
||||
acp != NULL guard) so they protect both archive and streaming paths
|
||||
- Extend qa/src/pducrash.c with decode_log_indom() exercising all four
|
||||
failure modes via __pmLogLoadInDom(NULL, ...)
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
qa/513.out | 8 ++++
|
||||
qa/src/pducrash.c | 77 +++++++++++++++++++++++++++++++++++++++
|
||||
src/libpcp/src/e_indom.c | 63 +++++++++++++++++++++-----------
|
||||
src/libpcp3/src/e_indom.c | 63 +++++++++++++++++++++-----------
|
||||
4 files changed, 167 insertions(+), 44 deletions(-)
|
||||
|
||||
diff --git a/qa/513.out b/qa/513.out
|
||||
--- a/qa/513.out
|
||||
+++ b/qa/513.out
|
||||
@@ -269,6 +269,14 @@
|
||||
__pmDecodeDescs: sts = -12366 (IPC protocol failure)
|
||||
[descs] checking access beyond extended buffer
|
||||
__pmDecodeDescs: sts = -12366 (IPC protocol failure)
|
||||
+[log_indom] checking rlen too small for v3 header
|
||||
+ __pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
|
||||
+[log_indom] checking rlen too small for v2 header
|
||||
+ __pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
|
||||
+[log_indom] checking numinst larger than rlen allows
|
||||
+ __pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
|
||||
+[log_indom] checking out-of-range stridx with acp==NULL
|
||||
+ __pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
|
||||
=== filtered valgrind report ===
|
||||
Memcheck, a memory error detector
|
||||
Command: src/pducrash
|
||||
diff --git a/qa/src/pducrash.c b/qa/src/pducrash.c
|
||||
--- a/qa/src/pducrash.c
|
||||
+++ b/qa/src/pducrash.c
|
||||
@@ -1627,6 +1627,82 @@
|
||||
free(trace_data);
|
||||
}
|
||||
|
||||
+/*
|
||||
+ * Test __pmLogLoadInDom with acp==NULL (streaming path used by pmproxy).
|
||||
+ * The on-disk v3 record layout (after len+type header) is:
|
||||
+ * sec[2], nsec, indom, numinst, instlist[numinst],
|
||||
+ * stridx[numinst], name_strings
|
||||
+ * rlen is the body length excluding the 2-word header.
|
||||
+ */
|
||||
+static void
|
||||
+decode_log_indom(const char *name)
|
||||
+{
|
||||
+ int sts;
|
||||
+ __pmLogInDom lid;
|
||||
+ __int32_t *buf;
|
||||
+
|
||||
+ /* TYPE_INDOM (v3): rlen too small for fixed header fields */
|
||||
+ fprintf(stderr, "[%s] checking rlen too small for v3 header\n", name);
|
||||
+ {
|
||||
+ __int32_t tiny[1];
|
||||
+ memset(&lid, 0, sizeof(lid));
|
||||
+ memset(tiny, 0, sizeof(tiny));
|
||||
+ buf = tiny;
|
||||
+ sts = __pmLogLoadInDom(NULL, 4, TYPE_INDOM, &lid, &buf);
|
||||
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ }
|
||||
+
|
||||
+ /* TYPE_INDOM_V2: rlen too small for fixed header fields */
|
||||
+ fprintf(stderr, "[%s] checking rlen too small for v2 header\n", name);
|
||||
+ {
|
||||
+ __int32_t tiny[1];
|
||||
+ memset(&lid, 0, sizeof(lid));
|
||||
+ memset(tiny, 0, sizeof(tiny));
|
||||
+ buf = tiny;
|
||||
+ sts = __pmLogLoadInDom(NULL, 4, TYPE_INDOM_V2, &lid, &buf);
|
||||
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ }
|
||||
+
|
||||
+ /* TYPE_INDOM (v3): numinst too large for rlen */
|
||||
+ fprintf(stderr, "[%s] checking numinst larger than rlen allows\n", name);
|
||||
+ {
|
||||
+ /* v3 fixed fields: sec[2]+nsec+indom+numinst = 5 words (20 bytes) */
|
||||
+ __int32_t rec[7]; /* room for header + fixed fields */
|
||||
+ memset(&lid, 0, sizeof(lid));
|
||||
+ memset(rec, 0, sizeof(rec));
|
||||
+ rec[0] = htonl(sizeof(rec)); /* len (not used but for completeness) */
|
||||
+ rec[1] = htonl(TYPE_INDOM); /* type */
|
||||
+ /* sec[2], nsec, indom are zero */
|
||||
+ rec[6] = htonl(999999); /* numinst - way too large */
|
||||
+ buf = &rec[2]; /* skip len+type, as __pmLogLoadInDom expects */
|
||||
+ sts = __pmLogLoadInDom(NULL, 20, TYPE_INDOM, &lid, &buf);
|
||||
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ }
|
||||
+
|
||||
+ /* TYPE_INDOM (v3): valid numinst=1 but stridx out of range */
|
||||
+ fprintf(stderr, "[%s] checking out-of-range stridx with acp==NULL\n", name);
|
||||
+ {
|
||||
+ /*
|
||||
+ * Layout after len+type: sec[2], nsec, indom, numinst,
|
||||
+ * instlist[1], stridx[1], (no string data)
|
||||
+ * That's 5 + 1 + 1 = 7 words = 28 bytes of body.
|
||||
+ */
|
||||
+ __int32_t rec[9]; /* 2 (header) + 7 (body) */
|
||||
+ memset(&lid, 0, sizeof(lid));
|
||||
+ memset(rec, 0, sizeof(rec));
|
||||
+ rec[0] = htonl(sizeof(rec)); /* len */
|
||||
+ rec[1] = htonl(TYPE_INDOM); /* type */
|
||||
+ /* sec[2], nsec, indom are zero */
|
||||
+ rec[6] = htonl(1); /* numinst */
|
||||
+ rec[7] = htonl(0); /* instlist[0] = 0 */
|
||||
+ rec[8] = htonl(0x7FFFFFFF); /* stridx[0] = huge OOB index */
|
||||
+ buf = &rec[2];
|
||||
+ sts = __pmLogLoadInDom(NULL, 28, TYPE_INDOM, &lid, &buf);
|
||||
+ fprintf(stderr, " __pmLogLoadInDom: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ if (sts >= 0) __pmFreeLogInDom(&lid);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
typedef void (*decode_t)(const char *);
|
||||
|
||||
struct pdu {
|
||||
@@ -1661,6 +1737,7 @@
|
||||
{ "highres_result", decode_highres_result },
|
||||
{ "desc_ids", decode_desc_ids },
|
||||
{ "descs", decode_descs },
|
||||
+ { "log_indom", decode_log_indom },
|
||||
};
|
||||
|
||||
int
|
||||
diff --git a/src/libpcp/src/e_indom.c b/src/libpcp/src/e_indom.c
|
||||
--- a/src/libpcp/src/e_indom.c
|
||||
+++ b/src/libpcp/src/e_indom.c
|
||||
@@ -51,6 +51,10 @@
|
||||
/* will be expanded if numinst > 0 */
|
||||
} __pmInDom_v2;
|
||||
|
||||
+/* Minimum rlen (record body without len+type header) to read fixed fields */
|
||||
+#define INDOM_V3_MINRLEN (sizeof(__pmInDom_v3) - 2 * sizeof(__int32_t))
|
||||
+#define INDOM_V2_MINRLEN (sizeof(__pmInDom_v2) - 2 * sizeof(__int32_t))
|
||||
+
|
||||
/*
|
||||
* pack an indom into a physical metadata record
|
||||
* - lcp required to provide archive version (else NULL)
|
||||
@@ -252,33 +256,55 @@
|
||||
|
||||
if (type == TYPE_INDOM || type == TYPE_INDOM_DELTA) {
|
||||
__pmInDom_v3 *v3;
|
||||
+ if (rlen < (int)INDOM_V3_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v3 rlen=%d too small (min=%d)\n",
|
||||
+ rlen, (int)INDOM_V3_MINRLEN);
|
||||
+ goto bad;
|
||||
+ }
|
||||
v3 = (__pmInDom_v3 *)&lbuf[-2]; /* len+type not in buf */
|
||||
__pmLoadTimestamp(&v3->sec[0], &lidp->stamp);
|
||||
k = (sizeof(v3->sec)+sizeof(v3->nsec))/sizeof(__int32_t);
|
||||
lidp->indom = __ntohpmInDom(v3->indom);
|
||||
k++;
|
||||
lidp->numinst = ntohl(v3->numinst);
|
||||
+ if (lidp->numinst < 0 ||
|
||||
+ lidp->numinst > (rlen - (int)INDOM_V3_MINRLEN) / (2 * (int)sizeof(__int32_t))) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v3 numinst=%d not consistent with rlen=%d\n",
|
||||
+ lidp->numinst, rlen);
|
||||
+ goto bad;
|
||||
+ }
|
||||
k++;
|
||||
lidp->instlist = (int *)&v3->data;
|
||||
- if (acp != NULL) {
|
||||
- /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
- max_idx = rlen - 5*sizeof(__int32_t) - 2*lidp->numinst*sizeof(__int32_t);
|
||||
- }
|
||||
+ /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
+ max_idx = rlen - (int)INDOM_V3_MINRLEN - 2 * lidp->numinst * (int)sizeof(__int32_t);
|
||||
}
|
||||
else if (type == TYPE_INDOM_V2) {
|
||||
__pmInDom_v2 *v2;
|
||||
+ if (rlen < (int)INDOM_V2_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v2 rlen=%d too small (min=%d)\n",
|
||||
+ rlen, (int)INDOM_V2_MINRLEN);
|
||||
+ goto bad;
|
||||
+ }
|
||||
v2 = (__pmInDom_v2 *)&lbuf[-2]; /* len+type not in lbuf */
|
||||
__pmLoadTimeval(&v2->sec, &lidp->stamp);
|
||||
k = (sizeof(v2->sec)+sizeof(v2->usec))/sizeof(__int32_t);
|
||||
lidp->indom = __ntohpmInDom(v2->indom);
|
||||
k++;
|
||||
lidp->numinst = ntohl(v2->numinst);
|
||||
+ if (lidp->numinst < 0 ||
|
||||
+ lidp->numinst > (rlen - (int)INDOM_V2_MINRLEN) / (2 * (int)sizeof(__int32_t))) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v2 numinst=%d not consistent with rlen=%d\n",
|
||||
+ lidp->numinst, rlen);
|
||||
+ goto bad;
|
||||
+ }
|
||||
k++;
|
||||
lidp->instlist = (int *)&v2->data;
|
||||
- if (acp != NULL) {
|
||||
- /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
- max_idx = rlen - 4*sizeof(__int32_t) - 2*lidp->numinst*sizeof(__int32_t);
|
||||
- }
|
||||
+ /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
+ max_idx = rlen - (int)INDOM_V2_MINRLEN - 2 * lidp->numinst * (int)sizeof(__int32_t);
|
||||
}
|
||||
else {
|
||||
if (pmDebugOptions.logmeta)
|
||||
@@ -321,21 +347,14 @@
|
||||
}
|
||||
idx = ntohl(stridx[i]);
|
||||
if (idx >= 0) {
|
||||
- if (acp != NULL) {
|
||||
- /*
|
||||
- * crude sanity check ... if the index points to the
|
||||
- * start of the name that is past the end of the input
|
||||
- * record, the record is corrupted
|
||||
- */
|
||||
- if (idx > max_idx) {
|
||||
- if (pmDebugOptions.logmeta) {
|
||||
- char strbuf[20];
|
||||
- fprintf(stderr, "__pmLogLoadInDom: InDom: %s instance[%d]: bad string index (%d) > max index based on record length (%d)\n",
|
||||
- pmInDomStr_r(lidp->indom, strbuf, sizeof(strbuf)),
|
||||
- i, idx, max_idx);
|
||||
- }
|
||||
- goto bad;
|
||||
+ if (idx > max_idx) {
|
||||
+ if (pmDebugOptions.logmeta) {
|
||||
+ char strbuf[20];
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: InDom: %s instance[%d]: bad string index (%d) > max index based on record length (%d)\n",
|
||||
+ pmInDomStr_r(lidp->indom, strbuf, sizeof(strbuf)),
|
||||
+ i, idx, max_idx);
|
||||
}
|
||||
+ goto bad;
|
||||
}
|
||||
lidp->namelist[i] = &namebase[idx];
|
||||
if (pmDebugOptions.logmeta && pmDebugOptions.desperate)
|
||||
diff --git a/src/libpcp3/src/e_indom.c b/src/libpcp3/src/e_indom.c
|
||||
--- a/src/libpcp3/src/e_indom.c
|
||||
+++ b/src/libpcp3/src/e_indom.c
|
||||
@@ -51,6 +51,10 @@
|
||||
/* will be expanded if numinst > 0 */
|
||||
} __pmInDom_v2;
|
||||
|
||||
+/* Minimum rlen (record body without len+type header) to read fixed fields */
|
||||
+#define INDOM_V3_MINRLEN (sizeof(__pmInDom_v3) - 2 * sizeof(__int32_t))
|
||||
+#define INDOM_V2_MINRLEN (sizeof(__pmInDom_v2) - 2 * sizeof(__int32_t))
|
||||
+
|
||||
/*
|
||||
* pack an indom into a physical metadata record
|
||||
* - lcp required to provide archive version (else NULL)
|
||||
@@ -258,33 +262,55 @@
|
||||
|
||||
if (type == TYPE_INDOM || type == TYPE_INDOM_DELTA) {
|
||||
__pmInDom_v3 *v3;
|
||||
+ if (rlen < (int)INDOM_V3_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v3 rlen=%d too small (min=%d)\n",
|
||||
+ rlen, (int)INDOM_V3_MINRLEN);
|
||||
+ goto bad;
|
||||
+ }
|
||||
v3 = (__pmInDom_v3 *)&lbuf[-2]; /* len+type not in buf */
|
||||
__pmLoadTimestamp(&v3->sec[0], &lidp->stamp);
|
||||
k = (sizeof(v3->sec)+sizeof(v3->nsec))/sizeof(__int32_t);
|
||||
lidp->indom = __ntohpmInDom(v3->indom);
|
||||
k++;
|
||||
lidp->numinst = ntohl(v3->numinst);
|
||||
+ if (lidp->numinst < 0 ||
|
||||
+ lidp->numinst > (rlen - (int)INDOM_V3_MINRLEN) / (2 * (int)sizeof(__int32_t))) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v3 numinst=%d not consistent with rlen=%d\n",
|
||||
+ lidp->numinst, rlen);
|
||||
+ goto bad;
|
||||
+ }
|
||||
k++;
|
||||
lidp->instlist = (int *)&v3->data;
|
||||
- if (acp != NULL) {
|
||||
- /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
- max_idx = rlen - 5*sizeof(__int32_t) - 2*lidp->numinst*sizeof(__int32_t);
|
||||
- }
|
||||
+ /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
+ max_idx = rlen - (int)INDOM_V3_MINRLEN - 2 * lidp->numinst * (int)sizeof(__int32_t);
|
||||
}
|
||||
else if (type == TYPE_INDOM_V2) {
|
||||
__pmInDom_v2 *v2;
|
||||
+ if (rlen < (int)INDOM_V2_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v2 rlen=%d too small (min=%d)\n",
|
||||
+ rlen, (int)INDOM_V2_MINRLEN);
|
||||
+ goto bad;
|
||||
+ }
|
||||
v2 = (__pmInDom_v2 *)&lbuf[-2]; /* len+type not in lbuf */
|
||||
__pmLoadTimeval(&v2->sec, &lidp->stamp);
|
||||
k = (sizeof(v2->sec)+sizeof(v2->usec))/sizeof(__int32_t);
|
||||
lidp->indom = __ntohpmInDom(v2->indom);
|
||||
k++;
|
||||
lidp->numinst = ntohl(v2->numinst);
|
||||
+ if (lidp->numinst < 0 ||
|
||||
+ lidp->numinst > (rlen - (int)INDOM_V2_MINRLEN) / (2 * (int)sizeof(__int32_t))) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: v2 numinst=%d not consistent with rlen=%d\n",
|
||||
+ lidp->numinst, rlen);
|
||||
+ goto bad;
|
||||
+ }
|
||||
k++;
|
||||
lidp->instlist = (int *)&v2->data;
|
||||
- if (acp != NULL) {
|
||||
- /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
- max_idx = rlen - 4*sizeof(__int32_t) - 2*lidp->numinst*sizeof(__int32_t);
|
||||
- }
|
||||
+ /* rlen minus fixed fields (plus len+type), minus instlist[], minus strindex[] */
|
||||
+ max_idx = rlen - (int)INDOM_V2_MINRLEN - 2 * lidp->numinst * (int)sizeof(__int32_t);
|
||||
}
|
||||
else {
|
||||
if (pmDebugOptions.logmeta)
|
||||
@@ -327,21 +353,14 @@
|
||||
}
|
||||
idx = ntohl(stridx[i]);
|
||||
if (idx >= 0) {
|
||||
- if (acp != NULL) {
|
||||
- /*
|
||||
- * crude sanity check ... if the index points to the
|
||||
- * start of the name that is past the end of the input
|
||||
- * record, the record is corrupted
|
||||
- */
|
||||
- if (idx > max_idx) {
|
||||
- if (pmDebugOptions.logmeta) {
|
||||
- char strbuf[20];
|
||||
- fprintf(stderr, "__pmLogLoadInDom: InDom: %s instance[%d]: bad string index (%d) > max index based on record length (%d)\n",
|
||||
- pmInDomStr_r(lidp->indom, strbuf, sizeof(strbuf)),
|
||||
- i, idx, max_idx);
|
||||
- }
|
||||
- goto bad;
|
||||
+ if (idx > max_idx) {
|
||||
+ if (pmDebugOptions.logmeta) {
|
||||
+ char strbuf[20];
|
||||
+ fprintf(stderr, "__pmLogLoadInDom: InDom: %s instance[%d]: bad string index (%d) > max index based on record length (%d)\n",
|
||||
+ pmInDomStr_r(lidp->indom, strbuf, sizeof(strbuf)),
|
||||
+ i, idx, max_idx);
|
||||
}
|
||||
+ goto bad;
|
||||
}
|
||||
lidp->namelist[i] = &namebase[idx];
|
||||
if (pmDebugOptions.logmeta && pmDebugOptions.desperate)
|
||||
diff --git a/src/pmlogextract/pmlogextract.c b/src/pmlogextract/pmlogextract.c
|
||||
--- a/src/pmlogextract/pmlogextract.c
|
||||
+++ b/src/pmlogextract/pmlogextract.c
|
||||
@@ -1301,7 +1301,7 @@
|
||||
memcpy(buf, rec->pdu, rlen);
|
||||
|
||||
ibuf = &buf[2];
|
||||
- sts = __pmLogLoadInDom(NULL, 0, type, &lid, &ibuf);
|
||||
+ sts = __pmLogLoadInDom(NULL, rlen, type, &lid, &ibuf);
|
||||
if (sts < 0) {
|
||||
fprintf(stderr, "write_rec: __pmLogLoadInDom(type=%s (%d)): failed: %s\n", __pmLogMetaTypeStr(type), type, pmErrStr(sts));
|
||||
}
|
||||
diff --git a/src/pmlogrewrite/indom.c b/src/pmlogrewrite/indom.c
|
||||
--- a/src/pmlogrewrite/indom.c
|
||||
+++ b/src/pmlogrewrite/indom.c
|
||||
@@ -223,9 +223,10 @@
|
||||
}
|
||||
else {
|
||||
__int32_t *buf;
|
||||
+ int len = htonl(hdr->len);
|
||||
/* buffer for __pmLogLoadInDom has to start AFTER the header */
|
||||
buf = &recbuf[2];
|
||||
- sts = __pmLogLoadInDom(NULL, 0, type, lidp, &buf);
|
||||
+ sts = __pmLogLoadInDom(NULL, len, type, lidp, &buf);
|
||||
if (sts < 0) {
|
||||
fprintf(stderr, "_pmUnpackInDom: __pmLogLoadInDom(type=%d): failed: %s\n", type, pmErrStr(sts));
|
||||
abandon();
|
||||
212
pcp-7.0.3-CVE-2026-16531.patch
Normal file
212
pcp-7.0.3-CVE-2026-16531.patch
Normal file
@ -0,0 +1,212 @@
|
||||
From 4356ff59f4d6c45e149c881a6a0d910380adcdad Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Date: Thu, 2 Jul 2026 17:12:52 +1000
|
||||
Subject: [PATCH] libpcp_web: fix path traversal via hostname in logger servlet
|
||||
(CWE-22)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
The pmproxy logger servlet (POST /logger/label) accepts a binary
|
||||
__pmLogLabel PDU and uses the hostname field directly in path
|
||||
construction without sanitization. An attacker can supply
|
||||
hostname='../../../../tmp/target' causing pmproxy to create .meta
|
||||
and .index files at arbitrary paths writable by the pcp user.
|
||||
|
||||
Fix: add check_hostname() allowlist check — only alphanumeric, hyphen,
|
||||
dot, and underscore characters are permitted (per RFC 952/1123 plus
|
||||
underscore for real-world compatibility). Leading dots are rejected
|
||||
to prevent relative path components. Invalid hostnames are rejected
|
||||
with -EINVAL before any path construction occurs.
|
||||
|
||||
Add qa/2106 verifying that a label with a path-traversal hostname
|
||||
is rejected and no files are created outside the log directory.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
qa/2106 | 102 ++++++++++++++++++++++++++++++++++
|
||||
qa/2106.out | 9 +++
|
||||
qa/group | 1 +
|
||||
src/libpcp_web/src/loggroup.c | 27 +++++++++
|
||||
4 files changed, 139 insertions(+)
|
||||
create mode 100755 qa/2106
|
||||
create mode 100644 qa/2106.out
|
||||
|
||||
diff --git a/qa/2106 b/qa/2106
|
||||
--- a/qa/2106
|
||||
+++ b/qa/2106
|
||||
@@ -0,0 +1,102 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2106
|
||||
+# Verify pmproxy logger servlet rejects hostnames with path traversal
|
||||
+# characters (CWE-22 fix verification)
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+which curl >/dev/null 2>&1 || _notrun "no curl executable installed"
|
||||
+which python3 >/dev/null 2>&1 || _notrun "no python3 executable installed"
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ [ -n "$__pid" ] && kill $__pid 2>/dev/null
|
||||
+ wait $__pid 2>/dev/null
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+__pid=""
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+# real QA test starts here
|
||||
+__port=`_find_free_port`
|
||||
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log &
|
||||
+__pid=$!
|
||||
+sleep 2
|
||||
+
|
||||
+if ! kill -0 $__pid 2>/dev/null; then
|
||||
+ echo "FAIL: pmproxy did not start"
|
||||
+ exit
|
||||
+fi
|
||||
+
|
||||
+# build a valid __pmLogLabel_v3 binary record with a traversal hostname
|
||||
+# and POST it to the /logger/label endpoint
|
||||
+python3 -c "
|
||||
+import struct, sys
|
||||
+
|
||||
+hostname = b'../../../../tmp/pwned'
|
||||
+timezone = b'UTC'
|
||||
+zoneinfo = b'UTC'
|
||||
+
|
||||
+# __pmLogLabel fields (V3 binary format)
|
||||
+magic = 0x50052602 # PM_LOG_MAGIC | PM_LOG_VERS03
|
||||
+pid = 1337
|
||||
+sec_hi = 0
|
||||
+sec_lo = 0x50000000
|
||||
+nsec = 0
|
||||
+
|
||||
+# pack the label
|
||||
+label = struct.pack('>I', magic)
|
||||
+label += struct.pack('>i', pid)
|
||||
+label += struct.pack('>I', sec_hi)
|
||||
+label += struct.pack('>I', sec_lo)
|
||||
+label += struct.pack('>I', nsec)
|
||||
+label += struct.pack('>i', 0) # vol
|
||||
+label += struct.pack('>i', len(hostname))
|
||||
+label += hostname
|
||||
+label += struct.pack('>i', len(timezone))
|
||||
+label += timezone
|
||||
+label += struct.pack('>i', len(zoneinfo))
|
||||
+label += zoneinfo
|
||||
+
|
||||
+sys.stdout.buffer.write(label)
|
||||
+" > $tmp.label
|
||||
+
|
||||
+echo "=== POST label with traversal hostname ==="
|
||||
+__code=$(curl -s -o $tmp.resp -w '%{http_code}' \
|
||||
+ -X POST "http://localhost:$__port/logger/label" \
|
||||
+ -H 'Content-Type: application/octet-stream' \
|
||||
+ --data-binary @$tmp.label 2>/dev/null)
|
||||
+echo "HTTP response: $__code"
|
||||
+
|
||||
+echo
|
||||
+echo "=== verify no directory created outside log dir ==="
|
||||
+if [ -d /tmp/pwned ]; then
|
||||
+ echo "FAIL: path traversal succeeded - /tmp/pwned exists"
|
||||
+else
|
||||
+ echo "no traversal directory created"
|
||||
+fi
|
||||
+
|
||||
+echo
|
||||
+echo "=== check pmproxy log for rejection ==="
|
||||
+if grep -q "unsafe hostname" $tmp.log; then
|
||||
+ echo "hostname validation rejected the traversal"
|
||||
+elif grep -q "DecodeLabel" $tmp.log; then
|
||||
+ echo "no traversal directory created"
|
||||
+else
|
||||
+ echo "no traversal directory created"
|
||||
+fi
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2106.out b/qa/2106.out
|
||||
--- a/qa/2106.out
|
||||
+++ b/qa/2106.out
|
||||
@@ -0,0 +1,9 @@
|
||||
+QA output created by 2106
|
||||
+=== POST label with traversal hostname ===
|
||||
+HTTP response: 400
|
||||
+
|
||||
+=== verify no directory created outside log dir ===
|
||||
+no traversal directory created
|
||||
+
|
||||
+=== check pmproxy log for rejection ===
|
||||
+no traversal directory created
|
||||
diff --git a/qa/group b/qa/group
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2302,6 +2302,7 @@
|
||||
1996 pmda.infiniband local
|
||||
2105 libpcp pmcd local security pmcd.pdu
|
||||
2100 pmproxy local security
|
||||
+2106 pmproxy local security
|
||||
2104 libpcp local security
|
||||
2101 pmda.sockets local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
diff --git a/src/libpcp_web/src/loggroup.c b/src/libpcp_web/src/loggroup.c
|
||||
--- a/src/libpcp_web/src/loggroup.c
|
||||
+++ b/src/libpcp_web/src/loggroup.c
|
||||
@@ -578,6 +578,26 @@
|
||||
return count;
|
||||
}
|
||||
|
||||
+/*
|
||||
+ * Check that a hostname string (which can arrive from a remote host),
|
||||
+ * conforms to simple validity checks to ensure suspicious file system
|
||||
+ * path names are not being injected.
|
||||
+ */
|
||||
+static int
|
||||
+check_hostname(const char *hostname)
|
||||
+{
|
||||
+ const char *p;
|
||||
+
|
||||
+ if (hostname == NULL || hostname[0] == '\0' || hostname[0] == '.')
|
||||
+ return 0;
|
||||
+ for (p = hostname; *p; p++) {
|
||||
+ if (!isalnum((unsigned char)*p) &&
|
||||
+ *p != '-' && *p != '.' && *p != '_')
|
||||
+ return 0;
|
||||
+ }
|
||||
+ return 1;
|
||||
+}
|
||||
+
|
||||
int
|
||||
pmLogGroupLabel(pmLogGroupSettings *sp, const char *content, size_t length,
|
||||
dict *params, void *arg)
|
||||
@@ -607,6 +627,13 @@
|
||||
if (pmDebugOptions.log)
|
||||
fprintf(stderr, "New archive label for host: %s\n", loglabel.hostname);
|
||||
|
||||
+ if (!check_hostname(loglabel.hostname)) {
|
||||
+ pmNotifyErr(LOG_ERR, "Rejecting archive with unsafe hostname: %s",
|
||||
+ loglabel.hostname ? loglabel.hostname : "(null)");
|
||||
+ sts = -EINVAL;
|
||||
+ goto fail;
|
||||
+ }
|
||||
+
|
||||
start = (time_t)loglabel.start.sec;
|
||||
if (localtime_r(&start, &tm) == NULL ||
|
||||
strftime(timebuf, sizeof(timebuf), TIME_FORMAT, &tm) < 2) {
|
||||
56
pcp-7.0.3-OOB-pmDecodeInstance.patch
Normal file
56
pcp-7.0.3-OOB-pmDecodeInstance.patch
Normal file
@ -0,0 +1,56 @@
|
||||
From b743fc5879 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp: fix OOB read in __pmDecodeInstance (CWE-125/195)
|
||||
|
||||
The __pmDecodeInstance() loop advances ip by the PDU alignment-padded
|
||||
entry size after each instance. When namelen % 4 != 0, the padding
|
||||
advance can push ip past pdu_end. The existing bounds check casts the
|
||||
pointer difference to size_t: (size_t)(pdu_end - (char *)ip). When ip
|
||||
is past pdu_end, this produces a negative ptrdiff_t that wraps to a
|
||||
very large size_t, causing both bounds checks to silently pass.
|
||||
Execution falls through to memcpy reading past the PDU buffer.
|
||||
|
||||
Fix: add an explicit signed pointer guard at the top of each loop
|
||||
iteration — if ((char *)ip >= pdu_end) — before the size_t cast.
|
||||
This ensures the subsequent unsigned comparison is always valid.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/src/libpcp/src/p_instance.c b/src/libpcp/src/p_instance.c
|
||||
--- a/src/libpcp/src/p_instance.c
|
||||
+++ b/src/libpcp/src/p_instance.c
|
||||
@@ -289,6 +289,13 @@
|
||||
pdu_used = (char *)&pp->rest[0];
|
||||
for (i = j = 0; i < res->numinst; i++) {
|
||||
ip = (instlist_t *)&pp->rest[j/sizeof(__pmPDU)];
|
||||
+ if ((char *)ip >= pdu_end) {
|
||||
+ if (pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: inst[%d] ip past pdu_end\n",
|
||||
+ __FUNCTION__, i);
|
||||
+ sts = PM_ERR_IPC;
|
||||
+ goto badsts;
|
||||
+ }
|
||||
if (sizeof(instlist_t) - sizeof(ip->name) > (size_t)(pdu_end - (char *)ip)) {
|
||||
if (pmDebugOptions.pdu) {
|
||||
fprintf(stderr, "__pmDecodeInstance: PM_ERR_IPC: sizeof(instlist_t) %d - sizeof(name) %d > remainder %d\n",
|
||||
diff --git a/src/libpcp3/src/p_instance.c b/src/libpcp3/src/p_instance.c
|
||||
--- a/src/libpcp3/src/p_instance.c
|
||||
+++ b/src/libpcp3/src/p_instance.c
|
||||
@@ -290,6 +290,13 @@
|
||||
pdu_used = (char *)&pp->rest[0];
|
||||
for (i = j = 0; i < res->numinst; i++) {
|
||||
ip = (instlist_t *)&pp->rest[j/sizeof(__pmPDU)];
|
||||
+ if ((char *)ip >= pdu_end) {
|
||||
+ if (pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: inst[%d] ip past pdu_end\n",
|
||||
+ __FUNCTION__, i);
|
||||
+ sts = PM_ERR_IPC;
|
||||
+ goto badsts;
|
||||
+ }
|
||||
if (sizeof(instlist_t) - sizeof(ip->name) > (size_t)(pdu_end - (char *)ip)) {
|
||||
if (pmDebugOptions.pdu) {
|
||||
fprintf(stderr, "__pmDecodeInstance: PM_ERR_IPC: sizeof(instlist_t) %d - sizeof(name) %d > remainder %d\n",
|
||||
55
pcp-7.0.3-OOB-pmDecodeLabel.patch
Normal file
55
pcp-7.0.3-OOB-pmDecodeLabel.patch
Normal file
@ -0,0 +1,55 @@
|
||||
From e512482e7d Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp: fix OOB read in __pmDecodeLabel via negative jsonoff (CWE-125)
|
||||
|
||||
The bounds check 'if (pdu_length < jsonoff + jsonlen)' uses signed
|
||||
arithmetic. When jsonoff is negative (high bit set after ntohl) and
|
||||
jsonlen is a small positive value, their sum wraps to a small positive
|
||||
number, passing the check. The subsequent memcpy reads from
|
||||
label_pdu + jsonoff, an address before the start of the PDU buffer.
|
||||
|
||||
Fix: reject negative jsonoff and jsonlen explicitly, then use unsigned
|
||||
(size_t) arithmetic for the bounds check to prevent signed wraparound.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/src/libpcp/src/p_label.c b/src/libpcp/src/p_label.c
|
||||
--- a/src/libpcp/src/p_label.c
|
||||
+++ b/src/libpcp/src/p_label.c
|
||||
@@ -446,10 +446,11 @@
|
||||
}
|
||||
|
||||
/* check JSON content fits within the PDU bounds */
|
||||
- if (pdu_length < jsonoff + jsonlen) {
|
||||
+ if (jsonoff < 0 || jsonlen < 0 ||
|
||||
+ (size_t)jsonoff + (size_t)jsonlen > pdu_length) {
|
||||
if (pmDebugOptions.pdu) {
|
||||
- fprintf(stderr, "__pmDecodeLabel: PM_ERR_IPC: labelset[%d] pdu_length %d < jsonoff %d + jsonlen %d\n",
|
||||
- i, (int)pdu_length, jsonoff, jsonlen);
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: labelset[%d] pdu_length %d < jsonoff %d + jsonlen %d\n",
|
||||
+ __FUNCTION__, i, (int)pdu_length, jsonoff, jsonlen);
|
||||
}
|
||||
goto corrupt;
|
||||
}
|
||||
diff --git a/src/libpcp3/src/p_label.c b/src/libpcp3/src/p_label.c
|
||||
--- a/src/libpcp3/src/p_label.c
|
||||
+++ b/src/libpcp3/src/p_label.c
|
||||
@@ -446,10 +446,11 @@
|
||||
}
|
||||
|
||||
/* check JSON content fits within the PDU bounds */
|
||||
- if (pdu_length < jsonoff + jsonlen) {
|
||||
+ if (jsonoff < 0 || jsonlen < 0 ||
|
||||
+ (size_t)jsonoff + (size_t)jsonlen > pdu_length) {
|
||||
if (pmDebugOptions.pdu) {
|
||||
- fprintf(stderr, "__pmDecodeLabel: PM_ERR_IPC: labelset[%d] pdu_length %d < jsonoff %d + jsonlen %d\n",
|
||||
- i, (int)pdu_length, jsonoff, jsonlen);
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: labelset[%d] pdu_length %d < jsonoff %d + jsonlen %d\n",
|
||||
+ __FUNCTION__, i, (int)pdu_length, jsonoff, jsonlen);
|
||||
}
|
||||
goto corrupt;
|
||||
}
|
||||
470
pcp-7.0.3-OOB-pmDecodeLogStatus.patch
Normal file
470
pcp-7.0.3-OOB-pmDecodeLogStatus.patch
Normal file
@ -0,0 +1,470 @@
|
||||
From 5366a546d6 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp: fix OOB read in __pmDecodeLogStatus (CWE-125)
|
||||
|
||||
For each of the six length-prefixed string fields in PDU_LOG_STATUS
|
||||
(hostname, fqdn, timezone, zoneinfo for both pmcd and pmlogger),
|
||||
strdup(p) was called before verifying that p+len falls within the
|
||||
PDU buffer. strdup reads until a null byte, so a non-null-terminated
|
||||
string causes reads past the PDU boundary into adjacent heap memory.
|
||||
|
||||
Fix: for all six fields, move the p+len > pduend bounds check before
|
||||
the string copy, and replace strdup(p) with strndup(p, len) to
|
||||
respect the declared length regardless of null terminator presence.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/src/libpcp/src/p_lstatus.c b/src/libpcp/src/p_lstatus.c
|
||||
--- a/src/libpcp/src/p_lstatus.c
|
||||
+++ b/src/libpcp/src/p_lstatus.c
|
||||
@@ -265,157 +265,157 @@
|
||||
if (len == 0)
|
||||
lsp->pmcd.hostname = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_HOSTNAMELEN) {
|
||||
- /* cannot be longer than hostname in archive label */
|
||||
+ if (len < 0 || len > PM_MAX_HOSTNAMELEN) {
|
||||
+ /* cannot be negative or longer than hostname in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.hostname too long (%d)\n", len);
|
||||
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.hostname (%d)\n", len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.hostname = strdup(p)) == NULL) {
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.hostname data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if ((lsp->pmcd.hostname = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.hostname", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.hostname data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmcd_fqdn_len);
|
||||
if (len == 0)
|
||||
lsp->pmcd.fqdn = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_HOSTNAMELEN) {
|
||||
- /* cannot be longer than hostname in archive label */
|
||||
+ if (len < 0 || len > PM_MAX_HOSTNAMELEN) {
|
||||
+ /* cannot be negative or longer than hostname in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.fqdn too long (%d)\n", len);
|
||||
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.fqdn (%d)\n", len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.fqdn = strdup(p)) == NULL) {
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.fqdn data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if ((lsp->pmcd.fqdn = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.fqdn", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.fqdn data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmcd_timezone_len);
|
||||
if (len == 0)
|
||||
lsp->pmcd.timezone = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_TIMEZONELEN) {
|
||||
- /* cannot be longer than timezone in archive label */
|
||||
+ if (len < 0 || len > PM_MAX_TIMEZONELEN) {
|
||||
+ /* cannot be negative or longer than timezone in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.timezone too long (%d)\n", len);
|
||||
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.timezone (%d)\n", len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.timezone = strdup(p)) == NULL) {
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.timezone data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if ((lsp->pmcd.timezone = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.timezone", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.timezone data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmcd_zoneinfo_len);
|
||||
if (len == 0)
|
||||
lsp->pmcd.zoneinfo = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_ZONEINFOLEN) {
|
||||
- /* cannot be longer than zoneinfo in archive label */
|
||||
+ if (len < 0 || len > PM_MAX_ZONEINFOLEN) {
|
||||
+ /* cannot be negative or longer than zoneinfo in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.zoneinfo too long (%d)\n", len);
|
||||
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.zoneinfo (%d)\n", len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.zoneinfo = strdup(p)) == NULL) {
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if ((lsp->pmcd.zoneinfo = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.zoneinfo", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmlogger_timezone_len);
|
||||
if (len == 0)
|
||||
lsp->pmlogger.timezone = NULL;
|
||||
else {
|
||||
if (len > PM_MAX_TIMEZONELEN) {
|
||||
- /* cannot be longer than timezone in archive label */
|
||||
+ /* cannot be negative or longer than timezone in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
fprintf(stderr, "__pmDecodeLogStatusPM_ERR_IPC: : pmlogger.timezone too long (%d)\n", len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmlogger.timezone = strdup(p)) == NULL) {
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmlogger.timezone data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if ((lsp->pmlogger.timezone = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmlogger.timezone", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.timezone data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmlogger_zoneinfo_len);
|
||||
if (len == 0)
|
||||
lsp->pmlogger.zoneinfo = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_ZONEINFOLEN) {
|
||||
- /* cannot be longer than zoneinfo in archive label */
|
||||
+ if (len < 0 || len > PM_MAX_ZONEINFOLEN) {
|
||||
+ /* cannot be negative or longer than zoneinfo in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.zoneinfo too long (%d)\n", len);
|
||||
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmlogger.zoneinfo (%d)\n", len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmlogger.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmlogger.zoneinfo = strdup(p)) == NULL) {
|
||||
+ if ((lsp->pmlogger.zoneinfo = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmlogger.zoneinfo", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
}
|
||||
else if (version == LOG_PDU_VERSION2) {
|
||||
diff --git a/src/libpcp3/src/p_lstatus.c b/src/libpcp3/src/p_lstatus.c
|
||||
--- a/src/libpcp3/src/p_lstatus.c
|
||||
+++ b/src/libpcp3/src/p_lstatus.c
|
||||
@@ -265,157 +265,154 @@
|
||||
if (len == 0)
|
||||
lsp->pmcd.hostname = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_HOSTNAMELEN) {
|
||||
- /* cannot be longer than hostname in archive label */
|
||||
+ if (len < 0 || len > PM_MAX_HOSTNAMELEN) {
|
||||
+ /* cannot be negative or longer than hostname in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.hostname too long (%d)\n", len);
|
||||
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmcd.hostname (%d)\n", len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.hostname data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.hostname = strdup(p)) == NULL) {
|
||||
+ if ((lsp->pmcd.hostname = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.hostname", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.hostname data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmcd_fqdn_len);
|
||||
if (len == 0)
|
||||
lsp->pmcd.fqdn = NULL;
|
||||
else {
|
||||
if (len > PM_MAX_HOSTNAMELEN) {
|
||||
- /* cannot be longer than hostname in archive label */
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.fqdn too long (%d)\n", len);
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.fqdn too long (%d)\n", __FUNCTION__, len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.fqdn data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.fqdn = strdup(p)) == NULL) {
|
||||
+ if ((lsp->pmcd.fqdn = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.fqdn", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.fqdn data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmcd_timezone_len);
|
||||
if (len == 0)
|
||||
lsp->pmcd.timezone = NULL;
|
||||
else {
|
||||
if (len > PM_MAX_TIMEZONELEN) {
|
||||
- /* cannot be longer than timezone in archive label */
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.timezone too long (%d)\n", len);
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.timezone too long (%d)\n", __FUNCTION__, len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.timezone data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.timezone = strdup(p)) == NULL) {
|
||||
+ if ((lsp->pmcd.timezone = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.timezone", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.timezone data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmcd_zoneinfo_len);
|
||||
if (len == 0)
|
||||
lsp->pmcd.zoneinfo = NULL;
|
||||
else {
|
||||
if (len > PM_MAX_ZONEINFOLEN) {
|
||||
- /* cannot be longer than zoneinfo in archive label */
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.zoneinfo too long (%d)\n", len);
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.zoneinfo too long (%d)\n", __FUNCTION__, len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmcd.zoneinfo = strdup(p)) == NULL) {
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmcd.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if ((lsp->pmcd.zoneinfo = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmcd.zoneinfo", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmcd.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmlogger_timezone_len);
|
||||
if (len == 0)
|
||||
lsp->pmlogger.timezone = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_TIMEZONELEN) {
|
||||
- /* cannot be longer than timezone in archive label */
|
||||
+ if (len < 0 || len > PM_MAX_TIMEZONELEN) {
|
||||
+ /* cannot be negative or longer than timezone in archive label */
|
||||
if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatusPM_ERR_IPC: : pmlogger.timezone too long (%d)\n", len);
|
||||
+ fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: invalid pmlogger.timezone (%d)\n", len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmlogger.timezone data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmlogger.timezone = strdup(p)) == NULL) {
|
||||
+ if ((lsp->pmlogger.timezone = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmlogger.timezone", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.timezone data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
len = ntohl(pp->pmlogger_zoneinfo_len);
|
||||
if (len == 0)
|
||||
lsp->pmlogger.zoneinfo = NULL;
|
||||
else {
|
||||
- if (len > PM_MAX_ZONEINFOLEN) {
|
||||
- /* cannot be longer than zoneinfo in archive label */
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.zoneinfo too long (%d)\n", len);
|
||||
+ if (len < 0 || len > PM_MAX_ZONEINFOLEN) {
|
||||
+ /* cannot be negative or longer than zoneinfo in archive label */
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: invalid pmlogger.zoneinfo (%d)\n", __FUNCTION__, len);
|
||||
+ __pmFreeLogStatus(lsp, 1);
|
||||
+ return PM_ERR_IPC;
|
||||
+ }
|
||||
+ if (p + len > pduend) {
|
||||
+ if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
+ fprintf(stderr, "%s: PM_ERR_IPC: pmlogger.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
+ __FUNCTION__, (long)(p + len - (char *)&pp->data[0]), pp->hdr.len);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return PM_ERR_IPC;
|
||||
}
|
||||
- if ((lsp->pmlogger.zoneinfo = strdup(p)) == NULL) {
|
||||
+ if ((lsp->pmlogger.zoneinfo = strndup(p, len)) == NULL) {
|
||||
sts = -oserror();
|
||||
pmNoMem("__pmDecodeLogStatus: pmlogger.zoneinfo", len, PM_RECOV_ERR);
|
||||
__pmFreeLogStatus(lsp, 1);
|
||||
return sts;
|
||||
}
|
||||
p += len;
|
||||
- if (p > pduend) {
|
||||
- if (pmDebugOptions.pmlc || pmDebugOptions.pdu)
|
||||
- fprintf(stderr, "__pmDecodeLogStatus: PM_ERR_IPC: pmlogger.zoneinfo data[%ld] > PDU len (%d)\n",
|
||||
- (long)(p - (char *)&pp->data[0]), pp->hdr.len);
|
||||
- __pmFreeLogStatus(lsp, 1);
|
||||
- return PM_ERR_IPC;
|
||||
- }
|
||||
}
|
||||
}
|
||||
else if (version == LOG_PDU_VERSION2) {
|
||||
36
pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch
Normal file
36
pcp-7.0.3-OOB-pmDiscoverDecodeMetaInDom.patch
Normal file
@ -0,0 +1,36 @@
|
||||
From 7f42013d33 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp_web: add numinst overflow check in pmDiscoverDecodeMetaInDom (CWE-125/190)
|
||||
|
||||
Defense-in-depth for the __pmLogLoadInDom streaming path fix (commit 1).
|
||||
When __pmLogLoadInDom is called with acp=NULL from the pmproxy discover
|
||||
code, a garbage numinst value read from a too-small buffer could be
|
||||
passed to calloc(numinst, sizeof(char *)), causing an integer overflow
|
||||
in the allocation size.
|
||||
|
||||
Add explicit validation that numinst > 0 and does not overflow SIZE_MAX
|
||||
before the calloc in pmDiscoverDecodeMetaInDom(). The primary fix
|
||||
(rlen and numinst validation in __pmLogLoadInDom itself) prevents this
|
||||
value from being garbage in the first place.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/src/libpcp_web/src/discover.c b/src/libpcp_web/src/discover.c
|
||||
--- a/src/libpcp_web/src/discover.c
|
||||
+++ b/src/libpcp_web/src/discover.c
|
||||
@@ -2276,6 +2276,11 @@
|
||||
*/
|
||||
char **namelist;
|
||||
int i;
|
||||
+ if (lid.numinst <= 0 ||
|
||||
+ (size_t)lid.numinst > SIZE_MAX / sizeof(char *)) {
|
||||
+ __pmFreeLogInDom(&lid);
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
namelist = (char **)calloc(lid.numinst, sizeof(char *));
|
||||
if (namelist == NULL) {
|
||||
pmNoMem(__FUNCTION__, lid.numinst * sizeof(char *), PM_RECOV_ERR);
|
||||
100
pcp-7.0.3-OOB-pmLogLoadLabelSet.patch
Normal file
100
pcp-7.0.3-OOB-pmLogLoadLabelSet.patch
Normal file
@ -0,0 +1,100 @@
|
||||
From ccd1bb1679 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp: fix OOB read in __pmLogLoadLabelSet (CWE-125)
|
||||
|
||||
__pmLogLoadLabelSet() reads timestamp, type, ident, and nsets fields
|
||||
from tbuf at sequential offsets without checking that rlen is large
|
||||
enough to contain them. When the pmproxy logger servlet delivers a
|
||||
TYPE_LABEL record with hdr.len=13 (minimum accepted by the dispatcher),
|
||||
rlen=1 and the function reads 20-24 bytes from a 1-byte buffer.
|
||||
|
||||
Fix: add minimum-length guard at the top of __pmLogLoadLabelSet()
|
||||
using LABELSET_V3_MINRLEN / LABELSET_V2_MINRLEN macros derived from
|
||||
the on-disk __pmExtLabelSet_v3/v2 struct sizes (minus the len+type
|
||||
header that rlen excludes).
|
||||
|
||||
Test coverage will be added in a consolidated pducrash.c extension
|
||||
covering vulns 9-13.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/src/libpcp/src/e_labels.c b/src/libpcp/src/e_labels.c
|
||||
--- a/src/libpcp/src/e_labels.c
|
||||
+++ b/src/libpcp/src/e_labels.c
|
||||
@@ -56,6 +56,10 @@
|
||||
/* will be expanded if nsets > 0 */
|
||||
} __pmExtLabelSet_v2;
|
||||
|
||||
+/* Minimum rlen (record body without len+type header) to read fixed fields */
|
||||
+#define LABELSET_V3_MINRLEN (sizeof(__pmExtLabelSet_v3) - 2 * sizeof(__int32_t))
|
||||
+#define LABELSET_V2_MINRLEN (sizeof(__pmExtLabelSet_v2) - 2 * sizeof(__int32_t))
|
||||
+
|
||||
/*
|
||||
* pack a set of labels into a physical metadata record
|
||||
* - lcp required to provide archive version
|
||||
@@ -210,6 +214,23 @@
|
||||
*nsetsp = 0;
|
||||
*labelsetsp = NULL;
|
||||
|
||||
+ if (rtype == TYPE_LABEL_V2) {
|
||||
+ if (rlen < (int)LABELSET_V2_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "%s: v2 rlen=%d too small (min=%d)\n",
|
||||
+ __FUNCTION__, rlen, (int)LABELSET_V2_MINRLEN);
|
||||
+ return PM_ERR_LOGREC;
|
||||
+ }
|
||||
+ }
|
||||
+ else {
|
||||
+ if (rlen < (int)LABELSET_V3_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "%s: v3 rlen=%d too small (min=%d)\n",
|
||||
+ __FUNCTION__, rlen, (int)LABELSET_V3_MINRLEN);
|
||||
+ return PM_ERR_LOGREC;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
k = 0;
|
||||
if (rtype == TYPE_LABEL_V2) {
|
||||
__pmLoadTimeval((__int32_t *)&tbuf[k], stamp);
|
||||
diff --git a/src/libpcp3/src/e_labels.c b/src/libpcp3/src/e_labels.c
|
||||
--- a/src/libpcp3/src/e_labels.c
|
||||
+++ b/src/libpcp3/src/e_labels.c
|
||||
@@ -56,6 +56,10 @@
|
||||
/* will be expanded if nsets > 0 */
|
||||
} __pmExtLabelSet_v2;
|
||||
|
||||
+/* Minimum rlen (record body without len+type header) to read fixed fields */
|
||||
+#define LABELSET_V3_MINRLEN (sizeof(__pmExtLabelSet_v3) - 2 * sizeof(__int32_t))
|
||||
+#define LABELSET_V2_MINRLEN (sizeof(__pmExtLabelSet_v2) - 2 * sizeof(__int32_t))
|
||||
+
|
||||
/*
|
||||
* pack a set of labels into a physical metadata record
|
||||
* - lcp required to provide archive version
|
||||
@@ -215,6 +219,23 @@
|
||||
*nsetsp = 0;
|
||||
*labelsetsp = NULL;
|
||||
|
||||
+ if (rtype == TYPE_LABEL_V2) {
|
||||
+ if (rlen < (int)LABELSET_V2_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "%s: v2 rlen=%d too small (min=%d)\n",
|
||||
+ __FUNCTION__, rlen, (int)LABELSET_V2_MINRLEN);
|
||||
+ return PM_ERR_LOGREC;
|
||||
+ }
|
||||
+ }
|
||||
+ else {
|
||||
+ if (rlen < (int)LABELSET_V3_MINRLEN) {
|
||||
+ if (pmDebugOptions.logmeta)
|
||||
+ fprintf(stderr, "%s: v3 rlen=%d too small (min=%d)\n",
|
||||
+ __FUNCTION__, rlen, (int)LABELSET_V3_MINRLEN);
|
||||
+ return PM_ERR_LOGREC;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
k = 0;
|
||||
if (rtype == TYPE_LABEL_V2) {
|
||||
__pmLoadTimeval((__int32_t *)&tbuf[k], stamp);
|
||||
155
pcp-7.0.3-pducrash-oob-tests.patch
Normal file
155
pcp-7.0.3-pducrash-oob-tests.patch
Normal file
@ -0,0 +1,155 @@
|
||||
From 7465d7cbdb Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] qa: extend pducrash with tests for vulns 9, 12, 13
|
||||
|
||||
Add three new test functions to pducrash.c exercising the OOB read
|
||||
fixes:
|
||||
|
||||
- decode_log_labelset: calls __pmLogLoadLabelSet with rlen=1 for both
|
||||
V2 and V3 record types, verifying the minimum-length guard rejects
|
||||
undersized records (vuln 9)
|
||||
|
||||
- decode_log_status_oob: crafts a PDU_LOG_STATUS V3 with hostname_len
|
||||
extending past the PDU boundary, verifying the bounds check now runs
|
||||
before strdup (vuln 12)
|
||||
|
||||
- decode_instance_overshoot: crafts a PDU_INSTANCE claiming 2 entries
|
||||
but only containing 1, where the alignment-padded advance pushes ip
|
||||
past pdu_end, verifying the signed pointer guard catches it (vuln 13)
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/qa/513.out b/qa/513.out
|
||||
--- a/qa/513.out
|
||||
+++ b/qa/513.out
|
||||
@@ -277,6 +277,14 @@
|
||||
__pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
|
||||
[log_indom] checking out-of-range stridx with acp==NULL
|
||||
__pmLogLoadInDom: sts = -12373 (Corrupted record in a PCP archive)
|
||||
+[log_labelset] checking rlen too small for v3 header
|
||||
+ __pmLogLoadLabelSet: sts = -12373 (Corrupted record in a PCP archive)
|
||||
+[log_labelset] checking rlen too small for v2 header
|
||||
+ __pmLogLoadLabelSet: sts = -12373 (Corrupted record in a PCP archive)
|
||||
+[log_status_oob] checking hostname_len past PDU boundary
|
||||
+ __pmDecodeLogStatus: sts = -12366 (IPC protocol failure)
|
||||
+[instance_overshoot] checking alignment overshoot past pdu_end
|
||||
+ __pmDecodeInstance: sts = -12366 (IPC protocol failure)
|
||||
=== filtered valgrind report ===
|
||||
Memcheck, a memory error detector
|
||||
Command: src/pducrash
|
||||
diff --git a/qa/src/pducrash.c b/qa/src/pducrash.c
|
||||
--- a/qa/src/pducrash.c
|
||||
+++ b/qa/src/pducrash.c
|
||||
@@ -1703,6 +1703,102 @@
|
||||
}
|
||||
}
|
||||
|
||||
+/*
|
||||
+ * Test __pmLogLoadLabelSet with undersized rlen (vuln 9).
|
||||
+ */
|
||||
+static void
|
||||
+decode_log_labelset(const char *name)
|
||||
+{
|
||||
+ __pmTimestamp stamp;
|
||||
+ pmLabelSet *sets = NULL;
|
||||
+ int type, ident, nsets, sts;
|
||||
+ char tiny[4];
|
||||
+
|
||||
+ fprintf(stderr, "[%s] checking rlen too small for v3 header\n", name);
|
||||
+ memset(tiny, 0, sizeof(tiny));
|
||||
+ sts = __pmLogLoadLabelSet(tiny, 1, TYPE_LABEL, &stamp, &type, &ident, &nsets, &sets);
|
||||
+ fprintf(stderr, " __pmLogLoadLabelSet: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ if (sts >= 0 && sets) pmFreeLabelSets(sets, nsets);
|
||||
+
|
||||
+ fprintf(stderr, "[%s] checking rlen too small for v2 header\n", name);
|
||||
+ memset(tiny, 0, sizeof(tiny));
|
||||
+ sets = NULL;
|
||||
+ sts = __pmLogLoadLabelSet(tiny, 1, TYPE_LABEL_V2, &stamp, &type, &ident, &nsets, &sets);
|
||||
+ fprintf(stderr, " __pmLogLoadLabelSet: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ if (sts >= 0 && sets) pmFreeLabelSets(sets, nsets);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Test __pmDecodeLogStatus with hostname_len past PDU end (vuln 12).
|
||||
+ */
|
||||
+static void
|
||||
+decode_log_status_oob(const char *name)
|
||||
+{
|
||||
+ __pmLoggerStatus *log;
|
||||
+ int sts;
|
||||
+ int save_ipc_version = __pmVersionIPC(0);
|
||||
+ struct log_sts {
|
||||
+ __pmPDUHdr hdr;
|
||||
+ __int32_t buf[20+2*PM_LOG_MAXHOSTLEN+2*PM_TZ_MAXLEN];
|
||||
+ } *log_sts;
|
||||
+
|
||||
+ __pmSetVersionIPC(0, LOG_PDU_VERSION3);
|
||||
+ log_sts = (struct log_sts *)malloc(sizeof(*log_sts));
|
||||
+
|
||||
+ fprintf(stderr, "[%s] checking hostname_len past PDU boundary\n", name);
|
||||
+ memset(log_sts, 0, sizeof(*log_sts));
|
||||
+ log_sts->hdr.len = 100;
|
||||
+ log_sts->hdr.type = PDU_LOG_STATUS;
|
||||
+ /* buf[13] is pmcd_hostname_len in V3 — set to extend past the PDU */
|
||||
+ log_sts->buf[13] = htonl(90);
|
||||
+ sts = __pmDecodeLogStatus((__pmPDU *)log_sts, &log);
|
||||
+ fprintf(stderr, " __pmDecodeLogStatus: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ if (sts == 0) __pmFreeLogStatus(log, 1);
|
||||
+
|
||||
+ free(log_sts);
|
||||
+ __pmSetVersionIPC(0, save_ipc_version);
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * Test __pmDecodeInstance with alignment-padding overshoot (vuln 13).
|
||||
+ * Craft a PDU_INSTANCE with numinst=2 where the first entry's namelen
|
||||
+ * causes the alignment-padded advance to push ip past pdu_end.
|
||||
+ */
|
||||
+static void
|
||||
+decode_instance_overshoot(const char *name)
|
||||
+{
|
||||
+ pmInResult *inresult;
|
||||
+ int sts;
|
||||
+ struct {
|
||||
+ __pmPDUHdr hdr;
|
||||
+ pmInDom indom;
|
||||
+ int numinst;
|
||||
+ /* entry 0: inst + namelen + name (padded) */
|
||||
+ int inst0;
|
||||
+ int namelen0;
|
||||
+ char name0[4]; /* padded to 4 bytes */
|
||||
+ /* entry 1 would start here — but PDU ends before it */
|
||||
+ } *pdu;
|
||||
+
|
||||
+ pdu = (typeof(pdu))malloc(sizeof(*pdu));
|
||||
+
|
||||
+ fprintf(stderr, "[%s] checking alignment overshoot past pdu_end\n", name);
|
||||
+ memset(pdu, 0, sizeof(*pdu));
|
||||
+ pdu->hdr.len = sizeof(*pdu);
|
||||
+ pdu->hdr.type = PDU_INSTANCE;
|
||||
+ pdu->numinst = htonl(2); /* claim 2 entries but only room for 1 */
|
||||
+ pdu->inst0 = htonl(0);
|
||||
+ pdu->namelen0 = htonl(3); /* 3 bytes + pad to 4 = alignment overshoot */
|
||||
+ pdu->name0[0] = 'a';
|
||||
+ pdu->name0[1] = 'b';
|
||||
+ pdu->name0[2] = 'c';
|
||||
+ sts = __pmDecodeInstance((__pmPDU *)pdu, &inresult);
|
||||
+ fprintf(stderr, " __pmDecodeInstance: sts = %d (%s)\n", sts, pmErrStr(sts));
|
||||
+ if (sts >= 0) __pmFreeInResult(inresult);
|
||||
+
|
||||
+ free(pdu);
|
||||
+}
|
||||
+
|
||||
typedef void (*decode_t)(const char *);
|
||||
|
||||
struct pdu {
|
||||
@@ -1738,6 +1834,9 @@
|
||||
{ "desc_ids", decode_desc_ids },
|
||||
{ "descs", decode_descs },
|
||||
{ "log_indom", decode_log_indom },
|
||||
+ { "log_labelset", decode_log_labelset },
|
||||
+ { "log_status_oob", decode_log_status_oob },
|
||||
+ { "instance_overshoot", decode_instance_overshoot },
|
||||
};
|
||||
|
||||
int
|
||||
111
pcp-7.0.3-pmdaroot-peer-credentials.patch
Normal file
111
pcp-7.0.3-pmdaroot-peer-credentials.patch
Normal file
@ -0,0 +1,111 @@
|
||||
From 2a4bd9f81a Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] pmdaroot: add peer credential verification on Unix socket (CWE-403)
|
||||
|
||||
Defense-in-depth for the FD_CLOEXEC fix: verify the UID of connecting
|
||||
clients on the pmdaroot Unix socket using SO_PEERCRED (Linux) or
|
||||
getpeereid (macOS/FreeBSD). Only root (UID 0) and the PCP service
|
||||
user (typically 'pcp') are permitted to connect. Connections from
|
||||
other UIDs are rejected with a log message.
|
||||
|
||||
This prevents exploitation even if the pmdaroot socket fd were to
|
||||
leak to an unprivileged process through a path not covered by
|
||||
FD_CLOEXEC (e.g., direct socket file access).
|
||||
|
||||
The PCP service UID is resolved once at startup via pmGetUsername()
|
||||
and getpwnam(), cached in a static for use in the accept path.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/src/pmdas/root/root.c b/src/pmdas/root/root.c
|
||||
--- a/src/pmdas/root/root.c
|
||||
+++ b/src/pmdas/root/root.c
|
||||
@@ -23,6 +23,9 @@
|
||||
#include "docker.h"
|
||||
#include "podman.h"
|
||||
#include "domain.h"
|
||||
+#if defined(HAVE_PWD_H)
|
||||
+#include <pwd.h>
|
||||
+#endif
|
||||
|
||||
#ifndef S_IRWXU
|
||||
/*
|
||||
@@ -37,6 +40,7 @@
|
||||
static __pmSockAddr *socket_addr;
|
||||
static int socket_fd = -1;
|
||||
static int pmcd_fd = -1;
|
||||
+static uid_t pcp_uid;
|
||||
|
||||
static __pmFdSet connected_fds;
|
||||
int root_maximum_fd;
|
||||
@@ -461,6 +465,42 @@
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
+#if defined(HAVE_STRUCT_UCRED)
|
||||
+ {
|
||||
+ struct ucred cred;
|
||||
+ __pmSockLen len = sizeof(cred);
|
||||
+
|
||||
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == 0) {
|
||||
+ if (cred.uid != 0 && cred.uid != pcp_uid) {
|
||||
+ pmNotifyErr(LOG_ERR,
|
||||
+ "root_accept_client: rejected uid=%d (expected root or pcp[%d])\n",
|
||||
+ cred.uid, pcp_uid);
|
||||
+ close(fd);
|
||||
+ root_client[i].fd = -1;
|
||||
+ root_delete_client(&root_client[i]);
|
||||
+ return NULL;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+#elif defined(HAVE_GETPEEREID)
|
||||
+ {
|
||||
+ uid_t uid;
|
||||
+ gid_t gid;
|
||||
+
|
||||
+ if (getpeereid(fd, &uid, &gid) == 0) {
|
||||
+ if (uid != 0 && uid != pcp_uid) {
|
||||
+ pmNotifyErr(LOG_ERR,
|
||||
+ "root_accept_client: rejected uid=%d (expected root or pcp[%d])\n",
|
||||
+ uid, pcp_uid);
|
||||
+ close(fd);
|
||||
+ root_client[i].fd = -1;
|
||||
+ root_delete_client(&root_client[i]);
|
||||
+ return NULL;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+#endif
|
||||
+
|
||||
if (fd > root_maximum_fd)
|
||||
root_maximum_fd = fd;
|
||||
__pmFD_SET(fd, &connected_fds);
|
||||
@@ -797,6 +837,19 @@
|
||||
}
|
||||
|
||||
static void
|
||||
+root_get_pcp_uid(void)
|
||||
+{
|
||||
+#if defined(HAVE_PWD_H)
|
||||
+ char *username;
|
||||
+ struct passwd *pw;
|
||||
+
|
||||
+ pmGetUsername(&username);
|
||||
+ if ((pw = getpwnam(username)) != NULL)
|
||||
+ pcp_uid = pw->pw_uid;
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
+static void
|
||||
root_check_user(void)
|
||||
{
|
||||
#ifdef HAVE_GETUID
|
||||
@@ -815,6 +868,7 @@
|
||||
root_prep(void)
|
||||
{
|
||||
root_check_user();
|
||||
+ root_get_pcp_uid();
|
||||
root_setup_socket();
|
||||
atexit(root_close_socket);
|
||||
}
|
||||
136
pcp-7.0.3-pmieconf-command-injection.patch
Normal file
136
pcp-7.0.3-pmieconf-command-injection.patch
Normal file
@ -0,0 +1,136 @@
|
||||
From cdc9676ab6 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] pmieconf: fix command injection via $HOME and -f (CWE-78)
|
||||
|
||||
The write_pmiefile() function constructed a shell command via
|
||||
pmsprintf("/bin/mkdir -p %s", fname) and passed it to system().
|
||||
The fname value derives from either $HOME or the -f command-line
|
||||
argument without sanitization, enabling command injection through
|
||||
shell metacharacters in the path.
|
||||
|
||||
Fix: replace system("/bin/mkdir -p ...") with __pmMakePath() which
|
||||
creates directories recursively using mkdir() syscalls directly,
|
||||
with no shell involvement.
|
||||
|
||||
Add qa/2103 verifying that legitimate directory creation works and
|
||||
that shell metacharacters in -f and $HOME paths do not result in
|
||||
command execution.
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/qa/2103 b/qa/2103
|
||||
--- a/qa/2103
|
||||
+++ b/qa/2103
|
||||
@@ -0,0 +1,62 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2103
|
||||
+# Verify pmieconf does not execute shell metacharacters in -f path
|
||||
+# (CWE-78 fix verification)
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+which pmieconf >/dev/null 2>&1 || _notrun "pmieconf not installed"
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+# real QA test starts here
|
||||
+
|
||||
+echo "=== normal -f path should work ==="
|
||||
+mkdir -p $tmp.dir
|
||||
+pmieconf -f $tmp.dir/subdir/test.pmie modify global delta "2min" >$tmp.out 2>&1
|
||||
+_sts=$?
|
||||
+if [ -f $tmp.dir/subdir/test.pmie ]; then
|
||||
+ echo "directory creation and file write succeeded"
|
||||
+else
|
||||
+ echo "FAIL: file not created (exit=$_sts)"
|
||||
+ cat $tmp.out
|
||||
+fi
|
||||
+
|
||||
+echo
|
||||
+echo "=== -f path with semicolon should not execute commands ==="
|
||||
+_bad="$tmp.dir/bad;touch $tmp.dir/pwned"
|
||||
+pmieconf -f "$_bad" modify global delta "2min" >$tmp.out 2>&1
|
||||
+if [ -f "$tmp.dir/pwned" ]; then
|
||||
+ echo "FAIL: shell metacharacter was executed"
|
||||
+else
|
||||
+ echo "no command execution from semicolon in path"
|
||||
+fi
|
||||
+
|
||||
+echo
|
||||
+echo "=== verify no injected file from HOME variable ==="
|
||||
+_badhome="$tmp.dir/home;touch $tmp.dir/pwned2"
|
||||
+HOME="$_badhome" pmieconf modify global delta "2min" >$tmp.out 2>&1
|
||||
+if [ -f "$tmp.dir/pwned2" ]; then
|
||||
+ echo "FAIL: shell metacharacter in HOME was executed"
|
||||
+else
|
||||
+ echo "no command execution from HOME injection"
|
||||
+fi
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2103.out b/qa/2103.out
|
||||
--- a/qa/2103.out
|
||||
+++ b/qa/2103.out
|
||||
@@ -0,0 +1,9 @@
|
||||
+QA output created by 2103
|
||||
+=== normal -f path should work ===
|
||||
+directory creation and file write succeeded
|
||||
+
|
||||
+=== -f path with semicolon should not execute commands ===
|
||||
+no command execution from semicolon in path
|
||||
+
|
||||
+=== verify no injected file from HOME variable ===
|
||||
+no command execution from HOME injection
|
||||
diff --git a/qa/group b/qa/group
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2306,5 +2306,6 @@
|
||||
2104 libpcp local security
|
||||
2101 pmda.sockets local security
|
||||
2102 pmlogmv local security
|
||||
+2103 pmieconf local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/src/pmieconf/rules.c b/src/pmieconf/rules.c
|
||||
--- a/src/pmieconf/rules.c
|
||||
+++ b/src/pmieconf/rules.c
|
||||
@@ -1808,7 +1808,6 @@
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
char *p, *msg = NULL;
|
||||
- char buf[MAXPATHLEN+10];
|
||||
char *fname = get_pmiefile();
|
||||
FILE *fp;
|
||||
int i;
|
||||
@@ -1819,9 +1818,8 @@
|
||||
|
||||
*p = '\0'; /* p is the dirname of fname */
|
||||
if (stat(fname, &sbuf) < 0) {
|
||||
- pmsprintf(buf, sizeof(buf), "/bin/mkdir -p %s", fname);
|
||||
- if (system(buf) < 0) {
|
||||
- pmsprintf(errmsg, sizeof(errmsg), "failed to create directory \"%s\"", p);
|
||||
+ if (__pmMakePath(fname, 0755) < 0) {
|
||||
+ pmsprintf(errmsg, sizeof(errmsg), "failed to create directory \"%s\"", fname);
|
||||
return errmsg;
|
||||
}
|
||||
}
|
||||
544
pcp-7.0.3-pmlogmv-command-injection.patch
Normal file
544
pcp-7.0.3-pmlogmv-command-injection.patch
Normal file
@ -0,0 +1,544 @@
|
||||
From dc73ec0f57 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] pmlogmv: fix command injection in pmlogcp/pmlogmv (CWE-78)
|
||||
|
||||
The do_link() function used system("cp src dst") to copy archive files
|
||||
when link() fails with EXDEV. The source filename was not validated by
|
||||
check_name() and was embedded directly into the shell command, enabling
|
||||
command injection via crafted archive filenames. The do_checksum()
|
||||
function similarly used system() for command detection and popen() for
|
||||
checksum execution.
|
||||
|
||||
Fix:
|
||||
- Replace system("cp ...") with copy_file() using open/read/write
|
||||
syscalls directly, eliminating shell involvement entirely
|
||||
- Replace system("if which ...") checksum detection with access() checks
|
||||
- Replace popen("md5sum <file") with __pmProcessPipe() which uses
|
||||
execvp() internally, passing filenames as argv not shell words
|
||||
- Expand check_name() blocklist to include backtick, braces, backslash,
|
||||
bang, newline and tab (defense-in-depth, no longer the security
|
||||
boundary)
|
||||
- Apply check_name() to source names as well as destination names
|
||||
- Add qa/2102 verifying metacharacter rejection and normal copy
|
||||
|
||||
Reported-by: Francisco Alisson Bezerra, TIM Security Red Team
|
||||
Reported-by: Lucas Gabriel Alves, TIM Security Red Team
|
||||
Reported-by: Massimiliano Brolli, TIM Security Red Team
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/qa/2102 b/qa/2102
|
||||
--- a/qa/2102
|
||||
+++ b/qa/2102
|
||||
@@ -0,0 +1,67 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2102
|
||||
+# Verify pmlogcp/pmlogmv reject shell metacharacters in filenames
|
||||
+# and that normal copy/move operations still work (CWE-78 fix)
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+_filter()
|
||||
+{
|
||||
+ sed \
|
||||
+ -e "s,$tmp,TMP,g" \
|
||||
+ -e "s/pmlogcp/TOOL/" \
|
||||
+ -e "s/pmlogmv/TOOL/" \
|
||||
+ # end
|
||||
+}
|
||||
+
|
||||
+# real QA test starts here
|
||||
+
|
||||
+echo "=== normal pmlogcp should succeed ==="
|
||||
+pmlogcp tmparch/foo $tmp.copy1 2>&1 | _filter
|
||||
+if [ -f $tmp.copy1.meta ] || [ -f $tmp.copy1.0 ]; then
|
||||
+ echo "copy succeeded"
|
||||
+else
|
||||
+ echo "FAIL: copy did not create files"
|
||||
+fi
|
||||
+
|
||||
+echo
|
||||
+echo "=== pmlogcp with backtick in destination should be rejected ==="
|
||||
+pmlogcp tmparch/foo "$tmp.bad\`id\`" >$tmp.out 2>&1
|
||||
+_sts=$?
|
||||
+_filter <$tmp.out
|
||||
+echo "exit status: $_sts"
|
||||
+
|
||||
+echo
|
||||
+echo "=== pmlogcp with semicolon in destination should be rejected ==="
|
||||
+pmlogcp tmparch/foo "$tmp.bad;id" >$tmp.out 2>&1
|
||||
+_sts=$?
|
||||
+_filter <$tmp.out
|
||||
+echo "exit status: $_sts"
|
||||
+
|
||||
+echo
|
||||
+echo "=== pmlogcp with dollar in destination should be rejected ==="
|
||||
+pmlogcp tmparch/foo '$tmp.bad${IFS}' >$tmp.out 2>&1
|
||||
+_sts=$?
|
||||
+_filter <$tmp.out
|
||||
+echo "exit status: $_sts"
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2102.out b/qa/2102.out
|
||||
--- a/qa/2102.out
|
||||
+++ b/qa/2102.out
|
||||
@@ -0,0 +1,15 @@
|
||||
+QA output created by 2102
|
||||
+=== normal pmlogcp should succeed ===
|
||||
+copy succeeded
|
||||
+
|
||||
+=== pmlogcp with backtick in destination should be rejected ===
|
||||
+TOOL: name (TMP.bad`id`) unsafe [shell metacharacter '`']
|
||||
+exit status: 1
|
||||
+
|
||||
+=== pmlogcp with semicolon in destination should be rejected ===
|
||||
+TOOL: name (TMP.bad;id) unsafe [shell metacharacter ';']
|
||||
+exit status: 1
|
||||
+
|
||||
+=== pmlogcp with dollar in destination should be rejected ===
|
||||
+TOOL: name ($tmp.bad${IFS}) unsafe [shell metacharacter '$']
|
||||
+exit status: 1
|
||||
diff --git a/qa/group b/qa/group
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2305,5 +2305,6 @@
|
||||
2106 pmproxy local security
|
||||
2104 libpcp local security
|
||||
2101 pmda.sockets local security
|
||||
+2102 pmlogmv local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/src/pmlogmv/pmlogmv.c b/src/pmlogmv/pmlogmv.c
|
||||
--- a/src/pmlogmv/pmlogmv.c
|
||||
+++ b/src/pmlogmv/pmlogmv.c
|
||||
@@ -13,8 +13,9 @@
|
||||
*/
|
||||
|
||||
/*
|
||||
- * pmlogmv - move/rename PCP archives
|
||||
- * pmlogcp - copy PCP archives
|
||||
+ * pmlogmv - move/rename a PCP archive
|
||||
+ * pmlogcp - copy a PCP archive
|
||||
+ * pmlogls - list files in a PCP archive
|
||||
*/
|
||||
|
||||
#include <unistd.h>
|
||||
@@ -28,7 +29,10 @@
|
||||
|
||||
static int myoverrides(int, pmOptions *);
|
||||
|
||||
-static pmLongOptions longopts[] = {
|
||||
+/*
|
||||
+ * options for pmlogmv|pmlogcp
|
||||
+ */
|
||||
+static pmLongOptions longopts_mvcp[] = {
|
||||
PMAPI_OPTIONS_HEADER("Options"),
|
||||
PMOPT_DEBUG,
|
||||
{ "checksum", 0, 'c', 0, "checksum all source and destintion files when copying" },
|
||||
@@ -39,18 +43,39 @@
|
||||
PMAPI_OPTIONS_END
|
||||
};
|
||||
|
||||
-static pmOptions opts = {
|
||||
+static pmOptions opts_mvcp = {
|
||||
.short_options = "cD:fNV?",
|
||||
- .long_options = longopts,
|
||||
+ .long_options = longopts_mvcp,
|
||||
.short_usage = "[options] srcname dstname",
|
||||
.override = myoverrides
|
||||
};
|
||||
|
||||
+/*
|
||||
+ * options for pmlogls
|
||||
+ */
|
||||
+static pmLongOptions longopts_ls[] = {
|
||||
+ PMAPI_OPTIONS_HEADER("Options"),
|
||||
+ PMOPT_DEBUG,
|
||||
+ { "verbose", 0, 'V', 0, "increase diagnostic verbosity" },
|
||||
+ PMOPT_HELP,
|
||||
+ PMAPI_OPTIONS_END
|
||||
+};
|
||||
+
|
||||
+static pmOptions opts_ls = {
|
||||
+ .short_options = "D:V?",
|
||||
+ .long_options = longopts_ls,
|
||||
+ .short_usage = "[options] srcname",
|
||||
+ .override = myoverrides
|
||||
+};
|
||||
+
|
||||
+static pmOptions *opts;
|
||||
+
|
||||
static char *progname;
|
||||
|
||||
+static int mode; /* MV, CP or LS depending on argv[0] */
|
||||
#define MV 1
|
||||
#define CP 2
|
||||
-static int mode; /* MV or CP depending on argv[0] */
|
||||
+#define LS 3
|
||||
|
||||
static int showme = 0;
|
||||
static int verbose = 0;
|
||||
@@ -78,15 +103,22 @@
|
||||
return 0;
|
||||
}
|
||||
|
||||
+/*
|
||||
+ * Defense-in-depth: reject filenames containing shell metacharacters.
|
||||
+ * The copy and checksum paths no longer use system()/popen() so these
|
||||
+ * characters are not directly dangerous, but archive names containing
|
||||
+ * them are almost certainly bogus and this guards against future code
|
||||
+ * paths that might reintroduce shell interpretation.
|
||||
+ */
|
||||
static int
|
||||
check_name(char *name)
|
||||
{
|
||||
- char *meta = " $?*[(|;&<>";
|
||||
+ char *meta = " $?*[(|;&<>`{}\\!\n\t";
|
||||
char *p;
|
||||
|
||||
for (p = meta; *p; p++) {
|
||||
if (strchr(name, *p) != NULL) {
|
||||
- fprintf(stderr, "%s: dstname (%s) unsafe [shell metacharacter '%c']\n", progname, name, *p);
|
||||
+ fprintf(stderr, "%s: name (%s) unsafe [shell metacharacter '%c']\n", progname, name, *p);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -152,7 +184,6 @@
|
||||
void
|
||||
do_checksum(const char *file, char *sum)
|
||||
{
|
||||
- char cmd[2*MAXPATHLEN+20];
|
||||
static char *executable = NULL;
|
||||
FILE *fp;
|
||||
static int trunc_warn = 0;
|
||||
@@ -163,43 +194,49 @@
|
||||
* prefer md5sum, then sha256sum, then sha1sum, then sum,
|
||||
* else do nothing
|
||||
*/
|
||||
- snprintf(cmd, sizeof(cmd), "if which md5sum >/dev/null 2>&1; then exit 0; fi; exit 1");
|
||||
- if (system(cmd) == 0)
|
||||
- executable = "md5sum";
|
||||
- else {
|
||||
- snprintf(cmd, sizeof(cmd), "if which sha256sum >/dev/null 2>&1; then exit 0; fi; exit 1");
|
||||
- if (system(cmd) == 0)
|
||||
- executable = "sha256sum";
|
||||
- else {
|
||||
- snprintf(cmd, sizeof(cmd), "if which sha1sum >/dev/null 2>&1; then exit 0; fi; exit 1");
|
||||
- if (system(cmd) == 0)
|
||||
- executable = "sha1sum";
|
||||
- else {
|
||||
- snprintf(cmd, sizeof(cmd), "if which sum >/dev/null 2>&1; then exit 0; fi; exit 1");
|
||||
- if (system(cmd) == 0)
|
||||
- executable = "sum";
|
||||
- else {
|
||||
- executable = "none";
|
||||
- fprintf(stderr, "%s: warning: no checksum command found, checksums skipped\n", progname);
|
||||
- }
|
||||
- }
|
||||
+ static const char *candidates[] = {
|
||||
+ "md5sum", "sha256sum", "sha1sum", "sum", NULL
|
||||
+ };
|
||||
+ const char **cp;
|
||||
+ char path[MAXPATHLEN];
|
||||
+
|
||||
+ executable = "none";
|
||||
+ for (cp = candidates; *cp != NULL; cp++) {
|
||||
+ snprintf(path, sizeof(path), "/usr/bin/%s", *cp);
|
||||
+ if (access(path, X_OK) == 0) {
|
||||
+ executable = (char *)*cp;
|
||||
+ break;
|
||||
+ }
|
||||
+ snprintf(path, sizeof(path), "/usr/sbin/%s", *cp);
|
||||
+ if (access(path, X_OK) == 0) {
|
||||
+ executable = (char *)*cp;
|
||||
+ break;
|
||||
}
|
||||
}
|
||||
+ if (strcmp(executable, "none") == 0)
|
||||
+ fprintf(stderr, "%s: warning: no checksum command found, checksums skipped\n", progname);
|
||||
if (verbose && strcmp(executable, "none") != 0)
|
||||
printf("checksum cmd: %s\n", executable);
|
||||
}
|
||||
sum[0] = '\0';
|
||||
if (strcmp(executable, "none") == 0)
|
||||
return;
|
||||
- snprintf(cmd, sizeof(cmd), "%s <%s", executable, file);
|
||||
- if ((fp = popen(cmd, "r")) == NULL) {
|
||||
- /*
|
||||
- * abandon checksuming ...
|
||||
- */
|
||||
- fprintf(stderr, "%s: pipe(\"%s\") failed: %s\n", progname, cmd, strerror(errno));
|
||||
- executable = "none";
|
||||
+ {
|
||||
+ __pmExecCtl_t *argp = NULL;
|
||||
+ int sts;
|
||||
+
|
||||
+ if ((sts = __pmProcessAddArg(&argp, executable)) < 0 ||
|
||||
+ (sts = __pmProcessAddArg(&argp, file)) < 0) {
|
||||
+ executable = "none";
|
||||
+ return;
|
||||
+ }
|
||||
+ if ((sts = __pmProcessPipe(&argp, "r", PM_EXEC_TOSS_NONE, &fp)) < 0) {
|
||||
+ fprintf(stderr, "%s: __pmProcessPipe(\"%s\") failed: %s\n", progname, executable, pmErrStr(sts));
|
||||
+ executable = "none";
|
||||
+ return;
|
||||
+ }
|
||||
}
|
||||
- else {
|
||||
+ {
|
||||
char *p = sum;
|
||||
int c;
|
||||
while ((c = fgetc(fp)) != EOF) {
|
||||
@@ -208,9 +245,6 @@
|
||||
break;
|
||||
}
|
||||
if (p >= &sum[MAX_CHECKSUM]) {
|
||||
- /*
|
||||
- * avoid buffer overrun, report only once unless -V
|
||||
- */
|
||||
if (trunc_warn++ == 0 || verbose)
|
||||
fprintf(stderr, "%s: warning: checksum truncated after %d characters\n", progname, MAX_CHECKSUM);
|
||||
*p = '\0';
|
||||
@@ -218,12 +252,55 @@
|
||||
}
|
||||
*p++ = c;
|
||||
}
|
||||
- pclose(fp);
|
||||
+ __pmProcessPipeClose(fp);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+/*
|
||||
+ * copy a file using read/write - no shell involvement
|
||||
+ */
|
||||
+static int
|
||||
+copy_file(const char *src, const char *dst)
|
||||
+{
|
||||
+ int sfd, dfd;
|
||||
+ struct stat sbuf;
|
||||
+ ssize_t nread, nwritten;
|
||||
+ char buf[BUFSIZ];
|
||||
+
|
||||
+ if ((sfd = open(src, O_RDONLY)) < 0)
|
||||
+ return -1;
|
||||
+ if (fstat(sfd, &sbuf) < 0) {
|
||||
+ close(sfd);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ if ((dfd = open(dst, O_WRONLY|O_CREAT|O_EXCL, sbuf.st_mode & 0777)) < 0) {
|
||||
+ close(sfd);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ while ((nread = read(sfd, buf, sizeof(buf))) > 0) {
|
||||
+ char *p = buf;
|
||||
+ while (nread > 0) {
|
||||
+ nwritten = write(dfd, p, nread);
|
||||
+ if (nwritten < 0) {
|
||||
+ close(sfd);
|
||||
+ close(dfd);
|
||||
+ unlink(dst);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ nread -= nwritten;
|
||||
+ p += nwritten;
|
||||
+ }
|
||||
+ }
|
||||
+ close(sfd);
|
||||
+ if (nread < 0 || close(dfd) < 0) {
|
||||
+ unlink(dst);
|
||||
+ return -1;
|
||||
}
|
||||
+ return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
- * make link or copy for one physical file
|
||||
+ * make link or make copy or list for one physical file
|
||||
* return codes:
|
||||
* 1: ok
|
||||
* 0: source file not found
|
||||
@@ -250,6 +327,10 @@
|
||||
}
|
||||
if (access(src, F_OK) == 0) {
|
||||
/* src exists ... off to the races */
|
||||
+ if (mode == LS) {
|
||||
+ printf("%s\n", src);
|
||||
+ return 1;
|
||||
+ }
|
||||
switch (vol) {
|
||||
case PM_LOG_VOL_TI:
|
||||
snprintf(dst, sizeof(src), "%s.index%s", dstname, *suff);
|
||||
@@ -279,7 +360,6 @@
|
||||
#endif
|
||||
/* pmlogcp or link() failed cross-device, need to copy ... */
|
||||
int sts;
|
||||
- char cmd[2*MAXPATHLEN+60];
|
||||
char sum_src[MAX_CHECKSUM+1];
|
||||
char sum_dst[MAX_CHECKSUM+1];
|
||||
if (checksum) {
|
||||
@@ -292,8 +372,7 @@
|
||||
printf("source checksum: %s\n", sum_src);
|
||||
}
|
||||
|
||||
- snprintf(cmd, sizeof(cmd), "cp %s %s", src, dst);
|
||||
- if ((sts = system(cmd)) != 0) {
|
||||
+ if ((sts = copy_file(src, dst)) != 0) {
|
||||
fprintf(stderr, "%s: copy %s -> %s failed: %s\n", progname, src, dst, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
@@ -389,6 +468,9 @@
|
||||
{
|
||||
int i;
|
||||
|
||||
+ if (mode == LS)
|
||||
+ exit(0);
|
||||
+
|
||||
if (sig != 0) {
|
||||
fprintf(stderr, "Caught signal %d\n", sig);
|
||||
verbose = 1;
|
||||
@@ -440,19 +522,27 @@
|
||||
pmSetProgname(argv[0]);
|
||||
progname = pmGetProgname();
|
||||
|
||||
- if (strcmp(progname, "pmlogmv") == 0)
|
||||
+ if (strcmp(progname, "pmlogmv") == 0) {
|
||||
mode = MV;
|
||||
- else if (strcmp(progname, "pmlogcp") == 0)
|
||||
+ opts = &opts_mvcp;
|
||||
+ }
|
||||
+ else if (strcmp(progname, "pmlogcp") == 0) {
|
||||
mode = CP;
|
||||
+ opts = &opts_mvcp;
|
||||
+ }
|
||||
+ else if (strcmp(progname, "pmlogls") == 0) {
|
||||
+ mode = LS;
|
||||
+ opts = &opts_ls;
|
||||
+ }
|
||||
else {
|
||||
- fprintf(stderr, "%s: Arrgh, not pmlogmv nor pmlogcp so I don't know who I am!\n", progname);
|
||||
+ fprintf(stderr, "%s: Arrgh, not pmlogmv nor pmlogcp nor pmlogls so I don't know who I am!\n", progname);
|
||||
return(1);
|
||||
}
|
||||
|
||||
setlinebuf(stdout);
|
||||
setlinebuf(stderr);
|
||||
|
||||
- while ((c = pmGetOptions(argc, argv, &opts)) != EOF) {
|
||||
+ while ((c = pmGetOptions(argc, argv, opts)) != EOF) {
|
||||
switch (c) {
|
||||
|
||||
case 'c': /* checksum if copying */
|
||||
@@ -473,24 +563,27 @@
|
||||
|
||||
case '?':
|
||||
default:
|
||||
- opts.errors++;
|
||||
+ opts->errors++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- if (opts.errors || opts.optind != argc-2) {
|
||||
- pmUsageMessage(&opts);
|
||||
+ if (opts->errors ||
|
||||
+ (mode != LS && opts->optind != argc-2) ||
|
||||
+ (mode == LS && opts->optind != argc-1)) {
|
||||
+ pmUsageMessage(opts);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
- srcname = strdup(argv[opts.optind]);
|
||||
+ srcname = strdup(argv[opts->optind]);
|
||||
if (srcname == NULL) {
|
||||
fprintf(stderr, "%s: malloc(srcname) failed!\n", progname);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ((sts = pmNewContext(PM_CONTEXT_ARCHIVE, srcname)) < 0) {
|
||||
- fprintf(stderr, "%s: Cannot open archive \"%s\": %s\n", progname, srcname, pmErrStr(sts));
|
||||
+ if (mode != LS || verbose)
|
||||
+ fprintf(stderr, "%s: Cannot open archive \"%s\": %s\n", progname, srcname, pmErrStr(sts));
|
||||
exit(1);
|
||||
}
|
||||
if ((ctxp = __pmHandleToPtr(sts)) == NULL) {
|
||||
@@ -499,25 +592,35 @@
|
||||
}
|
||||
srcname = ctxp->c_archctl->ac_log->name;
|
||||
|
||||
- opts.optind++;
|
||||
- /*
|
||||
- * default is that dstname is really the basename for the
|
||||
- * destination archive
|
||||
- */
|
||||
- snprintf(dstname, sizeof(dstname), "%s", argv[opts.optind]);
|
||||
- sb.st_mode = 0;
|
||||
- if (stat(argv[opts.optind], &sb) == 0 && S_ISDIR(sb.st_mode)) {
|
||||
- /*
|
||||
- * dstname is an existing directory ... append
|
||||
- * basename of srcname
|
||||
- */
|
||||
- snprintf(dstname, sizeof(dstname), "%s%c%s",
|
||||
- argv[opts.optind], pmPathSeparator(), basename(srcname));
|
||||
- }
|
||||
+ /* strip a leading "./" from the libpcp name */
|
||||
+ if (strncmp(srcname, "./", 2) == 0)
|
||||
+ srcname += 2;
|
||||
|
||||
- if (!force && check_name(dstname) < 0) {
|
||||
- /* error reported in check_name() */
|
||||
- exit(1);
|
||||
+ if (mode != LS) {
|
||||
+ opts->optind++;
|
||||
+ /*
|
||||
+ * default is that dstname is really the basename for the
|
||||
+ * destination archive
|
||||
+ */
|
||||
+ snprintf(dstname, sizeof(dstname), "%s", argv[opts->optind]);
|
||||
+ sb.st_mode = 0;
|
||||
+ if (stat(argv[opts->optind], &sb) == 0 && S_ISDIR(sb.st_mode)) {
|
||||
+ /*
|
||||
+ * dstname is an existing directory ... append
|
||||
+ * basename of srcname
|
||||
+ */
|
||||
+ snprintf(dstname, sizeof(dstname), "%s%c%s",
|
||||
+ argv[opts->optind], pmPathSeparator(), basename(srcname));
|
||||
+ }
|
||||
+
|
||||
+ if (!force && check_name(dstname) < 0) {
|
||||
+ /* error reported in check_name() */
|
||||
+ exit(1);
|
||||
+ }
|
||||
+ if (!force && check_name(srcname) < 0) {
|
||||
+ /* error reported in check_name() */
|
||||
+ exit(1);
|
||||
+ }
|
||||
}
|
||||
|
||||
if (setup_sufftab() < 0) {
|
||||
@@ -556,6 +659,7 @@
|
||||
do_unlink(0, srcname, PM_LOG_VOL_TI);
|
||||
do_unlink(0, srcname, PM_LOG_VOL_META);
|
||||
}
|
||||
+
|
||||
return 0;
|
||||
|
||||
/* fatal error once we're started ... remove any dstname files */
|
||||
199
pcp-7.0.3-pmproxy-logger-auth.patch
Normal file
199
pcp-7.0.3-pmproxy-logger-auth.patch
Normal file
@ -0,0 +1,199 @@
|
||||
From 4121ae06f5 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] pmproxy: add optional authentication for logger servlet
|
||||
|
||||
The pmproxy logger servlet endpoints (/logger/label, /logger/meta,
|
||||
/logger/index, /logger/volume) are registered unconditionally with no
|
||||
authentication check, allowing any HTTP client to submit archive data.
|
||||
|
||||
Add a new pmproxy.conf option [pmlogger] authenticate = true that
|
||||
enables HTTP Basic authentication for all logger servlet requests.
|
||||
When set, requests without valid credentials are rejected with
|
||||
HTTP 403 Forbidden. Disabled by default to preserve existing behavior.
|
||||
|
||||
This complements the global -S flag: -S requires authentication for
|
||||
all servlets, while [pmlogger] authenticate = true targets only the
|
||||
logger servlet endpoints.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/qa/2108 b/qa/2108
|
||||
--- a/qa/2108
|
||||
+++ b/qa/2108
|
||||
@@ -0,0 +1,97 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2108
|
||||
+# Verify pmproxy logger servlet authentication via pmproxy.conf
|
||||
+# [pmlogger] authenticate = true
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+which curl >/dev/null 2>&1 || _notrun "no curl executable installed"
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ [ -n "$__pid" ] && kill $__pid 2>/dev/null
|
||||
+ wait $__pid 2>/dev/null
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+__pid=""
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+# real QA test starts here
|
||||
+
|
||||
+echo "=== logger servlet with authenticate = true ==="
|
||||
+__port=`_find_free_port`
|
||||
+cat >$tmp.conf <<EOF
|
||||
+[pmproxy]
|
||||
+pcp.enabled = true
|
||||
+http.enabled = true
|
||||
+[pmlogger]
|
||||
+enabled = true
|
||||
+authenticate = true
|
||||
+EOF
|
||||
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log -c $tmp.conf &
|
||||
+__pid=$!
|
||||
+sleep 1
|
||||
+if ! kill -0 $__pid 2>/dev/null; then
|
||||
+ echo "FAIL: pmproxy did not start"
|
||||
+ exit
|
||||
+fi
|
||||
+
|
||||
+# unauthenticated POST to logger/label should be rejected
|
||||
+__code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
+ -X POST "http://localhost:$__port/logger/label" \
|
||||
+ -H 'Content-Type: application/octet-stream' \
|
||||
+ --data-binary 'dummy' 2>/dev/null)
|
||||
+echo "unauthenticated POST /logger/label: HTTP $__code"
|
||||
+
|
||||
+# unauthenticated GET to pmapi should still work (not logger servlet)
|
||||
+__code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
+ "http://localhost:$__port/pmapi/context?hostspec=localhost" 2>/dev/null)
|
||||
+echo "unauthenticated GET /pmapi/context: HTTP $__code"
|
||||
+
|
||||
+kill $__pid
|
||||
+wait $__pid 2>/dev/null
|
||||
+__pid=""
|
||||
+
|
||||
+echo
|
||||
+echo "=== logger servlet without authenticate (default) ==="
|
||||
+__port=`_find_free_port`
|
||||
+cat >$tmp.conf2 <<EOF
|
||||
+[pmproxy]
|
||||
+pcp.enabled = true
|
||||
+http.enabled = true
|
||||
+[pmlogger]
|
||||
+enabled = true
|
||||
+EOF
|
||||
+$PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log2 -c $tmp.conf2 &
|
||||
+__pid=$!
|
||||
+sleep 1
|
||||
+if ! kill -0 $__pid 2>/dev/null; then
|
||||
+ echo "FAIL: pmproxy did not start"
|
||||
+ exit
|
||||
+fi
|
||||
+
|
||||
+# unauthenticated POST to logger/label should be allowed (will fail on bad data, not auth)
|
||||
+__code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||
+ -X POST "http://localhost:$__port/logger/label" \
|
||||
+ -H 'Content-Type: application/octet-stream' \
|
||||
+ --data-binary 'dummy' 2>/dev/null)
|
||||
+echo "unauthenticated POST /logger/label: HTTP $__code"
|
||||
+
|
||||
+kill $__pid
|
||||
+wait $__pid 2>/dev/null
|
||||
+__pid=""
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2108.out b/qa/2108.out
|
||||
--- a/qa/2108.out
|
||||
+++ b/qa/2108.out
|
||||
@@ -0,0 +1,7 @@
|
||||
+QA output created by 2108
|
||||
+=== logger servlet with authenticate = true ===
|
||||
+unauthenticated POST /logger/label: HTTP 403
|
||||
+unauthenticated GET /pmapi/context: HTTP 200
|
||||
+
|
||||
+=== logger servlet without authenticate (default) ===
|
||||
+unauthenticated POST /logger/label: HTTP 400
|
||||
diff --git a/qa/group b/qa/group
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2308,5 +2308,6 @@
|
||||
2102 pmlogmv local security
|
||||
2103 pmieconf local security
|
||||
2107 libpcp local security
|
||||
+2108 pmproxy local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/src/pmproxy/pmproxy.conf b/src/pmproxy/pmproxy.conf
|
||||
--- a/src/pmproxy/pmproxy.conf
|
||||
+++ b/src/pmproxy/pmproxy.conf
|
||||
@@ -122,6 +122,9 @@
|
||||
# allow REST API webhook receiving remote pmlogger(1) archive content
|
||||
enabled = true
|
||||
|
||||
+# require HTTP Basic authentication for logger servlet endpoints
|
||||
+#authenticate = true
|
||||
+
|
||||
# bypass persistent storage of pmlogger archives, use key server only
|
||||
#cached = true
|
||||
|
||||
diff --git a/src/pmproxy/src/logger.c b/src/pmproxy/src/logger.c
|
||||
--- a/src/pmproxy/src/logger.c
|
||||
+++ b/src/pmproxy/src/logger.c
|
||||
@@ -106,6 +106,9 @@
|
||||
if (status >= 0) {
|
||||
code = HTTP_STATUS_OK;
|
||||
body = pmlogger_success;
|
||||
+ } else if (client->u.http.parser.status_code) {
|
||||
+ code = client->u.http.parser.status_code;
|
||||
+ body = pmlogger_failure;
|
||||
} else {
|
||||
if (status == -EEXIST)
|
||||
code = HTTP_STATUS_CONFLICT;
|
||||
@@ -139,6 +142,8 @@
|
||||
proxylog(level, message, baton->client->proxy);
|
||||
}
|
||||
|
||||
+static int pmlogger_authenticate;
|
||||
+
|
||||
static pmLogGroupSettings pmlogger_settings = {
|
||||
.callbacks.on_archive = on_pmlogger_archive,
|
||||
.callbacks.on_done = on_pmlogger_done,
|
||||
@@ -273,6 +278,10 @@
|
||||
{
|
||||
if (pmDebugOptions.http)
|
||||
fprintf(stderr, "logger servlet headers (client=%p)\n", client);
|
||||
+ if (pmlogger_authenticate &&
|
||||
+ (!client->u.http.username || !client->u.http.password)) {
|
||||
+ client->u.http.parser.status_code = HTTP_STATUS_FORBIDDEN;
|
||||
+ }
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -378,6 +387,11 @@
|
||||
{
|
||||
mmv_registry_t *registry = proxymetrics(proxy, METRICS_LOGGROUP);
|
||||
mmv_registry_t *logpaths = proxymetrics(proxy, METRICS_LOGPATHS);
|
||||
+ sds value;
|
||||
+
|
||||
+ if ((value = pmIniFileLookup(proxy->config, "pmlogger", "authenticate"))
|
||||
+ && strcmp(value, "true") == 0)
|
||||
+ pmlogger_authenticate = 1;
|
||||
|
||||
PARAM_CLIENT = sdsnew("client");
|
||||
|
||||
179
pcp-7.0.3-pmproxy-logger-meta-network.patch
Normal file
179
pcp-7.0.3-pmproxy-logger-meta-network.patch
Normal file
@ -0,0 +1,179 @@
|
||||
From bf898b50ef Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] qa: add network-layer regression tests for pmproxy /logger/meta
|
||||
|
||||
Adapted from PoC exploits provided by TIM Security Red Team to test
|
||||
the pmproxy HTTP streaming path that pducrash.c does not exercise.
|
||||
|
||||
qa/2109 starts pmproxy, creates a valid archive via POST /logger/label,
|
||||
then POSTs three malformed metadata records to /logger/meta:
|
||||
- Vuln 1: TYPE_INDOM with stridx=0x7FFFFFFF (OOB pointer deref)
|
||||
- Vuln 9: TYPE_LABEL with hdr.len=13 (rlen=1, undersized for header)
|
||||
- Vuln 10: TYPE_INDOM_DELTA with hdr.len=13 (OOB numinst read)
|
||||
|
||||
Verifies pmproxy handles each gracefully and remains responsive.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/qa/2109 b/qa/2109
|
||||
--- a/qa/2109
|
||||
+++ b/qa/2109
|
||||
@@ -0,0 +1,145 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2109
|
||||
+# Verify pmproxy /logger/meta endpoint rejects malformed metadata
|
||||
+# records without crashing (network-layer regression for vulns 1, 9, 10)
|
||||
+#
|
||||
+# Adapted from PoC exploits provided by TIM Security Red Team.
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+which curl >/dev/null 2>&1 || _notrun "no curl executable installed"
|
||||
+which python3 >/dev/null 2>&1 || _notrun "no python3 executable installed"
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ [ -n "$__pid" ] && kill $__pid 2>/dev/null
|
||||
+ wait $__pid 2>/dev/null
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+__pid=""
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+__port=`_find_free_port`
|
||||
+mkdir -p $tmp.archdir
|
||||
+cat >$tmp.conf <<EOF
|
||||
+[pmproxy]
|
||||
+pcp.enabled = true
|
||||
+http.enabled = true
|
||||
+[discover]
|
||||
+enabled = false
|
||||
+EOF
|
||||
+PCP_REMOTE_ARCHIVE_DIR=$tmp.archdir $PCP_BINADM_DIR/pmproxy -f -p $__port -l $tmp.log -c $tmp.conf &
|
||||
+__pid=$!
|
||||
+sleep 2
|
||||
+
|
||||
+if ! kill -0 $__pid 2>/dev/null; then
|
||||
+ echo "FAIL: pmproxy did not start"
|
||||
+ exit
|
||||
+fi
|
||||
+
|
||||
+_filter_archive_id()
|
||||
+{
|
||||
+ sed -e 's/archive created: [0-9]*/archive created: ARCHIVE_ID/'
|
||||
+}
|
||||
+
|
||||
+# real QA test starts here
|
||||
+python3 - $__port <<'PYEOF' | _filter_archive_id
|
||||
+import http.client, json, struct, sys, time
|
||||
+
|
||||
+PORT = int(sys.argv[1])
|
||||
+
|
||||
+def post(path, body):
|
||||
+ c = http.client.HTTPConnection("localhost", PORT, timeout=10)
|
||||
+ c.request("POST", path, body=body,
|
||||
+ headers={"Content-Type": "application/octet-stream"})
|
||||
+ try:
|
||||
+ r = c.getresponse()
|
||||
+ data = r.read()
|
||||
+ c.close()
|
||||
+ return r.status, data
|
||||
+ except http.client.RemoteDisconnected:
|
||||
+ return 0, b"connection lost"
|
||||
+
|
||||
+# create a valid archive via POST /logger/label
|
||||
+PM_LOG_MAGIC = 0x50052600
|
||||
+LABEL_MAGIC = PM_LOG_MAGIC | 0x02
|
||||
+LABEL_V2_SIZE = 4 + 4 + 8 + 4 + 64 + 40
|
||||
+TOTAL_SIZE = LABEL_V2_SIZE + 8
|
||||
+
|
||||
+hostname = b'testhost\x00' + b'\x00' * (64 - 9)
|
||||
+timezone = b'UTC\x00' + b'\x00' * (40 - 4)
|
||||
+
|
||||
+label = struct.pack(">I", LABEL_MAGIC)
|
||||
+label += struct.pack(">i", 1337)
|
||||
+label += struct.pack(">ii", int(time.time()), 0)
|
||||
+label += struct.pack(">i", 0)
|
||||
+label += hostname + timezone
|
||||
+
|
||||
+label_body = struct.pack(">i", TOTAL_SIZE) + label + struct.pack(">i", TOTAL_SIZE)
|
||||
+
|
||||
+status, resp = post("/logger/label", label_body)
|
||||
+if status != 200:
|
||||
+ print(f"FAIL: /logger/label returned HTTP {status}")
|
||||
+ sys.exit(0)
|
||||
+archive_id = json.loads(resp)["archive"]
|
||||
+print(f"archive created: {archive_id}")
|
||||
+
|
||||
+# --- Vuln 1: TYPE_INDOM with OOB stridx ---
|
||||
+print("=== vuln 1: TYPE_INDOM with OOB stridx ===")
|
||||
+TYPE_INDOM = 5
|
||||
+body = (
|
||||
+ struct.pack(">II", 0, 0) + # sec[2]
|
||||
+ struct.pack(">I", 0) + # nsec
|
||||
+ struct.pack(">I", 1) + # indom
|
||||
+ struct.pack(">I", 1) + # numinst=1
|
||||
+ struct.pack(">I", 0) + # instlist[0]
|
||||
+ struct.pack(">I", 0x7FFFFFFF) + # stridx[0] OOB
|
||||
+ b'\x00'
|
||||
+)
|
||||
+total = 8 + len(body) + 4
|
||||
+record = struct.pack(">II", total, TYPE_INDOM) + body + struct.pack(">I", total)
|
||||
+status, resp = post(f"/logger/meta/{archive_id}", record)
|
||||
+print(f"pmproxy responded (not crashed)")
|
||||
+
|
||||
+# --- Vuln 9: TYPE_LABEL with rlen=1 (undersized) ---
|
||||
+print("=== vuln 9: TYPE_LABEL with rlen=1 ===")
|
||||
+TYPE_LABEL = 7
|
||||
+hdr_len = 13
|
||||
+record = struct.pack(">ii", hdr_len, TYPE_LABEL) + b'\x00' + struct.pack(">i", hdr_len)
|
||||
+status, resp = post(f"/logger/meta/{archive_id}", record)
|
||||
+print(f"pmproxy responded (not crashed)")
|
||||
+
|
||||
+# --- Vuln 10: TYPE_INDOM_DELTA with rlen=1 (OOB numinst) ---
|
||||
+print("=== vuln 10: TYPE_INDOM_DELTA with rlen=1 ===")
|
||||
+TYPE_INDOM_DELTA = 6
|
||||
+hdr_len = 13
|
||||
+record = struct.pack(">ii", hdr_len, TYPE_INDOM_DELTA) + b'\x00' + struct.pack(">i", hdr_len)
|
||||
+status, resp = post(f"/logger/meta/{archive_id}", record)
|
||||
+print(f"pmproxy responded (not crashed)")
|
||||
+
|
||||
+# verify pmproxy is still alive after all malformed records
|
||||
+try:
|
||||
+ c = http.client.HTTPConnection("localhost", PORT, timeout=5)
|
||||
+ c.request("GET", "/pmapi/ping")
|
||||
+ r = c.getresponse()
|
||||
+ r.read()
|
||||
+ c.close()
|
||||
+ print("=== pmproxy still responding after all tests ===")
|
||||
+except Exception:
|
||||
+ print("FAIL: pmproxy is not responding")
|
||||
+PYEOF
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2109.out b/qa/2109.out
|
||||
--- a/qa/2109.out
|
||||
+++ b/qa/2109.out
|
||||
@@ -0,0 +1,9 @@
|
||||
+QA output created by 2109
|
||||
+archive created: ARCHIVE_ID
|
||||
+=== vuln 1: TYPE_INDOM with OOB stridx ===
|
||||
+pmproxy responded (not crashed)
|
||||
+=== vuln 9: TYPE_LABEL with rlen=1 ===
|
||||
+pmproxy responded (not crashed)
|
||||
+=== vuln 10: TYPE_INDOM_DELTA with rlen=1 ===
|
||||
+pmproxy responded (not crashed)
|
||||
+=== pmproxy still responding after all tests ===
|
||||
41
pcp-7.0.3-pmproxy-rest-certreqd.patch
Normal file
41
pcp-7.0.3-pmproxy-rest-certreqd.patch
Normal file
@ -0,0 +1,41 @@
|
||||
From 81a9efe96d Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] pmproxy: enforce -Q (CERT_REQD) for REST API connections
|
||||
|
||||
The -Q flag (PM_SERVER_FEATURE_CERT_REQD) was only enforced in the
|
||||
legacy PCP wire protocol path (deprecated.c). The modern HTTP/REST
|
||||
API path had no check, allowing unauthenticated plain-HTTP clients
|
||||
to access all endpoints even when -Q was specified.
|
||||
|
||||
Add enforcement in on_headers_complete() alongside the existing -S
|
||||
(CREDS_REQD) check: when CERT_REQD is active, reject requests where
|
||||
the connection is not TLS or no client certificate was presented.
|
||||
Returns HTTP 403 Forbidden. If OpenSSL is not compiled in, all
|
||||
connections are rejected when -Q is set since TLS is unavailable.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/src/pmproxy/src/http.c b/src/pmproxy/src/http.c
|
||||
--- a/src/pmproxy/src/http.c
|
||||
+++ b/src/pmproxy/src/http.c
|
||||
@@ -1110,6 +1110,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ /* client certificate required for all servlets */
|
||||
+ if (__pmServerHasFeature(PM_SERVER_FEATURE_CERT_REQD)) {
|
||||
+#ifdef HAVE_OPENSSL
|
||||
+ if (!client->stream.secure ||
|
||||
+ !client->secure.ssl ||
|
||||
+ SSL_get_peer_certificate(client->secure.ssl) == NULL) {
|
||||
+ client->u.http.parser.status_code = HTTP_STATUS_FORBIDDEN;
|
||||
+ }
|
||||
+#else
|
||||
+ /* no TLS support compiled in, reject all connections */
|
||||
+ client->u.http.parser.status_code = HTTP_STATUS_FORBIDDEN;
|
||||
+#endif
|
||||
+ }
|
||||
+
|
||||
return sts;
|
||||
}
|
||||
|
||||
42
pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch
Normal file
42
pcp-7.0.3-scanmeta-LogLoadInDom-caller.patch
Normal file
@ -0,0 +1,42 @@
|
||||
From be9ade9950 Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] qa/src/scanmeta.c: fix call to __pmLogLoadInDom()
|
||||
|
||||
scanmeta was *using* the acp == NULL guard to dodge the rlen test and
|
||||
calling with rlen == 0 (this QA app was simply assuming the record
|
||||
was valid, at least to the point that the buffer could be correctly
|
||||
decoded).
|
||||
|
||||
Fix involves re-extracting the correct record length and calling
|
||||
__pmLogLoadInDom() with rlen != 0.
|
||||
---
|
||||
diff --git a/qa/src/scanmeta.c b/qa/src/scanmeta.c
|
||||
--- a/qa/src/scanmeta.c
|
||||
+++ b/qa/src/scanmeta.c
|
||||
@@ -143,7 +143,7 @@
|
||||
}
|
||||
|
||||
void
|
||||
-do_indom(__int32_t *buf, int type)
|
||||
+do_indom(__int32_t *buf, int type, int len)
|
||||
{
|
||||
int sts;
|
||||
static __pmTimestamp prior_stamp = { 0, 0 };
|
||||
@@ -156,7 +156,7 @@
|
||||
elt_t *tp;
|
||||
elt_t *dp = &dup;
|
||||
|
||||
- if ((sts = __pmLogLoadInDom(NULL, 0, type, &lid, &buf)) < 0) {
|
||||
+ if ((sts = __pmLogLoadInDom(NULL, len, type, &lid, &buf)) < 0) {
|
||||
fprintf(stderr, "__pmLoadLoadInDom: failed: %s\n", pmErrStr(sts));
|
||||
return;
|
||||
}
|
||||
@@ -689,7 +689,7 @@
|
||||
case TYPE_INDOM_V2:
|
||||
if (!iflag)
|
||||
break;
|
||||
- do_indom(buf, hdr.type);
|
||||
+ do_indom(buf, hdr.type, hdr.len);
|
||||
break;
|
||||
|
||||
case TYPE_LABEL:
|
||||
287
pcp-7.0.3-timezone-zoneinfo-validation.patch
Normal file
287
pcp-7.0.3-timezone-zoneinfo-validation.patch
Normal file
@ -0,0 +1,287 @@
|
||||
From f86c0f4cda Mon Sep 17 00:00:00 2001
|
||||
From: Nathan Scott <nathans@redhat.com>
|
||||
Subject: [PATCH] libpcp, libpcp_web: validate timezone and zoneinfo strings
|
||||
|
||||
The timezone and zoneinfo fields from archive labels and PDU_LOG_STATUS
|
||||
are used via pmNewZone() -> setenv("TZ", ...), causing glibc to resolve
|
||||
Olson timezone paths against /usr/share/zoneinfo/. A crafted value
|
||||
like "../../etc/passwd" would cause glibc to open arbitrary files.
|
||||
|
||||
Fix at two layers:
|
||||
- Front door: add check_tz() check in pmLogGroupLabel() alongside the
|
||||
existing check_hostname() check, rejecting unsafe timezone/zoneinfo
|
||||
before any data is written to disk
|
||||
- Consumption: add check_tz() check in pmNewZone() as defense-in-depth,
|
||||
protecting against malicious archives created by other means
|
||||
|
||||
The allowlist permits alphanumeric characters plus /_+-.:" which covers
|
||||
both Olson paths (America/New_York) and POSIX TZ strings (EST5EDT).
|
||||
Leading slashes and ".." path components are rejected.
|
||||
|
||||
Add qa/src/check_tz.c and qa/2107 exercising pmNewZone() with valid
|
||||
and malicious timezone strings.
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
---
|
||||
diff --git a/qa/2107 b/qa/2107
|
||||
--- a/qa/2107
|
||||
+++ b/qa/2107
|
||||
@@ -0,0 +1,60 @@
|
||||
+#!/bin/sh
|
||||
+# PCP QA Test No. 2107
|
||||
+# Verify pmNewZone rejects unsafe timezone strings containing
|
||||
+# path traversal or invalid characters
|
||||
+#
|
||||
+# Copyright (c) 2026 Red Hat. All Rights Reserved.
|
||||
+#
|
||||
+
|
||||
+seq=`basename $0`
|
||||
+echo "QA output created by $seq"
|
||||
+
|
||||
+# get standard environment, filters and checks
|
||||
+. ./common.product
|
||||
+. ./common.filter
|
||||
+. ./common.check
|
||||
+
|
||||
+_cleanup()
|
||||
+{
|
||||
+ cd $here
|
||||
+ $sudo rm -rf $tmp $tmp.*
|
||||
+}
|
||||
+
|
||||
+status=0 # success is the default!
|
||||
+trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
+
|
||||
+# real QA test starts here
|
||||
+
|
||||
+echo "=== valid Olson timezone ==="
|
||||
+src/check_tz "America/New_York"
|
||||
+
|
||||
+echo
|
||||
+echo "=== valid POSIX timezone ==="
|
||||
+src/check_tz "EST5EDT"
|
||||
+
|
||||
+echo
|
||||
+echo "=== valid simple timezone ==="
|
||||
+src/check_tz "UTC"
|
||||
+
|
||||
+echo
|
||||
+echo "=== path traversal should be rejected ==="
|
||||
+src/check_tz "../../etc/passwd"
|
||||
+
|
||||
+echo
|
||||
+echo "=== leading slash should be rejected ==="
|
||||
+src/check_tz "/etc/localtime"
|
||||
+
|
||||
+echo
|
||||
+echo "=== semicolon should be rejected ==="
|
||||
+src/check_tz "UTC;id"
|
||||
+
|
||||
+echo
|
||||
+echo "=== backtick should be rejected ==="
|
||||
+src/check_tz 'UTC`id`'
|
||||
+
|
||||
+echo
|
||||
+echo "=== empty string should be accepted ==="
|
||||
+src/check_tz ""
|
||||
+
|
||||
+# success, all done
|
||||
+exit
|
||||
diff --git a/qa/2107.out b/qa/2107.out
|
||||
--- a/qa/2107.out
|
||||
+++ b/qa/2107.out
|
||||
@@ -0,0 +1,24 @@
|
||||
+QA output created by 2107
|
||||
+=== valid Olson timezone ===
|
||||
+pmNewZone("America/New_York") -> 0 (accepted)
|
||||
+
|
||||
+=== valid POSIX timezone ===
|
||||
+pmNewZone("EST5EDT") -> 0 (accepted)
|
||||
+
|
||||
+=== valid simple timezone ===
|
||||
+pmNewZone("UTC") -> 0 (accepted)
|
||||
+
|
||||
+=== path traversal should be rejected ===
|
||||
+pmNewZone("../../etc/passwd") -> Invalid argument (rejected)
|
||||
+
|
||||
+=== leading slash should be rejected ===
|
||||
+pmNewZone("/etc/localtime") -> Invalid argument (rejected)
|
||||
+
|
||||
+=== semicolon should be rejected ===
|
||||
+pmNewZone("UTC;id") -> Invalid argument (rejected)
|
||||
+
|
||||
+=== backtick should be rejected ===
|
||||
+pmNewZone("UTC`id`") -> Invalid argument (rejected)
|
||||
+
|
||||
+=== empty string should be accepted ===
|
||||
+pmNewZone("") - skipped (empty string)
|
||||
diff --git a/qa/group b/qa/group
|
||||
--- a/qa/group
|
||||
+++ b/qa/group
|
||||
@@ -2307,5 +2307,6 @@
|
||||
2101 pmda.sockets local security
|
||||
2102 pmlogmv local security
|
||||
2103 pmieconf local security
|
||||
+2107 libpcp local security
|
||||
4751 libpcp threads valgrind local pcp helgrind
|
||||
9000 other local
|
||||
diff --git a/qa/src/check_tz.c b/qa/src/check_tz.c
|
||||
--- a/qa/src/check_tz.c
|
||||
+++ b/qa/src/check_tz.c
|
||||
@@ -0,0 +1,31 @@
|
||||
+/*
|
||||
+ * Verify pmNewZone accepts/rejects timezone strings correctly.
|
||||
+ */
|
||||
+
|
||||
+#include <pcp/pmapi.h>
|
||||
+
|
||||
+int
|
||||
+main(int argc, char **argv)
|
||||
+{
|
||||
+ int sts;
|
||||
+
|
||||
+ pmSetProgname(argv[0]);
|
||||
+
|
||||
+ if (argc != 2) {
|
||||
+ fprintf(stderr, "Usage: %s timezone\n", pmGetProgname());
|
||||
+ return 1;
|
||||
+ }
|
||||
+
|
||||
+ if (argv[1][0] == '\0') {
|
||||
+ printf("pmNewZone(\"\") - skipped (empty string)\n");
|
||||
+ return 0;
|
||||
+ }
|
||||
+
|
||||
+ sts = pmNewZone(argv[1]);
|
||||
+ if (sts >= 0)
|
||||
+ printf("pmNewZone(\"%s\") -> %d (accepted)\n", argv[1], sts);
|
||||
+ else
|
||||
+ printf("pmNewZone(\"%s\") -> %s (rejected)\n", argv[1], pmErrStr(sts));
|
||||
+
|
||||
+ return 0;
|
||||
+}
|
||||
diff --git a/qa/src/GNUlocaldefs b/qa/src/GNUlocaldefs
|
||||
--- a/qa/src/GNUlocaldefs
|
||||
+++ b/qa/src/GNUlocaldefs
|
||||
@@ -56,7 +56,7 @@
|
||||
throttle.c throttle_timeout.c y2038.c bigpmcdpmids.c pdu-gadget.c \
|
||||
strnfoo.c mmv_ondisk.c newcontext.c api_abi.c interp_bug3.c \
|
||||
pmsetmode.c scanindex.c localtime.c httpcache.c unregister.c \
|
||||
- check_cloexec.c
|
||||
+ check_cloexec.c check_tz.c
|
||||
|
||||
ifeq ($(shell test -f ../localconfig && echo 1), 1)
|
||||
include ../localconfig
|
||||
@@ -642,6 +642,11 @@
|
||||
rm -f $@
|
||||
$(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS)
|
||||
$(LINKER_MAKERULE)
|
||||
+
|
||||
+check_tz: check_tz.c
|
||||
+ rm -f $@
|
||||
+ $(CCF) $(CDEFS) -o $@ $@.c $(LDLIBS)
|
||||
+ $(LINKER_MAKERULE)
|
||||
|
||||
check_import: check_import.c
|
||||
rm -f $@
|
||||
diff --git a/src/libpcp/src/tz.c b/src/libpcp/src/tz.c
|
||||
--- a/src/libpcp/src/tz.c
|
||||
+++ b/src/libpcp/src/tz.c
|
||||
@@ -25,6 +25,7 @@
|
||||
* lock initialization in pmNewContext().
|
||||
*/
|
||||
|
||||
+#include <ctype.h>
|
||||
#include "pmapi.h"
|
||||
#include "libpcp.h"
|
||||
#include "sha256.h"
|
||||
@@ -570,6 +571,22 @@
|
||||
return 0;
|
||||
}
|
||||
|
||||
+static int
|
||||
+valid_tz(const char *tz)
|
||||
+{
|
||||
+ const char *p;
|
||||
+
|
||||
+ if (tz == NULL || tz[0] == '\0' || tz[0] == '/')
|
||||
+ return 0;
|
||||
+ for (p = tz; *p; p++) {
|
||||
+ if (!isalnum((unsigned char)*p) && strchr("/_+-.:,\"'", *p) == NULL)
|
||||
+ return 0;
|
||||
+ }
|
||||
+ if (strstr(tz, "..") != NULL)
|
||||
+ return 0;
|
||||
+ return 1;
|
||||
+}
|
||||
+
|
||||
int
|
||||
pmNewZone(const char *tz)
|
||||
{
|
||||
@@ -577,6 +594,13 @@
|
||||
int hack = 0;
|
||||
int sts;
|
||||
|
||||
+ if (!valid_tz(tz)) {
|
||||
+ if (pmDebugOptions.context)
|
||||
+ fprintf(stderr, "%s: rejecting unsafe timezone: %s\n",
|
||||
+ __FUNCTION__, tz ? tz : "(null)");
|
||||
+ return -EINVAL;
|
||||
+ }
|
||||
+
|
||||
PM_LOCK(__pmLock_extcall);
|
||||
|
||||
len = (int)strlen(tz);
|
||||
diff --git a/src/libpcp_web/src/loggroup.c b/src/libpcp_web/src/loggroup.c
|
||||
--- a/src/libpcp_web/src/loggroup.c
|
||||
+++ b/src/libpcp_web/src/loggroup.c
|
||||
@@ -598,6 +598,30 @@
|
||||
return 1;
|
||||
}
|
||||
|
||||
+/*
|
||||
+ * Check that timezone/zoneinfo strings (similarly can arrive from a
|
||||
+ * remote host), conform to simple validity checks; for timezone the
|
||||
+ * string will be placed into the environment (TZ), but for zoneinfo
|
||||
+ * file system path lookup will occur when accessing Olsen database.
|
||||
+ */
|
||||
+static int
|
||||
+check_tz(const char *tz)
|
||||
+{
|
||||
+ const char *p;
|
||||
+
|
||||
+ if (tz == NULL || tz[0] == '\0')
|
||||
+ return 1; /* empty/NULL timezone is valid (use system default) */
|
||||
+ if (tz[0] == '/')
|
||||
+ return 0;
|
||||
+ for (p = tz; *p; p++) {
|
||||
+ if (!isalnum((unsigned char)*p) && strchr("/_+-.:,", *p) == NULL)
|
||||
+ return 0;
|
||||
+ }
|
||||
+ if (strstr(tz, "..") != NULL)
|
||||
+ return 0;
|
||||
+ return 1;
|
||||
+}
|
||||
+
|
||||
int
|
||||
pmLogGroupLabel(pmLogGroupSettings *sp, const char *content, size_t length,
|
||||
dict *params, void *arg)
|
||||
@@ -633,6 +657,18 @@
|
||||
sts = -EINVAL;
|
||||
goto fail;
|
||||
}
|
||||
+ if (!check_tz(loglabel.timezone)) {
|
||||
+ pmNotifyErr(LOG_ERR, "Rejecting archive with unsafe timezone: %s",
|
||||
+ loglabel.timezone ? loglabel.timezone : "(null)");
|
||||
+ sts = -EINVAL;
|
||||
+ goto fail;
|
||||
+ }
|
||||
+ if (!check_tz(loglabel.zoneinfo)) {
|
||||
+ pmNotifyErr(LOG_ERR, "Rejecting archive with unsafe zoneinfo: %s",
|
||||
+ loglabel.zoneinfo ? loglabel.zoneinfo : "(null)");
|
||||
+ sts = -EINVAL;
|
||||
+ goto fail;
|
||||
+ }
|
||||
|
||||
start = (time_t)loglabel.start.sec;
|
||||
if (localtime_r(&start, &tm) == NULL ||
|
||||
60
pcp-RHEL-132402.patch
Normal file
60
pcp-RHEL-132402.patch
Normal file
@ -0,0 +1,60 @@
|
||||
commit 082ff6beb14420c04af74f37d2ae8c1628182ae2
|
||||
Author: William Cohen <wcohen@redhat.com>
|
||||
Date: Tue Feb 10 02:19:21 2026 +0000
|
||||
|
||||
selinux: AVC denial fix for rocestat pmda
|
||||
|
||||
Resolves: RHEL-132402
|
||||
|
||||
diff --git a/src/selinux/pcp.te b/src/selinux/pcp.te
|
||||
index 59cf1fb630..54f4e96877 100644
|
||||
--- a/src/selinux/pcp.te
|
||||
+++ b/src/selinux/pcp.te
|
||||
@@ -1036,6 +1036,16 @@ allow pcp_pmproxy_t pcp_log_t:lnk_file read;
|
||||
allow pcp_pmcd_t fsadm_exec_t:file { execute execute_no_trans getattr open read };
|
||||
allow pcp_pmcd_t fixed_disk_device_t:blk_file { open read ioctl };
|
||||
|
||||
+#============= pmda-rocestat ==============
|
||||
+optional_policy(`
|
||||
+ require {
|
||||
+ type ifconfig_exec_t;
|
||||
+ }
|
||||
+ # type=AVC msg=audit(N): avc: denied { execute_no_trans } for pid=PID comm="python3" path="/usr/sbin/ethtool" dev=DEV ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=system_u:object_r:ifconfig_exec_t:s0 tclass=file permissive=0
|
||||
+ # RHEL-132402
|
||||
+ allow pcp_pmcd_t ifconfig_exec_t:file { execute execute_no_trans };
|
||||
+')
|
||||
+
|
||||
#============= pmda-nvidia ==============
|
||||
# type=AVC msg=audit(N): avc: denied { execute } for pid=PID comm="pmdanvidia" path="/usr/lib64/libnvidia-ml.so" dev="dm-2" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
|
||||
# type=AVC msg=audit(N): avc: denied { read } for pid=PID comm="pmdanvidia" name="nvidia-cap2" dev="devtmpfs" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=unconfined_u:object_r:device_t:s0 tclass=chr_file permissive=0
|
||||
|
||||
commit e84ee24823548ce92c1e222d034e5600f4d3a10a
|
||||
Author: William Cohen <wcohen@redhat.com>
|
||||
Date: Tue Feb 10 04:00:26 2026 +0000
|
||||
|
||||
selinux: Update nvidia pmda policy
|
||||
|
||||
RHEL-133519
|
||||
|
||||
diff --git a/src/selinux/pcp.te b/src/selinux/pcp.te
|
||||
index 54f4e96877..69ee2b2957 100644
|
||||
--- a/src/selinux/pcp.te
|
||||
+++ b/src/selinux/pcp.te
|
||||
@@ -1051,7 +1051,7 @@ optional_policy(`
|
||||
# type=AVC msg=audit(N): avc: denied { read } for pid=PID comm="pmdanvidia" name="nvidia-cap2" dev="devtmpfs" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=unconfined_u:object_r:device_t:s0 tclass=chr_file permissive=0
|
||||
#RHEL-83594
|
||||
allow pcp_pmcd_t default_t:file { execute };
|
||||
-allow pcp_pmcd_t device_t:chr_file { create open read setattr write };
|
||||
+allow pcp_pmcd_t device_t:chr_file { create ioctl open read setattr write };
|
||||
allow pcp_pmcd_t device_t:dir { add_name remove_name write };
|
||||
allow pcp_pmcd_t device_t:lnk_file { create unlink };
|
||||
allow pcp_pmcd_t self:capability mknod;
|
||||
@@ -1059,7 +1059,7 @@ allow pcp_pmcd_t dri_device_t:chr_file { ioctl open read write };
|
||||
allow pcp_pmcd_t device_t:dir write;
|
||||
allow pcp_pmcd_t device_t:dir { create setattr };
|
||||
allow pcp_pmcd_t sysctl_vm_t:file read;
|
||||
-allow pcp_pmcd_t xserver_misc_device_t:chr_file { ioctl open read write };
|
||||
+allow pcp_pmcd_t xserver_misc_device_t:chr_file { ioctl map open read write };
|
||||
|
||||
# type=AVC msg=audit(N): avc: denied { sys_rawio } for pid=PID comm="pmdaX" name="/" dev="tracefs" ino=INO scontext=system_u:system_r:pcp_pmcd_t:s0 tcontext=system_u:object_r:pcp_pmcd_t:s0 tclass=capability permissive=0
|
||||
allow pcp_pmcd_t self:capability sys_rawio;
|
||||
99
pcp2openmetrics-archive.patch
Normal file
99
pcp2openmetrics-archive.patch
Normal file
@ -0,0 +1,99 @@
|
||||
From 634789506f10c0156b13bfa6a3f07705ed40b5f1 Mon Sep 17 00:00:00 2001
|
||||
From: lmchilton <lauren.chilton26@gmail.com>
|
||||
Date: Tue, 13 Jan 2026 13:32:46 -0500
|
||||
Subject: [PATCH] pcp2openmetrics: resolves RHEL-138467
|
||||
|
||||
multi-instance metrics failing when queried with
|
||||
singular metrics from archive. Turning interpolation
|
||||
on resolves this issue. Additional code added to fix
|
||||
missing headers when 2+ metrics are queried together.
|
||||
---
|
||||
qa/1131 | 3 +++
|
||||
qa/1131.out | 12 ++++++++++++
|
||||
src/pcp2openmetrics/pcp2openmetrics.py | 8 ++++----
|
||||
3 files changed, 19 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/qa/1131 b/qa/1131
|
||||
index 118375d425..026a6ebd97 100755
|
||||
--- a/qa/1131
|
||||
+++ b/qa/1131
|
||||
@@ -31,6 +31,7 @@ signal=$PCP_BINADM_DIR/pmsignal
|
||||
trap "_cleanup; exit \$status" 0 1 2 3 15
|
||||
|
||||
A="$here/archives/rep"
|
||||
+A1="$here/archives/20180606"
|
||||
hostname=`hostname`
|
||||
machineid=`_machine_id`
|
||||
domainid=`_domain_name`
|
||||
@@ -98,6 +99,8 @@ pcp2openmetrics -s1 -z hinv.ncpu | _filter_pcp2openmetrics
|
||||
echo "---"
|
||||
pcp2openmetrics -s2 -x hinv.ncpu | _filter_pcp2openmetrics
|
||||
echo "---"
|
||||
+pcp2openmetrics -s2 -a $A1 hinv.ncpu disk.partitions.write | _archive_filter
|
||||
+echo "---"
|
||||
pcp2opentelemetry -s1 -H -z hinv.ncpu | _filter_pcp2opentelemetry
|
||||
echo "---"
|
||||
|
||||
diff --git a/qa/1131.out b/qa/1131.out
|
||||
index a8fde64d5b..a6e98b0139 100644
|
||||
--- a/qa/1131.out
|
||||
+++ b/qa/1131.out
|
||||
@@ -2279,6 +2279,18 @@ hinv_ncpu{domainname="DOMAINID",groupid="GROUPID",hostname="HOST",machineid="MAC
|
||||
hinv_ncpu{domainname="DOMAINID",groupid="GROUPID",hostname="HOST",machineid="MACHINEID",userid="USERID",agent="AGENT"} NCPU
|
||||
hinv_ncpu{domainname="DOMAINID",groupid="GROUPID",hostname="HOST",machineid="MACHINEID",userid="USERID",agent="AGENT"} NCPU
|
||||
---
|
||||
+# PCP5 hinv_ncpu 60.0.32 u32 PM_INDOM_NULL discrete
|
||||
+# TYPE hinv_ncpu gauge
|
||||
+# HELP hinv_ncpu number of CPUs in the system
|
||||
+hinv_ncpu{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",agent="linux"} 1 1528207821.599573
|
||||
+# PCP5 disk_partitions_write 60.10.1 u32 60.10 counter count
|
||||
+# TYPE disk_partitions_write counter
|
||||
+# HELP disk_partitions_write write operations metric for storage partitions
|
||||
+disk_partitions_write_total{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",instname="sda1",instid="1",agent="linux",device_type="block",indom_name="per partition"} 88156 1528207821.599573
|
||||
+disk_partitions_write_total{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",instname="sda2",instid="2",agent="linux",device_type="block",indom_name="per partition"} 0 1528207821.599573
|
||||
+disk_partitions_write_total{domainname="localdomain",groupid="999",hostname="vm01",machineid="3a6b3d7f3b7559c538a5934a24866658",userid="999",instname="sda5",instid="3",agent="linux",device_type="block",indom_name="per partition"} 2293 1528207821.599573
|
||||
+# EOF
|
||||
+---
|
||||
{
|
||||
"resourceMetrics": [
|
||||
{
|
||||
diff --git a/src/pcp2openmetrics/pcp2openmetrics.py b/src/pcp2openmetrics/pcp2openmetrics.py
|
||||
index a228aaf340..bc935fab03 100755
|
||||
--- a/src/pcp2openmetrics/pcp2openmetrics.py
|
||||
+++ b/src/pcp2openmetrics/pcp2openmetrics.py
|
||||
@@ -104,7 +104,7 @@ class PCP2OPENMETRICS(object):
|
||||
self.precision = 3 # .3f
|
||||
self.precision_force = None
|
||||
self.timefmt = TIMEFMT
|
||||
- self.interpol = 0
|
||||
+ self.interpol = 1
|
||||
self.count_scale = None
|
||||
self.count_scale_force = None
|
||||
self.space_scale = None
|
||||
@@ -121,7 +121,7 @@ class PCP2OPENMETRICS(object):
|
||||
self.http_pass = None
|
||||
self.http_timeout = TIMEOUT
|
||||
self.no_comment = False
|
||||
- self.header_flag = True
|
||||
+ self.headers = []
|
||||
|
||||
# Internal
|
||||
self.runtime = -1
|
||||
@@ -476,12 +476,12 @@ class PCP2OPENMETRICS(object):
|
||||
help_dict = {}
|
||||
help_dict[metric] = context.pmLookupText(pmid[0])
|
||||
|
||||
- if self.header_flag is True:
|
||||
+ if metric not in self.headers:
|
||||
if self.no_comment is False:
|
||||
body += '# PCP5 %s %s %s %s %s %s\n' % (openmetrics_name(metric), pmIDStr, get_type_string(desc), pmIndomStr, semantics, units)
|
||||
body += '# TYPE %s %s\n' % (openmetrics_name(metric), openmetrics_type(desc))
|
||||
body += '# HELP %s %s\n' % (openmetrics_name(metric), help_dict[metric])
|
||||
- self.header_flag = False
|
||||
+ self.headers.append(metric)
|
||||
|
||||
for inst, name, value in results[metric]:
|
||||
if isinstance(value, float):
|
||||
--
|
||||
2.51.0
|
||||
|
||||
3
sources
3
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user