Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F176251771
D59372.id188478.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
100 KB
Referenced Files
None
Subscribers
None
D59372.id188478.diff
View Options
diff --git a/tests/sys/pmc/Makefile b/tests/sys/pmc/Makefile
--- a/tests/sys/pmc/Makefile
+++ b/tests/sys/pmc/Makefile
@@ -4,6 +4,7 @@
ATF_TESTS_C+= pmc_credexec_test
ATF_TESTS_C+= pmc_detach_test
ATF_TESTS_C+= pmc_exec_test
+ATF_TESTS_C+= pmc_fork_test
ATF_TESTS_C+= pmc_group_gate_test
ATF_TESTS_C+= pmc_group_test
ATF_TESTS_C+= pmc_groupread_test
@@ -18,6 +19,7 @@
LIBADD+= pmc
LIBADD.pmc_detach_test+= pthread
+LIBADD.pmc_fork_test+= pthread
CFLAGS.pmc_exterr_test.c+= -I${SRCTOP}/tests
diff --git a/tests/sys/pmc/pmc_fork_test.c b/tests/sys/pmc/pmc_fork_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/pmc_fork_test.c
@@ -0,0 +1,3096 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ */
+
+/*
+ * Fork inheritance for grouped PMCs (spec §3.9).
+ *
+ * PMC_F_DESCENDANTS on a group leader makes the whole group follow
+ * every fork of every attached target. Children's work adds to the
+ * same member totals. The measurement covers the whole tree.
+ * A run that forks children which do most of the work must count
+ * far more than the parent alone.
+ *
+ * The controls matter more than the headline check.
+ * An unattached child does the same work in every case.
+ * So a run WITHOUT the flag shows what "parent only" costs.
+ * The test compares the two runs to each other, not to a fixed
+ * number that could vary by machine.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/sysctl.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <pmc.h>
+#include <pmclog.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define TEST_EVENT "instructions"
+#define NCHILDREN 4
+#define MAX_ROW_OCCUPIERS 64
+/* Each child does this much work. The parent only forks and waits. */
+#define CHILD_ITERS (200ULL * 1000 * 1000)
+#define CHILD_MIN_DELTA (10ULL * 1000 * 1000)
+#define TEST_FORK_LOG_MARKER 0x47464d49U
+#define FORK_LOG_NGROUPS 2
+#define FORK_LOG_NMEMBERS 2
+/*
+ * The no-install build uses the host's libpmc headers.
+ * Keep this record number local.
+ * Install that userland, or link it directly, before you change this.
+ */
+#define TEST_PMCLOG_TYPE_GROUP_INHERIT_MISS 21
+
+#define CHILD_READY 'r'
+#define CHILD_CONTROL 'c'
+#define CHILD_WORK 'w'
+#define CHILD_EXIT 'x'
+#define TARGET_READY 'R'
+#define TARGET_FORK 'F'
+#define TARGET_FORKING 'f'
+#define TARGET_EXIT 'P'
+
+#define TEST_SYSCTL_PREFIX "kern.hwpmc.test."
+#define TEST_FAIL_GROUP_TARGET_ALLOC \
+ TEST_SYSCTL_PREFIX "fail_group_target_alloc_after"
+#define TEST_FAIL_TARGET_LINK_ALLOC \
+ TEST_SYSCTL_PREFIX "fail_target_link_alloc_after"
+#define TEST_FAIL_ATTACH_AUTHORIZATION \
+ TEST_SYSCTL_PREFIX "fail_attach_authorization_after"
+#define TEST_HOLD_GROUP_RESIDENT \
+ TEST_SYSCTL_PREFIX "hold_group_resident"
+#define TEST_HOLD_GROUP_RESIDENT_ACK \
+ TEST_SYSCTL_PREFIX "hold_group_resident_ack"
+#define TEST_HOLD_GROUP_EVICTED \
+ TEST_SYSCTL_PREFIX "hold_group_evicted"
+#define TEST_HOLD_GROUP_EVICTED_ACK \
+ TEST_SYSCTL_PREFIX "hold_group_evicted_ack"
+#define TEST_PAUSE_DESCENDANTS_ATTACH \
+ TEST_SYSCTL_PREFIX "pause_descendants_attach"
+#define TEST_PAUSE_DESCENDANTS_ATTACH_ACK \
+ TEST_SYSCTL_PREFIX "pause_descendants_attach_ack"
+#define TEST_PAUSE_DESCENDANTS_TARGET_COUNT \
+ TEST_SYSCTL_PREFIX "pause_descendants_target_count"
+#define TEST_DESCENDANTS_EXIT_PID \
+ TEST_SYSCTL_PREFIX "descendants_exit_pid"
+#define TEST_DESCENDANTS_EXIT_ACK \
+ TEST_SYSCTL_PREFIX "descendants_exit_ack"
+#define TEST_LIVE_PMC_TARGETS \
+ TEST_SYSCTL_PREFIX "live_pmc_targets"
+#define TEST_LIVE_GROUP_TARGETS \
+ TEST_SYSCTL_PREFIX "live_group_targets"
+#define TEST_LIVE_TARGET_PROCESSES \
+ TEST_SYSCTL_PREFIX "live_target_processes"
+#define TEST_LIVE_RESIDUAL_ENTRIES \
+ TEST_SYSCTL_PREFIX "live_residual_entries"
+#define TEST_LIVE_ROTATION_REFS \
+ TEST_SYSCTL_PREFIX "live_rotation_refs"
+#define TEST_LIVE_RUN_REFS \
+ TEST_SYSCTL_PREFIX "live_run_refs"
+
+struct group {
+ uint32_t g_id;
+ pmc_id_t g_ids[2];
+ u_int g_n;
+ bool g_started;
+};
+
+struct row_occupancy {
+ pmc_id_t ro_ids[MAX_ROW_OCCUPIERS];
+ u_int ro_n;
+};
+
+struct child_read_result {
+ int cr_control_rc;
+ int cr_control_errno;
+ int cr_leader_rc;
+ int cr_leader_errno;
+ int cr_sibling_rc;
+ int cr_sibling_errno;
+ pmc_value_t cr_control_value;
+ pmc_value_t cr_leader_value;
+ pmc_value_t cr_sibling_value;
+};
+
+struct fork_log_child_result {
+ int flcr_rc[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+ int flcr_errno[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+ pmc_value_t flcr_value[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+};
+
+struct fork_log_counts {
+ u_int flc_procfork;
+ u_int flc_attach[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+ u_int flc_detach[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+ bool flc_marker_seen;
+};
+
+enum inherited_fork_result_stage {
+ INHERITED_FORK_RESULT_CHILD = 0,
+ INHERITED_FORK_RESULT_FAILPOINT,
+ INHERITED_FORK_RESULT_FORK
+};
+
+struct inherited_fork_result {
+ struct fork_log_child_result ifr_reads;
+ pid_t ifr_pid;
+ int ifr_errno;
+ enum inherited_fork_result_stage ifr_stage;
+};
+
+struct inherited_fork_log_counts {
+ u_int iflc_procfork;
+ u_int iflc_parent_attach[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+ u_int iflc_child_attach[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+ u_int iflc_child_detach[FORK_LOG_NGROUPS][FORK_LOG_NMEMBERS];
+ u_int iflc_child_miss[FORK_LOG_NGROUPS];
+ u_int iflc_unmatched_child_miss;
+ bool iflc_marker_seen;
+};
+
+struct test_pmclog_group_inherit_miss {
+ PMCLOG_ENTRY_HEADER
+ uint32_t pl_pmcid;
+ uint32_t pl_pid;
+} __packed;
+
+struct live_target_counts {
+ uint64_t ltc_pmc_targets;
+ uint64_t ltc_group_targets;
+ uint64_t ltc_target_processes;
+ uint64_t ltc_residual_entries;
+ uint64_t ltc_rotation_refs;
+ uint64_t ltc_run_refs;
+};
+
+struct attach_thread_result {
+ pmc_id_t atr_leader;
+ pid_t atr_target;
+ int atr_rc;
+ int atr_errno;
+};
+
+static ssize_t read_full(int fd, void *buffer, size_t length);
+
+static bool
+is_amd(void)
+{
+ char vendor[64];
+ size_t len;
+
+ len = sizeof(vendor);
+ if (sysctlbyname("kern.hwpmc.cpuid", vendor, &len, NULL, 0) != 0)
+ return (false);
+ return (strstr(vendor, "AuthenticAMD") != NULL ||
+ strstr(vendor, "HygonGenuine") != NULL);
+}
+
+static void
+require_hwpmc(void)
+{
+
+ if (pmc_init() != 0)
+ atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+ if (!is_amd())
+ atf_tc_skip("PMC grouping is supported only on AMD CPUs");
+}
+
+static int
+sysctl_read_u32(const char *name, u_int *value)
+{
+ size_t length;
+
+ length = sizeof(*value);
+ return (sysctlbyname(name, value, &length, NULL, 0));
+}
+
+static int
+sysctl_write_u32(const char *name, u_int value)
+{
+
+ return (sysctlbyname(name, NULL, NULL, &value, sizeof(value)));
+}
+
+static int
+sysctl_read_u64(const char *name, uint64_t *value)
+{
+ size_t length;
+
+ length = sizeof(*value);
+ return (sysctlbyname(name, value, &length, NULL, 0));
+}
+
+static void
+require_hwpmc_test_support(void)
+{
+ uint64_t value;
+ int error;
+
+ errno = 0;
+ error = sysctl_read_u64(TEST_LIVE_PMC_TARGETS, &value);
+ if (error == 0)
+ return;
+ if (errno == ENOENT)
+ atf_tc_skip("requires an HWPMCDEBUG module with test support");
+ atf_tc_fail("reading %s failed: errno %d (%s)",
+ TEST_LIVE_PMC_TARGETS, errno, strerror(errno));
+}
+
+static void
+read_live_target_counts(struct live_target_counts *counts)
+{
+
+ ATF_REQUIRE_MSG(sysctl_read_u64(TEST_LIVE_PMC_TARGETS,
+ &counts->ltc_pmc_targets) == 0,
+ "reading %s failed: %s", TEST_LIVE_PMC_TARGETS, strerror(errno));
+ ATF_REQUIRE_MSG(sysctl_read_u64(TEST_LIVE_GROUP_TARGETS,
+ &counts->ltc_group_targets) == 0,
+ "reading %s failed: %s", TEST_LIVE_GROUP_TARGETS, strerror(errno));
+ ATF_REQUIRE_MSG(sysctl_read_u64(TEST_LIVE_TARGET_PROCESSES,
+ &counts->ltc_target_processes) == 0,
+ "reading %s failed: %s", TEST_LIVE_TARGET_PROCESSES,
+ strerror(errno));
+ ATF_REQUIRE_MSG(sysctl_read_u64(TEST_LIVE_RESIDUAL_ENTRIES,
+ &counts->ltc_residual_entries) == 0,
+ "reading %s failed: %s", TEST_LIVE_RESIDUAL_ENTRIES,
+ strerror(errno));
+ ATF_REQUIRE_MSG(sysctl_read_u64(TEST_LIVE_ROTATION_REFS,
+ &counts->ltc_rotation_refs) == 0,
+ "reading %s failed: %s", TEST_LIVE_ROTATION_REFS,
+ strerror(errno));
+ ATF_REQUIRE_MSG(sysctl_read_u64(TEST_LIVE_RUN_REFS,
+ &counts->ltc_run_refs) == 0,
+ "reading %s failed: %s", TEST_LIVE_RUN_REFS, strerror(errno));
+}
+
+static void
+check_live_counts_equal(const char *description,
+ const struct live_target_counts *actual,
+ const struct live_target_counts *expected)
+{
+
+ ATF_CHECK_MSG(actual->ltc_pmc_targets == expected->ltc_pmc_targets &&
+ actual->ltc_group_targets == expected->ltc_group_targets &&
+ actual->ltc_target_processes == expected->ltc_target_processes &&
+ actual->ltc_residual_entries == expected->ltc_residual_entries &&
+ actual->ltc_rotation_refs == expected->ltc_rotation_refs &&
+ actual->ltc_run_refs == expected->ltc_run_refs,
+ "%s: pmc_target %ju/%ju group_target %ju/%ju process %ju/%ju "
+ "residual %ju/%ju rotation_ref %ju/%ju run_ref %ju/%ju",
+ description,
+ (uintmax_t)actual->ltc_pmc_targets,
+ (uintmax_t)expected->ltc_pmc_targets,
+ (uintmax_t)actual->ltc_group_targets,
+ (uintmax_t)expected->ltc_group_targets,
+ (uintmax_t)actual->ltc_target_processes,
+ (uintmax_t)expected->ltc_target_processes,
+ (uintmax_t)actual->ltc_residual_entries,
+ (uintmax_t)expected->ltc_residual_entries,
+ (uintmax_t)actual->ltc_rotation_refs,
+ (uintmax_t)expected->ltc_rotation_refs,
+ (uintmax_t)actual->ltc_run_refs,
+ (uintmax_t)expected->ltc_run_refs);
+}
+
+static int
+open_named_logfile(char *path, size_t path_size)
+{
+ int length;
+
+ length = snprintf(path, path_size, "/tmp/pmc-fork-test.XXXXXX");
+ if (length < 0 || (size_t)length >= path_size) {
+ errno = ENAMETOOLONG;
+ return (-1);
+ }
+ return (mkstemp(path));
+}
+
+static bool
+find_fork_log_member(const struct group groups[FORK_LOG_NGROUPS],
+ pmc_id_t pmcid, u_int *group_index, u_int *member_index)
+{
+ u_int group, member;
+
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ for (member = 0; member < groups[group].g_n; member++) {
+ if (groups[group].g_ids[member] != pmcid)
+ continue;
+ *group_index = group;
+ *member_index = member;
+ return (true);
+ }
+ }
+ return (false);
+}
+
+static int
+scan_fork_inherit_log(const char *path, pid_t parent_pid, pid_t child_pid,
+ const struct group groups[FORK_LOG_NGROUPS],
+ struct fork_log_counts *counts)
+{
+ struct pmclog_ev event;
+ void *parser;
+ pmc_id_t pmcid;
+ pid_t pid;
+ u_int group, member;
+ int fd, saved_errno;
+
+ memset(counts, 0, sizeof(*counts));
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return (-1);
+ parser = pmclog_open(fd);
+ if (parser == NULL) {
+ saved_errno = errno;
+ (void)close(fd);
+ errno = saved_errno;
+ return (-1);
+ }
+
+ memset(&event, 0, sizeof(event));
+ while (pmclog_read(parser, &event) == 0) {
+ if (event.pl_type == PMCLOG_TYPE_PROCFORK &&
+ event.pl_u.pl_f.pl_oldpid == parent_pid &&
+ event.pl_u.pl_f.pl_newpid == child_pid)
+ counts->flc_procfork++;
+ if (event.pl_type == PMCLOG_TYPE_PMCATTACH) {
+ pmcid = event.pl_u.pl_t.pl_pmcid;
+ pid = event.pl_u.pl_t.pl_pid;
+ if (pid == child_pid &&
+ find_fork_log_member(groups, pmcid, &group, &member))
+ counts->flc_attach[group][member]++;
+ }
+ if (event.pl_type == PMCLOG_TYPE_PMCDETACH) {
+ pmcid = event.pl_u.pl_d.pl_pmcid;
+ pid = event.pl_u.pl_d.pl_pid;
+ if (pid == child_pid &&
+ find_fork_log_member(groups, pmcid, &group, &member))
+ counts->flc_detach[group][member]++;
+ }
+ if (event.pl_type == PMCLOG_TYPE_USERDATA &&
+ event.pl_u.pl_u.pl_userdata == TEST_FORK_LOG_MARKER) {
+ counts->flc_marker_seen = true;
+ break;
+ }
+ }
+ if (event.pl_state == PMCLOG_ERROR) {
+ saved_errno = EPROTO;
+ pmclog_close(parser);
+ (void)close(fd);
+ errno = saved_errno;
+ return (-1);
+ }
+ pmclog_close(parser);
+ if (close(fd) != 0)
+ return (-1);
+ return (0);
+}
+
+/*
+ * Read the wire format directly.
+ * The host's libpmc may not know this new append-only record.
+ */
+static int
+scan_inherited_fork_miss_log(const char *path, pid_t inherited_parent_pid,
+ pid_t child_pid, const struct group groups[FORK_LOG_NGROUPS],
+ struct inherited_fork_log_counts *counts)
+{
+ const struct pmclog_header *header;
+ const struct pmclog_pmcattach *attach;
+ const struct pmclog_pmcdetach *detach;
+ const struct pmclog_procfork *procfork;
+ const struct pmclog_userdata *userdata;
+ const struct test_pmclog_group_inherit_miss *miss;
+ struct stat sb;
+ uint8_t *data;
+ size_t length, offset, record_length;
+ ssize_t nread;
+ pmc_id_t pmcid;
+ pid_t pid;
+ u_int group, member, type;
+ int fd, saved_errno;
+
+ memset(counts, 0, sizeof(*counts));
+ data = NULL;
+ fd = open(path, O_RDONLY);
+ if (fd < 0)
+ return (-1);
+ if (fstat(fd, &sb) != 0)
+ goto fail;
+ if (sb.st_size <= 0 || (uintmax_t)sb.st_size > SIZE_MAX) {
+ errno = EPROTO;
+ goto fail;
+ }
+ length = (size_t)sb.st_size;
+ data = malloc(length);
+ if (data == NULL)
+ goto fail;
+ nread = read_full(fd, data, length);
+ if (nread != (ssize_t)length) {
+ if (nread >= 0)
+ errno = EPROTO;
+ goto fail;
+ }
+ if (close(fd) != 0) {
+ fd = -1;
+ goto fail;
+ }
+ fd = -1;
+
+ for (offset = 0; offset < length; offset += record_length) {
+ if (length - offset < sizeof(*header))
+ goto protocol_error;
+ header = (const void *)(data + offset);
+ if (!PMCLOG_HEADER_CHECK_MAGIC(header->pl_header))
+ goto protocol_error;
+ record_length = PMCLOG_HEADER_TO_LENGTH(header->pl_header);
+ if (record_length < sizeof(*header) ||
+ record_length % sizeof(uint32_t) != 0 ||
+ record_length > length - offset)
+ goto protocol_error;
+ type = PMCLOG_HEADER_TO_TYPE(header->pl_header);
+
+ switch (type) {
+ case PMCLOG_TYPE_PROCFORK:
+ if (record_length < sizeof(*procfork))
+ goto protocol_error;
+ procfork = (const void *)(data + offset);
+ if ((pid_t)procfork->pl_oldpid == inherited_parent_pid &&
+ (pid_t)procfork->pl_newpid == child_pid)
+ counts->iflc_procfork++;
+ break;
+ case PMCLOG_TYPE_PMCATTACH:
+ if (record_length <
+ offsetof(struct pmclog_pmcattach, pl_pathname))
+ goto protocol_error;
+ attach = (const void *)(data + offset);
+ pmcid = attach->pl_pmcid;
+ pid = (pid_t)attach->pl_pid;
+ if (!find_fork_log_member(groups, pmcid, &group, &member))
+ break;
+ if (pid == inherited_parent_pid)
+ counts->iflc_parent_attach[group][member]++;
+ else if (pid == child_pid)
+ counts->iflc_child_attach[group][member]++;
+ break;
+ case PMCLOG_TYPE_PMCDETACH:
+ if (record_length < sizeof(*detach))
+ goto protocol_error;
+ detach = (const void *)(data + offset);
+ pmcid = detach->pl_pmcid;
+ pid = (pid_t)detach->pl_pid;
+ if (pid == child_pid &&
+ find_fork_log_member(groups, pmcid, &group, &member))
+ counts->iflc_child_detach[group][member]++;
+ break;
+ case PMCLOG_TYPE_USERDATA:
+ if (record_length < sizeof(*userdata))
+ goto protocol_error;
+ userdata = (const void *)(data + offset);
+ if (userdata->pl_userdata == TEST_FORK_LOG_MARKER)
+ counts->iflc_marker_seen = true;
+ break;
+ case TEST_PMCLOG_TYPE_GROUP_INHERIT_MISS:
+ if (record_length < sizeof(*miss))
+ goto protocol_error;
+ miss = (const void *)(data + offset);
+ if ((pid_t)miss->pl_pid != child_pid)
+ break;
+ pmcid = miss->pl_pmcid;
+ if (find_fork_log_member(groups, pmcid, &group, &member) &&
+ member == 0)
+ counts->iflc_child_miss[group]++;
+ else
+ counts->iflc_unmatched_child_miss++;
+ break;
+ default:
+ break;
+ }
+ if (counts->iflc_marker_seen)
+ break;
+ }
+
+ free(data);
+ return (0);
+
+protocol_error:
+ errno = EPROTO;
+fail:
+ saved_errno = errno;
+ if (fd >= 0)
+ (void)close(fd);
+ free(data);
+ errno = saved_errno;
+ return (-1);
+}
+
+static void *
+attach_group_thread(void *arg)
+{
+ struct attach_thread_result *result;
+
+ result = arg;
+ errno = 0;
+ result->atr_rc = pmc_attach(result->atr_leader, result->atr_target);
+ result->atr_errno = errno;
+ return (NULL);
+}
+
+static void
+require_test_sysctl_write(const char *name, u_int value)
+{
+
+ ATF_REQUIRE_MSG(sysctl_write_u32(name, value) == 0,
+ "writing %s=%u failed: errno %d (%s)", name, value, errno,
+ strerror(errno));
+}
+
+static void
+wait_test_sysctl_ack(const char *name, u_int expected)
+{
+ u_int value;
+ int i;
+
+ for (i = 0; i < 5000; i++) {
+ ATF_REQUIRE_MSG(sysctl_read_u32(name, &value) == 0,
+ "reading %s failed: errno %d (%s)", name, errno,
+ strerror(errno));
+ if (value == expected)
+ return;
+ usleep(1000);
+ }
+ atf_tc_fail("%s did not acknowledge value %u", name, expected);
+}
+
+static void
+reset_fork_test_hooks(void)
+{
+
+ (void)sysctl_write_u32(TEST_FAIL_GROUP_TARGET_ALLOC, UINT_MAX);
+ (void)sysctl_write_u32(TEST_FAIL_TARGET_LINK_ALLOC, UINT_MAX);
+ (void)sysctl_write_u32(TEST_FAIL_ATTACH_AUTHORIZATION, UINT_MAX);
+ (void)sysctl_write_u32(TEST_HOLD_GROUP_RESIDENT, 0);
+ (void)sysctl_write_u32(TEST_HOLD_GROUP_EVICTED, 0);
+ (void)sysctl_write_u32(TEST_PAUSE_DESCENDANTS_ATTACH, 0);
+ (void)sysctl_write_u32(TEST_DESCENDANTS_EXIT_PID, 0);
+}
+
+static void
+group_init(struct group *g)
+{
+ u_int i;
+
+ memset(g, 0, sizeof(*g));
+ for (i = 0; i < nitems(g->g_ids); i++)
+ g->g_ids[i] = PMC_ID_INVALID;
+}
+
+static void
+group_teardown(struct group *g)
+{
+ u_int i;
+
+ if (g->g_started)
+ (void)pmc_stop(g->g_ids[0]);
+ for (i = 0; i < g->g_n; i++) {
+ if (g->g_ids[i] != PMC_ID_INVALID)
+ (void)pmc_release(g->g_ids[i]);
+ }
+ group_init(g);
+}
+
+/* A two-member counting group. The leader may carry PMC_F_DESCENDANTS. */
+static int
+group_build(struct group *g, bool descendants)
+{
+ uint32_t flags;
+ u_int i;
+
+ group_init(g);
+ g->g_n = 2;
+ for (i = 0; i < g->g_n; i++) {
+ flags = (i == 0 && descendants) ? PMC_F_DESCENDANTS : 0;
+ if (pmc_allocate_group(TEST_EVENT, PMC_MODE_TC, flags,
+ PMC_CPU_ANY, &g->g_ids[i], 0) != 0)
+ return (-1);
+ }
+ if (pmc_group_create(&g->g_id) != 0)
+ return (-1);
+ for (i = 0; i < g->g_n; i++) {
+ if (pmc_group_add(g->g_id, g->g_ids[i], i == 0) != 0)
+ return (-1);
+ }
+ return (pmc_group_commit(g->g_id));
+}
+
+static void
+group_build_with_mux_required(struct group *g, bool descendants, bool mux)
+{
+ uint32_t flags;
+ u_int i;
+
+ group_init(g);
+ g->g_n = 2;
+ for (i = 0; i < g->g_n; i++) {
+ flags = (i == 0 && descendants) ? PMC_F_DESCENDANTS : 0;
+ if (i == 0 && mux)
+ flags |= PMC_F_GROUP_MUX;
+ ATF_REQUIRE_MSG(pmc_allocate_group(TEST_EVENT, PMC_MODE_TC, flags,
+ PMC_CPU_ANY, &g->g_ids[i], 0) == 0,
+ "member %u allocation failed: %s", i, strerror(errno));
+ }
+ ATF_REQUIRE_MSG(pmc_group_create(&g->g_id) == 0,
+ "group creation failed: %s", strerror(errno));
+ for (i = 0; i < g->g_n; i++) {
+ ATF_REQUIRE_MSG(
+ pmc_group_add(g->g_id, g->g_ids[i], i == 0) == 0,
+ "group add for member %u failed: %s", i, strerror(errno));
+ }
+ ATF_REQUIRE_MSG(pmc_group_commit(g->g_id) == 0,
+ "group commit failed: %s", strerror(errno));
+}
+
+static void
+group_build_required(struct group *g, bool descendants)
+{
+
+ group_build_with_mux_required(g, descendants, false);
+}
+
+static void
+group_build_one_required(struct group *g, bool mux)
+{
+ uint32_t flags;
+
+ group_init(g);
+ g->g_n = 1;
+ flags = PMC_F_DESCENDANTS;
+ if (mux)
+ flags |= PMC_F_GROUP_MUX;
+ ATF_REQUIRE_MSG(pmc_allocate_group(TEST_EVENT, PMC_MODE_TC, flags,
+ PMC_CPU_ANY, &g->g_ids[0], 0) == 0,
+ "member allocation failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_group_create(&g->g_id) == 0,
+ "group creation failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_group_add(g->g_id, g->g_ids[0], true) == 0,
+ "group leader add failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_group_commit(g->g_id) == 0,
+ "group commit failed: %s", strerror(errno));
+}
+
+static void
+row_occupancy_init(struct row_occupancy *occupancy)
+{
+ u_int i;
+
+ memset(occupancy, 0, sizeof(*occupancy));
+ for (i = 0; i < nitems(occupancy->ro_ids); i++)
+ occupancy->ro_ids[i] = PMC_ID_INVALID;
+}
+
+/*
+ * Reserve every row that can host TEST_EVENT.
+ * For an ungrouped PMC with no free row, the error is EINVAL.
+ * Treat any other failure as a setup error. Do not skip the test.
+ */
+static void
+row_occupancy_fill_required(struct row_occupancy *occupancy)
+{
+ pmc_id_t id;
+ int allocation_errno;
+
+ row_occupancy_init(occupancy);
+ while (occupancy->ro_n < nitems(occupancy->ro_ids)) {
+ errno = 0;
+ if (pmc_allocate(TEST_EVENT, PMC_MODE_TC, 0, PMC_CPU_ANY, &id,
+ 0) == 0) {
+ occupancy->ro_ids[occupancy->ro_n++] = id;
+ continue;
+ }
+ allocation_errno = errno;
+ ATF_REQUIRE_MSG(allocation_errno == EINVAL,
+ "occupier allocation %u failed with errno %d (%s), "
+ "expected no-row EINVAL", occupancy->ro_n,
+ allocation_errno, strerror(allocation_errno));
+ ATF_REQUIRE_MSG(occupancy->ro_n > 0,
+ "could not reserve any %s rows", TEST_EVENT);
+ return;
+ }
+ atf_tc_fail("allocated %u %s occupiers without reaching no-row EINVAL",
+ occupancy->ro_n, TEST_EVENT);
+}
+
+static void
+row_occupancy_release_required(struct row_occupancy *occupancy)
+{
+ u_int i;
+
+ for (i = 0; i < occupancy->ro_n; i++) {
+ ATF_REQUIRE_MSG(pmc_release(occupancy->ro_ids[i]) == 0,
+ "releasing occupier %u failed: errno %d (%s)", i, errno,
+ strerror(errno));
+ occupancy->ro_ids[i] = PMC_ID_INVALID;
+ }
+ occupancy->ro_n = 0;
+}
+
+static void __attribute__((noinline))
+work(uint64_t iters)
+{
+ volatile uint64_t sink;
+ uint64_t i;
+
+ sink = 0;
+ for (i = 0; i < iters; i++)
+ sink += i;
+}
+
+static int
+read_byte(int fd, char *value)
+{
+ ssize_t n;
+
+ do {
+ n = read(fd, value, 1);
+ } while (n == -1 && errno == EINTR);
+ if (n == 0)
+ errno = EPIPE;
+ return (n == 1 ? 0 : -1);
+}
+
+static int
+write_byte(int fd, char value)
+{
+ ssize_t n;
+
+ do {
+ n = write(fd, &value, 1);
+ } while (n == -1 && errno == EINTR);
+ return (n == 1 ? 0 : -1);
+}
+
+static ssize_t
+read_full(int fd, void *buffer, size_t length)
+{
+ char *cursor;
+ ssize_t n;
+ size_t done;
+
+ cursor = buffer;
+ done = 0;
+ while (done < length) {
+ n = read(fd, cursor + done, length - done);
+ if (n == -1 && errno == EINTR)
+ continue;
+ if (n <= 0)
+ return (n == 0 ? (ssize_t)done : -1);
+ done += (size_t)n;
+ }
+ return ((ssize_t)done);
+}
+
+static int
+write_full(int fd, const void *buffer, size_t length)
+{
+ const char *cursor;
+ ssize_t n;
+ size_t done;
+
+ cursor = buffer;
+ done = 0;
+ while (done < length) {
+ n = write(fd, cursor + done, length - done);
+ if (n == -1 && errno == EINTR)
+ continue;
+ if (n <= 0)
+ return (-1);
+ done += (size_t)n;
+ }
+ return (0);
+}
+
+/*
+ * The timeout is only a deadlock guard.
+ * EOF is the real sync condition.
+ * exit1() runs the hwpmc exit hook before it closes the file
+ * descriptors.
+ */
+static void
+require_pipe_eof(int fd, const char *description)
+{
+ struct pollfd pfd;
+ char value;
+ int poll_errno, poll_rc;
+
+ memset(&pfd, 0, sizeof(pfd));
+ pfd.fd = fd;
+ pfd.events = POLLIN | POLLHUP;
+ errno = 0;
+ do {
+ poll_rc = poll(&pfd, 1, 5000);
+ } while (poll_rc == -1 && errno == EINTR);
+ poll_errno = errno;
+ ATF_REQUIRE_MSG(poll_rc == 1,
+ "%s pipe did not close: poll rc=%d errno=%d (%s)",
+ description, poll_rc, poll_errno, strerror(poll_errno));
+
+ errno = 0;
+ ATF_REQUIRE_MSG(read_byte(fd, &value) == -1 && errno == EPIPE,
+ "%s pipe produced data or remained open after exit: revents=%#x",
+ description, pfd.revents);
+}
+
+static void
+gated_child(int command_fd, int ack_fd)
+{
+ char command;
+
+ if (write_byte(ack_fd, CHILD_READY) != 0)
+ _exit(2);
+ if (raise(SIGSTOP) != 0)
+ _exit(3);
+ while (read_byte(command_fd, &command) == 0) {
+ switch (command) {
+ case CHILD_CONTROL:
+ break;
+ case CHILD_WORK:
+ work(CHILD_ITERS);
+ break;
+ case CHILD_EXIT:
+ if (write_byte(ack_fd, command) != 0)
+ _exit(4);
+ _exit(0);
+ default:
+ _exit(5);
+ }
+ if (write_byte(ack_fd, command) != 0)
+ _exit(6);
+ }
+ /* The parent exits on an assertion failure and closes the command pipe. */
+ _exit(0);
+}
+
+/*
+ * This function becomes an explicit group target.
+ * On command, it forks one descendant, then exits before the descendant.
+ * The descendant uses a separate pipe.
+ * This keeps the descendant's phases deterministic after this
+ * process exits.
+ */
+static void
+target_parent_with_child(int parent_command_fd, int child_command_fd,
+ int ack_fd, int result_fd, pmc_id_t leader, pmc_id_t sibling)
+{
+ struct child_read_result result;
+ pid_t child;
+ char command;
+
+ if (write_byte(ack_fd, TARGET_READY) != 0)
+ _exit(2);
+ if (read_byte(parent_command_fd, &command) != 0 ||
+ command != TARGET_FORK)
+ _exit(3);
+ if (write_byte(ack_fd, TARGET_FORKING) != 0)
+ _exit(4);
+
+ child = fork();
+ if (child == -1)
+ _exit(5);
+ if (child == 0) {
+ close(parent_command_fd);
+ memset(&result, 0, sizeof(result));
+ errno = 0;
+ result.cr_leader_rc =
+ pmc_read(leader, &result.cr_leader_value);
+ result.cr_leader_errno = errno;
+ if (sibling != PMC_ID_INVALID) {
+ errno = 0;
+ result.cr_sibling_rc =
+ pmc_read(sibling, &result.cr_sibling_value);
+ result.cr_sibling_errno = errno;
+ }
+ if (write_full(result_fd, &result, sizeof(result)) != 0)
+ _exit(6);
+ close(result_fd);
+ if (write_byte(ack_fd, CHILD_READY) != 0)
+ _exit(7);
+ while (read_byte(child_command_fd, &command) == 0) {
+ switch (command) {
+ case CHILD_CONTROL:
+ break;
+ case CHILD_WORK:
+ work(CHILD_ITERS);
+ break;
+ case CHILD_EXIT:
+ if (write_byte(ack_fd, command) != 0)
+ _exit(8);
+ _exit(0);
+ default:
+ _exit(9);
+ }
+ if (write_byte(ack_fd, command) != 0)
+ _exit(10);
+ }
+ _exit(0);
+ }
+
+ close(child_command_fd);
+ close(result_fd);
+ if (read_byte(parent_command_fd, &command) != 0 ||
+ command != TARGET_EXIT)
+ _exit(11);
+ _exit(0);
+}
+
+static pid_t
+spawn_gated_child(int *command_fd, int *ack_fd)
+{
+ int command_pipe[2], ack_pipe[2];
+ pid_t child;
+ char ready;
+ int status;
+
+ ATF_REQUIRE_MSG(pipe(command_pipe) == 0,
+ "command pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(ack_pipe) == 0,
+ "ack pipe failed: %s", strerror(errno));
+ child = fork();
+ ATF_REQUIRE_MSG(child != -1, "fork failed: %s", strerror(errno));
+ if (child == 0) {
+ close(command_pipe[1]);
+ close(ack_pipe[0]);
+ gated_child(command_pipe[0], ack_pipe[1]);
+ }
+ close(command_pipe[0]);
+ close(ack_pipe[1]);
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "child readiness read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, CHILD_READY);
+ ATF_REQUIRE_MSG(waitpid(child, &status, WUNTRACED) == child,
+ "wait for parked child failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP,
+ "child did not park before attach: status %#x", status);
+ *command_fd = command_pipe[1];
+ *ack_fd = ack_pipe[0];
+ return (child);
+}
+
+static void
+run_child_phase(int command_fd, int ack_fd, char command)
+{
+ char ack;
+
+ ATF_REQUIRE_MSG(write_byte(command_fd, command) == 0,
+ "child command %c failed: %s", command, strerror(errno));
+ ATF_REQUIRE_MSG(read_byte(ack_fd, &ack) == 0,
+ "child acknowledgment for %c failed: %s", command,
+ strerror(errno));
+ ATF_REQUIRE_EQ(ack, command);
+}
+
+static void
+group_snapshot(struct group *g, pmc_value_t values[2])
+{
+ struct pmc_group_member members[2];
+ uint32_t n;
+ u_int i;
+
+ n = g->g_n;
+ memset(members, 0, sizeof(members));
+ ATF_REQUIRE_MSG(pmc_group_read(g->g_ids[0], &n, members, NULL) == 0,
+ "group snapshot failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(n, g->g_n);
+ for (i = 0; i < g->g_n; i++) {
+ ATF_REQUIRE_EQ(members[i].pm_pmcid, g->g_ids[i]);
+ values[i] = members[i].pm_value;
+ }
+}
+
+/* Fork NCHILDREN. Each child does the work. Reap all children. */
+static void
+fork_workload(void)
+{
+ pid_t pids[NCHILDREN];
+ int i, status;
+
+ for (i = 0; i < NCHILDREN; i++) {
+ pids[i] = fork();
+ ATF_REQUIRE(pids[i] != -1);
+ if (pids[i] == 0) {
+ work(CHILD_ITERS);
+ _exit(0);
+ }
+ }
+ for (i = 0; i < NCHILDREN; i++) {
+ ATF_REQUIRE(waitpid(pids[i], &status, 0) == pids[i]);
+ ATF_REQUIRE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
+ }
+}
+
+static pmc_value_t
+leader_value(struct group *g)
+{
+ struct pmc_group_member m[2];
+ uint32_t n;
+
+ n = g->g_n;
+ memset(m, 0, sizeof(m));
+ ATF_REQUIRE_MSG(pmc_group_read(g->g_ids[0], &n, m, NULL) == 0,
+ "group read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(n, g->g_n);
+ return (m[0].pm_value);
+}
+
+/* Run the forking workload under a group. Return the leader's count. */
+static pmc_value_t
+measure(bool descendants)
+{
+ struct group g;
+ pmc_value_t v;
+
+ ATF_REQUIRE_MSG(group_build(&g, descendants) == 0,
+ "commit failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_attach(g.g_ids[0], getpid()) == 0,
+ "attach failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_start(g.g_ids[0]) == 0, "start failed: %s",
+ strerror(errno));
+ g.g_started = true;
+ fork_workload();
+ v = leader_value(&g);
+ group_teardown(&g);
+ return (v);
+}
+
+/*
+ * The children's work must show up in the totals.
+ * They do all the work, so a tree-wide count is far above a
+ * parent-only one.
+ * The test compares this run against the same workload measured
+ * without the flag. It does not assume any absolute event count.
+ *
+ * This test is skipped until the scheduler places a forked child's
+ * PMCs. A child's first turn on a CPU goes through sched_fork_exit().
+ * Unlike sched_switch(), sched_fork_exit() never calls
+ * PMC_SWITCH_CONTEXT(td, PMC_FN_CSW_IN). So the system never programs
+ * the inherited counters, and nobody counts the child's events.
+ * This gap is not new and is not specific to groups: ungrouped
+ * PMC_F_DESCENDANTS measures the same nothing, on a stock module and
+ * on both schedulers. The rest of this file still checks the part
+ * §3.9 requires: the whole group follows the fork, and totals never
+ * go backwards.
+ */
+ATF_TC_WITHOUT_HEAD(descendants_counted);
+ATF_TC_BODY(descendants_counted, tc)
+{
+ pmc_value_t with, without;
+
+ require_hwpmc();
+
+ without = measure(false);
+ with = measure(true);
+
+ printf("parent only: %ju\ntree wide: %ju\n", (uintmax_t)without,
+ (uintmax_t)with);
+ ATF_CHECK_MSG(with > without * 2,
+ "grouped -d counted %ju against %ju without the flag: the "
+ "children's work is missing from the totals",
+ (uintmax_t)with, (uintmax_t)without);
+}
+
+/*
+ * Every member follows the fork, not just the leader.
+ * §2.4 must hold in the child too, so the siblings must match.
+ */
+ATF_TC_WITHOUT_HEAD(descendants_whole_group);
+ATF_TC_BODY(descendants_whole_group, tc)
+{
+ struct pmc_group_member m[2];
+ struct group g;
+ uint32_t n;
+
+ require_hwpmc();
+ ATF_REQUIRE_MSG(group_build(&g, true) == 0, "commit failed: %s",
+ strerror(errno));
+ ATF_REQUIRE(pmc_attach(g.g_ids[0], getpid()) == 0);
+ ATF_REQUIRE(pmc_start(g.g_ids[0]) == 0);
+ g.g_started = true;
+ fork_workload();
+
+ n = g.g_n;
+ memset(m, 0, sizeof(m));
+ ATF_REQUIRE_MSG(pmc_group_read(g.g_ids[0], &n, m, NULL) == 0,
+ "group read failed: %s", strerror(errno));
+ ATF_CHECK_EQ(n, 2);
+ ATF_CHECK(m[0].pm_value > 0);
+ ATF_CHECK_MSG(m[1].pm_value > 0,
+ "the non-leader member counted nothing, so the group did not "
+ "follow the fork as a whole");
+ group_teardown(&g);
+}
+
+/*
+ * A resident grouped fork must publish all member links together.
+ * The leader read proves that the child inherited the group.
+ * The sibling read proves that the child got more than the old
+ * leader-only link.
+ */
+ATF_TC_WITHOUT_HEAD(descendants_child_reads_whole_group);
+ATF_TC_BODY(descendants_child_reads_whole_group, tc)
+{
+ struct child_read_result result;
+ struct group g;
+ pmc_id_t control;
+ ssize_t nread;
+ pid_t child, waited;
+ int result_pipe[2], status, wait_errno;
+
+ require_hwpmc();
+ group_build_required(&g, true);
+ ATF_REQUIRE_MSG(pmc_attach(g.g_ids[0], getpid()) == 0,
+ "group attach failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_start(g.g_ids[0]) == 0,
+ "group start failed: %s", strerror(errno));
+ g.g_started = true;
+ ATF_REQUIRE_MSG(pmc_allocate(TEST_EVENT, PMC_MODE_TC,
+ PMC_F_DESCENDANTS, PMC_CPU_ANY, &control, 0) == 0,
+ "ungrouped descendants control allocation failed: %s",
+ strerror(errno));
+ ATF_REQUIRE_MSG(pmc_attach(control, getpid()) == 0,
+ "ungrouped descendants control attach failed: %s",
+ strerror(errno));
+ ATF_REQUIRE_MSG(pmc_start(control) == 0,
+ "ungrouped descendants control start failed: %s",
+ strerror(errno));
+ ATF_REQUIRE_MSG(pipe(result_pipe) == 0,
+ "result pipe failed: %s", strerror(errno));
+
+ child = fork();
+ ATF_REQUIRE_MSG(child != -1, "fork failed: %s", strerror(errno));
+ if (child == 0) {
+ close(result_pipe[0]);
+ memset(&result, 0, sizeof(result));
+ errno = 0;
+ result.cr_control_rc =
+ pmc_read(control, &result.cr_control_value);
+ result.cr_control_errno = errno;
+ errno = 0;
+ result.cr_leader_rc =
+ pmc_read(g.g_ids[0], &result.cr_leader_value);
+ result.cr_leader_errno = errno;
+ errno = 0;
+ result.cr_sibling_rc =
+ pmc_read(g.g_ids[1], &result.cr_sibling_value);
+ result.cr_sibling_errno = errno;
+ if (write_full(result_pipe[1], &result, sizeof(result)) != 0)
+ _exit(2);
+ _exit(0);
+ }
+
+ close(result_pipe[1]);
+ memset(&result, 0, sizeof(result));
+ nread = read_full(result_pipe[0], &result, sizeof(result));
+ close(result_pipe[0]);
+ waited = waitpid(child, &status, 0);
+ wait_errno = errno;
+ (void)pmc_stop(control);
+ (void)pmc_release(control);
+ group_teardown(&g);
+
+ ATF_REQUIRE_MSG(nread == (ssize_t)sizeof(result),
+ "child result read returned %zd of %zu bytes", nread,
+ sizeof(result));
+ ATF_REQUIRE_MSG(waited == child, "waitpid failed: errno %d (%s)",
+ wait_errno, strerror(wait_errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "child exited abnormally: status %#x", status);
+ ATF_REQUIRE_MSG(result.cr_control_rc == 0,
+ "ungrouped descendants control was not inherited: errno %d (%s)",
+ result.cr_control_errno, strerror(result.cr_control_errno));
+ ATF_REQUIRE_MSG(result.cr_leader_rc == 0,
+ "child leader read did not reach inherited target: errno %d (%s)",
+ result.cr_leader_errno, strerror(result.cr_leader_errno));
+ ATF_CHECK_MSG(result.cr_sibling_rc == 0,
+ "child inherited only the leader; sibling read failed: errno %d "
+ "(%s)", result.cr_sibling_errno,
+ strerror(result.cr_sibling_errno));
+}
+
+static void
+fork_inherit_alloc_failure_atomic(const char *failpoint_name, u_int fail_after,
+ const char *failure_kind)
+{
+ struct child_read_result result;
+ struct live_target_counts initial, parent_only, child_live, child_gone;
+ struct live_target_counts final;
+ struct group g;
+ ssize_t nread;
+ pid_t child, waited;
+ char release;
+ u_int failpoint;
+ int release_pipe[2], result_pipe[2], status, wait_errno;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+
+ group_build_required(&g, true);
+ ATF_REQUIRE_MSG(pmc_attach(g.g_ids[0], getpid()) == 0,
+ "group attach failed: %s", strerror(errno));
+ require_test_sysctl_write(TEST_HOLD_GROUP_RESIDENT, g.g_id);
+ ATF_REQUIRE_MSG(pmc_start(g.g_ids[0]) == 0,
+ "group start failed: %s", strerror(errno));
+ g.g_started = true;
+ wait_test_sysctl_ack(TEST_HOLD_GROUP_RESIDENT_ACK, g.g_id);
+ read_live_target_counts(&parent_only);
+
+ require_test_sysctl_write(failpoint_name, fail_after);
+ ATF_REQUIRE_MSG(pipe(result_pipe) == 0,
+ "result pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(release_pipe) == 0,
+ "release pipe failed: %s", strerror(errno));
+ child = fork();
+ ATF_REQUIRE_MSG(child != -1, "fork failed: %s", strerror(errno));
+ if (child == 0) {
+ close(result_pipe[0]);
+ close(release_pipe[1]);
+ memset(&result, 0, sizeof(result));
+ errno = 0;
+ result.cr_leader_rc =
+ pmc_read(g.g_ids[0], &result.cr_leader_value);
+ result.cr_leader_errno = errno;
+ errno = 0;
+ result.cr_sibling_rc =
+ pmc_read(g.g_ids[1], &result.cr_sibling_value);
+ result.cr_sibling_errno = errno;
+ if (write_full(result_pipe[1], &result, sizeof(result)) != 0)
+ _exit(2);
+ if (read_byte(release_pipe[0], &release) != 0)
+ _exit(3);
+ _exit(release == CHILD_EXIT ? 0 : 4);
+ }
+
+ close(result_pipe[1]);
+ close(release_pipe[0]);
+ memset(&result, 0, sizeof(result));
+ nread = read_full(result_pipe[0], &result, sizeof(result));
+ close(result_pipe[0]);
+ ATF_REQUIRE_MSG(sysctl_read_u32(failpoint_name, &failpoint) == 0,
+ "reading consumed %s failpoint failed: errno %d (%s)",
+ failure_kind, errno, strerror(errno));
+ read_live_target_counts(&child_live);
+ ATF_REQUIRE_MSG(write_byte(release_pipe[1], CHILD_EXIT) == 0,
+ "releasing child failed: %s", strerror(errno));
+ close(release_pipe[1]);
+ waited = waitpid(child, &status, 0);
+ wait_errno = errno;
+ read_live_target_counts(&child_gone);
+
+ reset_fork_test_hooks();
+ group_teardown(&g);
+ read_live_target_counts(&final);
+
+ ATF_REQUIRE_MSG(nread == (ssize_t)sizeof(result),
+ "child result read returned %zd of %zu bytes", nread,
+ sizeof(result));
+ ATF_REQUIRE_MSG(waited == child, "waitpid failed: errno %d (%s)",
+ wait_errno, strerror(wait_errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "child exited abnormally: status %#x", status);
+ ATF_REQUIRE_MSG(failpoint == UINT_MAX,
+ "%s failpoint was not consumed: value %u", failure_kind,
+ failpoint);
+ ATF_CHECK_MSG(result.cr_leader_rc == -1 &&
+ result.cr_leader_errno == ESRCH,
+ "%s failure left leader access rc=%d errno=%d (%s)",
+ failure_kind, result.cr_leader_rc, result.cr_leader_errno,
+ strerror(result.cr_leader_errno));
+ ATF_CHECK_MSG(result.cr_sibling_rc == -1 &&
+ result.cr_sibling_errno == ESRCH,
+ "%s failure left sibling access rc=%d errno=%d (%s)",
+ failure_kind, result.cr_sibling_rc, result.cr_sibling_errno,
+ strerror(result.cr_sibling_errno));
+ ATF_CHECK_MSG(child_live.ltc_pmc_targets ==
+ parent_only.ltc_pmc_targets &&
+ child_live.ltc_group_targets == parent_only.ltc_group_targets &&
+ child_live.ltc_target_processes == parent_only.ltc_target_processes,
+ "%s failure changed live counts while child was alive: "
+ "pmc_target %ju/%ju group_target %ju/%ju process %ju/%ju",
+ failure_kind,
+ (uintmax_t)child_live.ltc_pmc_targets,
+ (uintmax_t)parent_only.ltc_pmc_targets,
+ (uintmax_t)child_live.ltc_group_targets,
+ (uintmax_t)parent_only.ltc_group_targets,
+ (uintmax_t)child_live.ltc_target_processes,
+ (uintmax_t)parent_only.ltc_target_processes);
+ ATF_CHECK_MSG(child_gone.ltc_pmc_targets ==
+ parent_only.ltc_pmc_targets &&
+ child_gone.ltc_group_targets == parent_only.ltc_group_targets &&
+ child_gone.ltc_target_processes == parent_only.ltc_target_processes,
+ "child exit after %s failure did not restore the parent-only "
+ "baseline", failure_kind);
+ ATF_CHECK_MSG(final.ltc_pmc_targets == initial.ltc_pmc_targets &&
+ final.ltc_group_targets == initial.ltc_group_targets &&
+ final.ltc_target_processes == initial.ltc_target_processes &&
+ final.ltc_residual_entries == initial.ltc_residual_entries &&
+ final.ltc_rotation_refs == initial.ltc_rotation_refs &&
+ final.ltc_run_refs == initial.ltc_run_refs,
+ "group teardown after %s failure did not restore initial live counts: "
+ "pmc_target %ju/%ju group_target %ju/%ju process %ju/%ju "
+ "residual %ju/%ju rotation_ref %ju/%ju run_ref %ju/%ju",
+ failure_kind,
+ (uintmax_t)final.ltc_pmc_targets,
+ (uintmax_t)initial.ltc_pmc_targets,
+ (uintmax_t)final.ltc_group_targets,
+ (uintmax_t)initial.ltc_group_targets,
+ (uintmax_t)final.ltc_target_processes,
+ (uintmax_t)initial.ltc_target_processes,
+ (uintmax_t)final.ltc_residual_entries,
+ (uintmax_t)initial.ltc_residual_entries,
+ (uintmax_t)final.ltc_rotation_refs,
+ (uintmax_t)initial.ltc_rotation_refs,
+ (uintmax_t)final.ltc_run_refs,
+ (uintmax_t)initial.ltc_run_refs);
+}
+
+/*
+ * The authoritative inherited edge may fail to allocate.
+ * If it fails, no transient leader link may survive.
+ * No child process descriptor may survive either.
+ * The child stays alive while the parent checks the object-count
+ * baseline.
+ */
+ATF_TC_WITH_CLEANUP(descendants_fork_inherit_alloc_failure_atomic);
+ATF_TC_HEAD(descendants_fork_inherit_alloc_failure_atomic, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_fork_inherit_alloc_failure_atomic, tc)
+{
+
+ fork_inherit_alloc_failure_atomic(TEST_FAIL_GROUP_TARGET_ALLOC, 0,
+ "group-target allocation");
+}
+ATF_TC_CLEANUP(descendants_fork_inherit_alloc_failure_atomic, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * A later member-link allocation can fail.
+ * This failure must roll back the first link, which already allocated.
+ * It must also roll back the authoritative edge.
+ * Use two members and allow one allocation.
+ * This proves that the failpoint reached the second link.
+ */
+ATF_TC_WITH_CLEANUP(descendants_fork_inherit_link_alloc_failure_atomic);
+ATF_TC_HEAD(descendants_fork_inherit_link_alloc_failure_atomic, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_fork_inherit_link_alloc_failure_atomic, tc)
+{
+
+ fork_inherit_alloc_failure_atomic(TEST_FAIL_TARGET_LINK_ALLOC, 1,
+ "later target-link allocation");
+}
+ATF_TC_CLEANUP(descendants_fork_inherit_link_alloc_failure_atomic, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * A fork-inheritance miss is not a detach.
+ * The child edge was never published.
+ * Allow one of two descendants groups to inherit.
+ * Use a child-side handle lookup to find the success and the miss.
+ * This method does not depend on list order.
+ * The log must show one PROCFORK record.
+ * The log must show one PMCATTACH record for each successful member.
+ * The log must not show PMCATTACH or PMCDETACH for any missed member
+ * before the marker.
+ */
+ATF_TC_WITH_CLEANUP(descendants_fork_miss_has_no_false_detach);
+ATF_TC_HEAD(descendants_fork_miss_has_no_false_detach, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_fork_miss_has_no_false_detach, tc)
+{
+ struct fork_log_child_result child_result;
+ struct fork_log_counts log_counts;
+ struct live_target_counts initial, final;
+ struct group groups[FORK_LOG_NGROUPS];
+ struct pollfd pfd;
+ char failure[1024], log_path[PATH_MAX], release;
+ pid_t child, waited;
+ ssize_t nread;
+ u_int failpoint, group, member;
+ int inherited_group, missed_group;
+ int log_fd, poll_errno, poll_rc, release_pipe[2], result_pipe[2];
+ int saved_errno, status;
+ bool all_missing, all_read, logfile_configured;
+
+ for (group = 0; group < FORK_LOG_NGROUPS; group++)
+ group_init(&groups[group]);
+ memset(&child_result, 0, sizeof(child_result));
+ memset(&log_counts, 0, sizeof(log_counts));
+ memset(&initial, 0, sizeof(initial));
+ memset(&final, 0, sizeof(final));
+ memset(&pfd, 0, sizeof(pfd));
+ failure[0] = '\0';
+ log_path[0] = '\0';
+ child = -1;
+ waited = -1;
+ status = 0;
+ failpoint = 0;
+ log_fd = -1;
+ release_pipe[0] = release_pipe[1] = -1;
+ result_pipe[0] = result_pipe[1] = -1;
+ logfile_configured = false;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+
+ log_fd = open_named_logfile(log_path, sizeof(log_path));
+ if (log_fd < 0) {
+ saved_errno = errno;
+ log_path[0] = '\0';
+ snprintf(failure, sizeof(failure),
+ "creating fork-inherit logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_configure_logfile(log_fd) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "configuring fork-inherit logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ logfile_configured = true;
+
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ if (group_build(&groups[group], true) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "building descendants group %u failed: errno %d (%s)",
+ group, saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_attach(groups[group].g_ids[0], getpid()) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "attaching descendants group %u failed: errno %d (%s)",
+ group, saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ }
+
+ if (sysctl_write_u32(TEST_FAIL_GROUP_TARGET_ALLOC, 1) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "arming fork group-target failpoint failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pipe(result_pipe) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "creating child-result pipe failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pipe(release_pipe) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "creating child-release pipe failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+
+ child = fork();
+ if (child < 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "forking log target failed: errno %d (%s)", saved_errno,
+ strerror(saved_errno));
+ goto cleanup;
+ }
+ if (child == 0) {
+ close(result_pipe[0]);
+ close(release_pipe[1]);
+ close(log_fd);
+ memset(&child_result, 0, sizeof(child_result));
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ for (member = 0; member < FORK_LOG_NMEMBERS; member++) {
+ errno = 0;
+ child_result.flcr_rc[group][member] =
+ pmc_read(groups[group].g_ids[member],
+ &child_result.flcr_value[group][member]);
+ child_result.flcr_errno[group][member] = errno;
+ }
+ }
+ if (write_full(result_pipe[1], &child_result,
+ sizeof(child_result)) != 0)
+ _exit(2);
+ if (read_byte(release_pipe[0], &release) != 0)
+ _exit(3);
+ _exit(release == CHILD_EXIT ? 0 : 4);
+ }
+
+ close(result_pipe[1]);
+ result_pipe[1] = -1;
+ close(release_pipe[0]);
+ release_pipe[0] = -1;
+ pfd.fd = result_pipe[0];
+ pfd.events = POLLIN | POLLHUP;
+ errno = 0;
+ do {
+ poll_rc = poll(&pfd, 1, 5000);
+ } while (poll_rc == -1 && errno == EINTR);
+ poll_errno = errno;
+ if (poll_rc != 1) {
+ snprintf(failure, sizeof(failure),
+ "child result was not ready: poll rc=%d errno=%d (%s)",
+ poll_rc, poll_errno, strerror(poll_errno));
+ goto cleanup;
+ }
+ nread = read_full(result_pipe[0], &child_result,
+ sizeof(child_result));
+ if (nread != (ssize_t)sizeof(child_result)) {
+ snprintf(failure, sizeof(failure),
+ "child result read returned %zd of %zu bytes", nread,
+ sizeof(child_result));
+ goto cleanup;
+ }
+ close(result_pipe[0]);
+ result_pipe[0] = -1;
+
+ if (sysctl_read_u32(TEST_FAIL_GROUP_TARGET_ALLOC, &failpoint) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "reading consumed fork failpoint failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_writelog(TEST_FORK_LOG_MARKER) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "writing fork-inherit marker failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_flush_logfile() != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "flushing fork-inherit marker failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (scan_fork_inherit_log(log_path, getpid(), child, groups,
+ &log_counts) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "parsing fork-inherit logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+
+ if (write_byte(release_pipe[1], CHILD_EXIT) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "releasing fork-log child failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ close(release_pipe[1]);
+ release_pipe[1] = -1;
+ do {
+ waited = waitpid(child, &status, 0);
+ } while (waited < 0 && errno == EINTR);
+ if (waited != child) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "waiting for fork-log child failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ }
+
+cleanup:
+ if (result_pipe[0] >= 0)
+ (void)close(result_pipe[0]);
+ if (result_pipe[1] >= 0)
+ (void)close(result_pipe[1]);
+ if (release_pipe[0] >= 0)
+ (void)close(release_pipe[0]);
+ if (release_pipe[1] >= 0) {
+ (void)close(release_pipe[1]);
+ release_pipe[1] = -1;
+ }
+ if (child > 0 && waited != child) {
+ do {
+ waited = waitpid(child, &status, 0);
+ } while (waited < 0 && errno == EINTR);
+ }
+ reset_fork_test_hooks();
+ for (group = 0; group < FORK_LOG_NGROUPS; group++)
+ group_teardown(&groups[group]);
+ if (logfile_configured && pmc_configure_logfile(-1) != 0 &&
+ failure[0] == '\0') {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "deconfiguring fork-inherit logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ }
+ if (log_fd >= 0 && close(log_fd) != 0 && failure[0] == '\0') {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "closing fork-inherit logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ }
+ if (log_path[0] != '\0' && unlink(log_path) != 0 &&
+ failure[0] == '\0') {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "unlinking fork-inherit logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ }
+ read_live_target_counts(&final);
+ check_live_counts_equal("fork-miss logging cleanup", &final, &initial);
+
+ if (failure[0] != '\0')
+ atf_tc_fail("%s", failure);
+ ATF_REQUIRE_MSG(waited == child,
+ "fork-log child was not reaped: waited=%jd child=%jd",
+ (intmax_t)waited, (intmax_t)child);
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "fork-log child exited abnormally: status %#x", status);
+ ATF_REQUIRE_MSG(failpoint == UINT_MAX,
+ "fork group-target failpoint was not consumed: value %u",
+ failpoint);
+
+ inherited_group = -1;
+ missed_group = -1;
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ all_read = true;
+ all_missing = true;
+ for (member = 0; member < FORK_LOG_NMEMBERS; member++) {
+ all_read = all_read &&
+ child_result.flcr_rc[group][member] == 0;
+ all_missing = all_missing &&
+ child_result.flcr_rc[group][member] == -1 &&
+ child_result.flcr_errno[group][member] == ESRCH;
+ }
+ if (all_read)
+ inherited_group = (int)group;
+ else if (all_missing)
+ missed_group = (int)group;
+ }
+ ATF_REQUIRE_MSG(inherited_group >= 0 && missed_group >= 0 &&
+ inherited_group != missed_group,
+ "fork failpoint did not produce one whole inherited group and one "
+ "whole missed group: g0=[%d/%d,%d/%d] g1=[%d/%d,%d/%d]",
+ child_result.flcr_rc[0][0], child_result.flcr_errno[0][0],
+ child_result.flcr_rc[0][1], child_result.flcr_errno[0][1],
+ child_result.flcr_rc[1][0], child_result.flcr_errno[1][0],
+ child_result.flcr_rc[1][1], child_result.flcr_errno[1][1]);
+ ATF_CHECK_MSG(log_counts.flc_marker_seen &&
+ log_counts.flc_procfork == 1 &&
+ log_counts.flc_attach[missed_group][0] == 0 &&
+ log_counts.flc_attach[missed_group][1] == 0 &&
+ log_counts.flc_detach[missed_group][0] == 0 &&
+ log_counts.flc_detach[missed_group][1] == 0 &&
+ log_counts.flc_attach[inherited_group][0] == 1 &&
+ log_counts.flc_attach[inherited_group][1] == 1 &&
+ log_counts.flc_detach[inherited_group][0] == 0 &&
+ log_counts.flc_detach[inherited_group][1] == 0,
+ "fork-miss log mismatch: marker=%u PROCFORK=%u "
+ "missed attach=[%u,%u] detach=[%u,%u], "
+ "inherited attach=[%u,%u] detach=[%u,%u]",
+ log_counts.flc_marker_seen ? 1 : 0, log_counts.flc_procfork,
+ log_counts.flc_attach[missed_group][0],
+ log_counts.flc_attach[missed_group][1],
+ log_counts.flc_detach[missed_group][0],
+ log_counts.flc_detach[missed_group][1],
+ log_counts.flc_attach[inherited_group][0],
+ log_counts.flc_attach[inherited_group][1],
+ log_counts.flc_detach[inherited_group][0],
+ log_counts.flc_detach[inherited_group][1]);
+}
+ATF_TC_CLEANUP(descendants_fork_miss_has_no_false_detach, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * Repeat the partial-inheritance case.
+ * This time, the forking process is itself an inherited target.
+ * The attach records from the first fork prove that both groups
+ * reached the intermediate process.
+ * The second fork must identify its one failed group.
+ * Use the leader handle and the child PID to identify it.
+ */
+ATF_TC_WITH_CLEANUP(descendants_inherited_parent_fork_miss_is_identified);
+ATF_TC_HEAD(descendants_inherited_parent_fork_miss_is_identified, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_inherited_parent_fork_miss_is_identified, tc)
+{
+ struct inherited_fork_result result;
+ struct inherited_fork_log_counts log_counts;
+ struct live_target_counts initial, final;
+ struct group groups[FORK_LOG_NGROUPS];
+ struct pollfd pfd;
+ char failure[1024], log_path[PATH_MAX], release;
+ pmc_id_t leaders[FORK_LOG_NGROUPS];
+ pid_t child, inherited_parent, waited;
+ ssize_t nread;
+ u_int failpoint, group, member;
+ int inherited_group, missed_group;
+ int log_fd, poll_errno, poll_rc, release_pipe[2], result_pipe[2];
+ int saved_errno, status;
+ bool all_missing, all_read, grandchild_ready, logfile_configured;
+
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ group_init(&groups[group]);
+ leaders[group] = PMC_ID_INVALID;
+ }
+ memset(&result, 0, sizeof(result));
+ memset(&log_counts, 0, sizeof(log_counts));
+ memset(&initial, 0, sizeof(initial));
+ memset(&final, 0, sizeof(final));
+ memset(&pfd, 0, sizeof(pfd));
+ failure[0] = '\0';
+ log_path[0] = '\0';
+ child = -1;
+ inherited_parent = -1;
+ waited = -1;
+ status = 0;
+ failpoint = 0;
+ log_fd = -1;
+ release_pipe[0] = release_pipe[1] = -1;
+ result_pipe[0] = result_pipe[1] = -1;
+ grandchild_ready = false;
+ logfile_configured = false;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+
+ log_fd = open_named_logfile(log_path, sizeof(log_path));
+ if (log_fd < 0) {
+ saved_errno = errno;
+ log_path[0] = '\0';
+ snprintf(failure, sizeof(failure),
+ "creating inherited-parent logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_configure_logfile(log_fd) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "configuring inherited-parent logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ logfile_configured = true;
+
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ if (group_build(&groups[group], true) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "building inherited-parent group %u failed: "
+ "errno %d (%s)", group, saved_errno,
+ strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_attach(groups[group].g_ids[0], getpid()) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "attaching inherited-parent group %u failed: "
+ "errno %d (%s)", group, saved_errno,
+ strerror(saved_errno));
+ goto cleanup;
+ }
+ leaders[group] = groups[group].g_ids[0];
+ }
+
+ if (pipe(result_pipe) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "creating inherited-parent result pipe failed: "
+ "errno %d (%s)", saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pipe(release_pipe) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "creating inherited-parent release pipe failed: "
+ "errno %d (%s)", saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+
+ inherited_parent = fork();
+ if (inherited_parent < 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "forking inherited parent failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (inherited_parent == 0) {
+ close(result_pipe[0]);
+ close(release_pipe[1]);
+ close(log_fd);
+ memset(&result, 0, sizeof(result));
+ if (sysctl_write_u32(TEST_FAIL_GROUP_TARGET_ALLOC, 1) != 0) {
+ result.ifr_stage = INHERITED_FORK_RESULT_FAILPOINT;
+ result.ifr_errno = errno;
+ (void)write_full(result_pipe[1], &result,
+ sizeof(result));
+ _exit(2);
+ }
+
+ child = fork();
+ if (child < 0) {
+ result.ifr_stage = INHERITED_FORK_RESULT_FORK;
+ result.ifr_errno = errno;
+ (void)write_full(result_pipe[1], &result,
+ sizeof(result));
+ _exit(3);
+ }
+ if (child == 0) {
+ result.ifr_stage = INHERITED_FORK_RESULT_CHILD;
+ result.ifr_pid = getpid();
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ for (member = 0; member < FORK_LOG_NMEMBERS;
+ member++) {
+ errno = 0;
+ result.ifr_reads.flcr_rc[group][member] =
+ pmc_read(groups[group].g_ids[member],
+ &result.ifr_reads.flcr_value[group][member]);
+ result.ifr_reads.flcr_errno[group][member] =
+ errno;
+ }
+ }
+ if (write_full(result_pipe[1], &result,
+ sizeof(result)) != 0)
+ _exit(4);
+ if (read_byte(release_pipe[0], &release) != 0)
+ _exit(5);
+ _exit(release == CHILD_EXIT ? 0 : 6);
+ }
+
+ close(result_pipe[1]);
+ close(release_pipe[0]);
+ do {
+ waited = waitpid(child, &status, 0);
+ } while (waited < 0 && errno == EINTR);
+ if (waited != child)
+ _exit(7);
+ if (!WIFEXITED(status))
+ _exit(8);
+ _exit(WEXITSTATUS(status));
+ }
+
+ close(result_pipe[1]);
+ result_pipe[1] = -1;
+ close(release_pipe[0]);
+ release_pipe[0] = -1;
+ pfd.fd = result_pipe[0];
+ pfd.events = POLLIN | POLLHUP;
+ errno = 0;
+ do {
+ poll_rc = poll(&pfd, 1, 5000);
+ } while (poll_rc == -1 && errno == EINTR);
+ poll_errno = errno;
+ if (poll_rc != 1) {
+ snprintf(failure, sizeof(failure),
+ "inherited-parent result was not ready: poll rc=%d "
+ "errno=%d (%s)", poll_rc, poll_errno,
+ strerror(poll_errno));
+ goto cleanup;
+ }
+ nread = read_full(result_pipe[0], &result, sizeof(result));
+ if (nread != (ssize_t)sizeof(result)) {
+ snprintf(failure, sizeof(failure),
+ "inherited-parent result read returned %zd of %zu bytes",
+ nread, sizeof(result));
+ goto cleanup;
+ }
+ close(result_pipe[0]);
+ result_pipe[0] = -1;
+ if (result.ifr_stage != INHERITED_FORK_RESULT_CHILD) {
+ snprintf(failure, sizeof(failure),
+ "inherited-parent setup failed at stage %d: errno %d (%s)",
+ result.ifr_stage, result.ifr_errno,
+ strerror(result.ifr_errno));
+ goto cleanup;
+ }
+ child = result.ifr_pid;
+ if (child <= 0) {
+ snprintf(failure, sizeof(failure),
+ "inherited-parent result contained invalid child pid %jd",
+ (intmax_t)child);
+ goto cleanup;
+ }
+ grandchild_ready = true;
+
+ if (sysctl_read_u32(TEST_FAIL_GROUP_TARGET_ALLOC, &failpoint) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "reading inherited-parent failpoint failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_writelog(TEST_FORK_LOG_MARKER) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "writing inherited-parent marker failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (pmc_flush_logfile() != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "flushing inherited-parent marker failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ if (scan_inherited_fork_miss_log(log_path, inherited_parent, child,
+ groups, &log_counts) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "parsing inherited-parent logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+
+ grandchild_ready = false;
+ if (write_byte(release_pipe[1], CHILD_EXIT) != 0) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "releasing inherited grandchild failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ goto cleanup;
+ }
+ close(release_pipe[1]);
+ release_pipe[1] = -1;
+ do {
+ waited = waitpid(inherited_parent, &status, 0);
+ } while (waited < 0 && errno == EINTR);
+ if (waited != inherited_parent) {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "waiting for inherited parent failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ }
+
+cleanup:
+ if (result_pipe[0] >= 0)
+ (void)close(result_pipe[0]);
+ if (result_pipe[1] >= 0)
+ (void)close(result_pipe[1]);
+ if (release_pipe[0] >= 0)
+ (void)close(release_pipe[0]);
+ if (release_pipe[1] >= 0) {
+ if (grandchild_ready)
+ (void)write_byte(release_pipe[1], CHILD_EXIT);
+ (void)close(release_pipe[1]);
+ release_pipe[1] = -1;
+ }
+ if (inherited_parent > 0 && waited != inherited_parent) {
+ do {
+ waited = waitpid(inherited_parent, &status, 0);
+ } while (waited < 0 && errno == EINTR);
+ }
+ reset_fork_test_hooks();
+ for (group = 0; group < FORK_LOG_NGROUPS; group++)
+ group_teardown(&groups[group]);
+ if (logfile_configured && pmc_configure_logfile(-1) != 0 &&
+ failure[0] == '\0') {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "deconfiguring inherited-parent logfile failed: "
+ "errno %d (%s)", saved_errno, strerror(saved_errno));
+ }
+ if (log_fd >= 0 && close(log_fd) != 0 && failure[0] == '\0') {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "closing inherited-parent logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ }
+ if (log_path[0] != '\0' && unlink(log_path) != 0 &&
+ failure[0] == '\0') {
+ saved_errno = errno;
+ snprintf(failure, sizeof(failure),
+ "unlinking inherited-parent logfile failed: errno %d (%s)",
+ saved_errno, strerror(saved_errno));
+ }
+ read_live_target_counts(&final);
+ check_live_counts_equal("inherited-parent fork-miss cleanup", &final,
+ &initial);
+
+ if (failure[0] != '\0')
+ atf_tc_fail("%s", failure);
+ ATF_REQUIRE_MSG(waited == inherited_parent,
+ "inherited parent was not reaped: waited=%jd parent=%jd",
+ (intmax_t)waited, (intmax_t)inherited_parent);
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "inherited parent exited abnormally: status %#x", status);
+ ATF_REQUIRE_MSG(failpoint == UINT_MAX,
+ "inherited-parent failpoint was not consumed: value %u",
+ failpoint);
+
+ inherited_group = -1;
+ missed_group = -1;
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ all_read = true;
+ all_missing = true;
+ for (member = 0; member < FORK_LOG_NMEMBERS; member++) {
+ all_read = all_read &&
+ result.ifr_reads.flcr_rc[group][member] == 0;
+ all_missing = all_missing &&
+ result.ifr_reads.flcr_rc[group][member] == -1 &&
+ result.ifr_reads.flcr_errno[group][member] == ESRCH;
+ }
+ if (all_read)
+ inherited_group = (int)group;
+ else if (all_missing)
+ missed_group = (int)group;
+ }
+ ATF_REQUIRE_MSG(inherited_group >= 0 && missed_group >= 0 &&
+ inherited_group != missed_group,
+ "inherited-parent failpoint did not produce one whole inherited "
+ "group and one whole missed group: "
+ "g0=[%d/%d,%d/%d] g1=[%d/%d,%d/%d]",
+ result.ifr_reads.flcr_rc[0][0],
+ result.ifr_reads.flcr_errno[0][0],
+ result.ifr_reads.flcr_rc[0][1],
+ result.ifr_reads.flcr_errno[0][1],
+ result.ifr_reads.flcr_rc[1][0],
+ result.ifr_reads.flcr_errno[1][0],
+ result.ifr_reads.flcr_rc[1][1],
+ result.ifr_reads.flcr_errno[1][1]);
+ ATF_REQUIRE_MSG(log_counts.iflc_marker_seen,
+ "inherited-parent marker was not found");
+ ATF_REQUIRE_MSG(log_counts.iflc_procfork == 1,
+ "inherited-parent child PROCFORK count=%u, expected 1",
+ log_counts.iflc_procfork);
+ for (group = 0; group < FORK_LOG_NGROUPS; group++) {
+ for (member = 0; member < FORK_LOG_NMEMBERS; member++) {
+ ATF_REQUIRE_MSG(
+ log_counts.iflc_parent_attach[group][member] == 1,
+ "intermediate target group %u member %u "
+ "PMCATTACH count=%u, expected 1", group, member,
+ log_counts.iflc_parent_attach[group][member]);
+ ATF_REQUIRE_MSG(
+ log_counts.iflc_child_detach[group][member] == 0,
+ "grandchild group %u member %u PMCDETACH count=%u, "
+ "expected 0", group, member,
+ log_counts.iflc_child_detach[group][member]);
+ }
+ }
+ for (member = 0; member < FORK_LOG_NMEMBERS; member++) {
+ ATF_REQUIRE_MSG(
+ log_counts.iflc_child_attach[inherited_group][member] == 1,
+ "inherited grandchild member %u PMCATTACH count=%u, "
+ "expected 1", member,
+ log_counts.iflc_child_attach[inherited_group][member]);
+ ATF_REQUIRE_MSG(
+ log_counts.iflc_child_attach[missed_group][member] == 0,
+ "missed grandchild member %u PMCATTACH count=%u, expected 0",
+ member,
+ log_counts.iflc_child_attach[missed_group][member]);
+ }
+ ATF_CHECK_MSG(log_counts.iflc_child_miss[missed_group] == 1 &&
+ log_counts.iflc_child_miss[inherited_group] == 0 &&
+ log_counts.iflc_unmatched_child_miss == 0,
+ "inherited-parent miss log mismatch: failed leader %#x miss=%u, "
+ "successful leader %#x miss=%u, unmatched=%u; expected [1,0,0]",
+ leaders[missed_group],
+ log_counts.iflc_child_miss[missed_group],
+ leaders[inherited_group],
+ log_counts.iflc_child_miss[inherited_group],
+ log_counts.iflc_unmatched_child_miss);
+}
+ATF_TC_CLEANUP(descendants_inherited_parent_fork_miss_is_identified, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * Fail a later target in an existing process-tree attachment.
+ * Do this after the root target is ready.
+ * The system must not publish anything.
+ * The group must stay usable after the rollback.
+ */
+static void
+descendants_attach_failure_atomic(const char *failpoint_name,
+ u_int fail_after, int expected_errno, const char *failure_kind)
+{
+ struct live_target_counts after_failure, final, initial, prepared;
+ struct group g;
+ pid_t child, waited;
+ u_int failpoint;
+ int ack_fd, attach_errno, attach_rc, command_fd, retry_errno, retry_rc;
+ int status, wait_errno;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+
+ group_build_required(&g, true);
+ child = spawn_gated_child(&command_fd, &ack_fd);
+ read_live_target_counts(&prepared);
+
+ require_test_sysctl_write(failpoint_name, fail_after);
+ errno = 0;
+ attach_rc = pmc_attach(g.g_ids[0], getpid());
+ attach_errno = errno;
+ ATF_REQUIRE_MSG(sysctl_read_u32(failpoint_name, &failpoint) == 0,
+ "reading consumed %s failpoint failed: errno %d (%s)",
+ failure_kind, errno, strerror(errno));
+ read_live_target_counts(&after_failure);
+
+ reset_fork_test_hooks();
+ retry_rc = -2;
+ retry_errno = 0;
+ if (attach_rc == -1) {
+ errno = 0;
+ retry_rc = pmc_attach(g.g_ids[0], getpid());
+ retry_errno = errno;
+ }
+
+ ATF_REQUIRE_MSG(kill(child, SIGCONT) == 0,
+ "could not release parked child: errno %d (%s)", errno,
+ strerror(errno));
+ run_child_phase(command_fd, ack_fd, CHILD_EXIT);
+ close(command_fd);
+ close(ack_fd);
+ waited = waitpid(child, &status, 0);
+ wait_errno = errno;
+
+ group_teardown(&g);
+ read_live_target_counts(&final);
+
+ ATF_CHECK_MSG(attach_rc == -1 && attach_errno == expected_errno,
+ "%s returned rc=%d errno=%d (%s), expected -1/%d (%s)",
+ failure_kind, attach_rc, attach_errno, strerror(attach_errno),
+ expected_errno, strerror(expected_errno));
+ ATF_CHECK_MSG(failpoint == UINT_MAX,
+ "%s failpoint was not consumed: value %u", failure_kind,
+ failpoint);
+ check_live_counts_equal("failed descendants attach changed live counts",
+ &after_failure, &prepared);
+ ATF_CHECK_MSG(retry_rc == 0,
+ "same-group retry after %s returned rc=%d errno=%d (%s)",
+ failure_kind, retry_rc, retry_errno, strerror(retry_errno));
+ ATF_REQUIRE_MSG(waited == child,
+ "waitpid after %s failed: errno %d (%s)", failure_kind,
+ wait_errno, strerror(wait_errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "child after %s exited abnormally: status %#x", failure_kind,
+ status);
+ check_live_counts_equal("rollback/retry cleanup did not restore baseline",
+ &final, &initial);
+}
+
+ATF_TC_WITH_CLEANUP(descendants_attach_alloc_rollback);
+ATF_TC_HEAD(descendants_attach_alloc_rollback, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_attach_alloc_rollback, tc)
+{
+
+ descendants_attach_failure_atomic(TEST_FAIL_GROUP_TARGET_ALLOC, 1,
+ ENOMEM, "later group-target allocation failure");
+}
+ATF_TC_CLEANUP(descendants_attach_alloc_rollback, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+ATF_TC_WITH_CLEANUP(descendants_attach_authorization_rollback);
+ATF_TC_HEAD(descendants_attach_authorization_rollback, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_attach_authorization_rollback, tc)
+{
+
+ descendants_attach_failure_atomic(TEST_FAIL_ATTACH_AUTHORIZATION, 1,
+ EPERM, "later target authorization failure");
+}
+ATF_TC_CLEANUP(descendants_attach_authorization_rollback, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * Take the target-tree snapshot.
+ * Then hold the attachment while the only enumerated target forks.
+ * The process-tree lock orders that fork after publication.
+ * So the new child must inherit the whole group.
+ */
+ATF_TC_WITH_CLEANUP(descendants_attach_race_fork);
+ATF_TC_HEAD(descendants_attach_race_fork, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_attach_race_fork, tc)
+{
+ struct attach_thread_result attach;
+ struct child_read_result child_read;
+ struct group g;
+ struct live_target_counts final, initial;
+ pthread_t attach_thread;
+ pid_t target_parent, waited;
+ ssize_t nread;
+ char ready;
+ u_int paused_targets;
+ int ack_pipe[2], child_command_pipe[2], parent_command_pipe[2];
+ int result_pipe[2], status, thread_error, wait_errno;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+ group_build_required(&g, true);
+ ATF_REQUIRE_MSG(pipe(parent_command_pipe) == 0,
+ "target-parent command pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(child_command_pipe) == 0,
+ "descendant command pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(ack_pipe) == 0,
+ "target acknowledgment pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(result_pipe) == 0,
+ "descendant result pipe failed: %s", strerror(errno));
+
+ target_parent = fork();
+ ATF_REQUIRE_MSG(target_parent != -1, "target fork failed: %s",
+ strerror(errno));
+ if (target_parent == 0) {
+ close(parent_command_pipe[1]);
+ close(child_command_pipe[1]);
+ close(ack_pipe[0]);
+ close(result_pipe[0]);
+ target_parent_with_child(parent_command_pipe[0],
+ child_command_pipe[0], ack_pipe[1], result_pipe[1],
+ g.g_ids[0], g.g_ids[1]);
+ }
+
+ close(parent_command_pipe[0]);
+ close(child_command_pipe[0]);
+ close(ack_pipe[1]);
+ close(result_pipe[1]);
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "target readiness read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, TARGET_READY);
+
+ require_test_sysctl_write(TEST_PAUSE_DESCENDANTS_ATTACH, g.g_id);
+ memset(&attach, 0, sizeof(attach));
+ attach.atr_leader = g.g_ids[0];
+ attach.atr_target = target_parent;
+ thread_error = pthread_create(&attach_thread, NULL, attach_group_thread,
+ &attach);
+ ATF_REQUIRE_MSG(thread_error == 0,
+ "creating attach thread failed: %s", strerror(thread_error));
+ wait_test_sysctl_ack(TEST_PAUSE_DESCENDANTS_ATTACH_ACK, g.g_id);
+ ATF_REQUIRE_MSG(sysctl_read_u32(TEST_PAUSE_DESCENDANTS_TARGET_COUNT,
+ &paused_targets) == 0,
+ "reading paused target count failed: errno %d (%s)", errno,
+ strerror(errno));
+
+ ATF_REQUIRE_MSG(write_byte(parent_command_pipe[1], TARGET_FORK) == 0,
+ "requesting target fork failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "fork-attempt acknowledgment failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, TARGET_FORKING);
+ require_test_sysctl_write(TEST_PAUSE_DESCENDANTS_ATTACH, 0);
+ thread_error = pthread_join(attach_thread, NULL);
+ ATF_REQUIRE_MSG(thread_error == 0,
+ "joining attach thread failed: %s", strerror(thread_error));
+
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "descendant readiness read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, CHILD_READY);
+ memset(&child_read, 0, sizeof(child_read));
+ nread = read_full(result_pipe[0], &child_read, sizeof(child_read));
+ close(result_pipe[0]);
+
+ run_child_phase(child_command_pipe[1], ack_pipe[0], CHILD_EXIT);
+ close(child_command_pipe[1]);
+ ATF_REQUIRE_MSG(write_byte(parent_command_pipe[1], TARGET_EXIT) == 0,
+ "releasing target parent failed: %s", strerror(errno));
+ close(parent_command_pipe[1]);
+ waited = waitpid(target_parent, &status, 0);
+ wait_errno = errno;
+ require_pipe_eof(ack_pipe[0], "fork-race target tree exit");
+ close(ack_pipe[0]);
+
+ group_teardown(&g);
+ reset_fork_test_hooks();
+ read_live_target_counts(&final);
+
+ ATF_CHECK_MSG(paused_targets == 1,
+ "fork race paused with %u targets, expected only the parent",
+ paused_targets);
+ ATF_CHECK_MSG(attach.atr_rc == 0,
+ "fork-race attach returned rc=%d errno=%d (%s)",
+ attach.atr_rc, attach.atr_errno, strerror(attach.atr_errno));
+ ATF_CHECK_MSG(nread == (ssize_t)sizeof(child_read),
+ "fork-race child result read returned %zd of %zu bytes", nread,
+ sizeof(child_read));
+ ATF_CHECK_MSG(child_read.cr_leader_rc == 0,
+ "post-linearization child leader read failed: errno %d (%s)",
+ child_read.cr_leader_errno,
+ strerror(child_read.cr_leader_errno));
+ ATF_CHECK_MSG(child_read.cr_sibling_rc == 0,
+ "post-linearization child sibling read failed: errno %d (%s)",
+ child_read.cr_sibling_errno,
+ strerror(child_read.cr_sibling_errno));
+ ATF_REQUIRE_MSG(waited == target_parent,
+ "waitpid for fork-race target failed: errno %d (%s)",
+ wait_errno, strerror(wait_errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "fork-race target exited abnormally: status %#x", status);
+ check_live_counts_equal("fork-race cleanup did not restore baseline",
+ &final, &initial);
+}
+ATF_TC_CLEANUP(descendants_attach_race_fork, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * Enumerate the owner and one parked child. Then hold the attachment.
+ * Make the child take a snapshot of P_HWPMC in pmc_process_exit().
+ * Take this snapshot before publication. Then release the attachment.
+ * Exit cleanup must remove the child's new edge.
+ * Exit cleanup must keep the owner's edge.
+ */
+ATF_TC_WITH_CLEANUP(descendants_attach_race_exit);
+ATF_TC_HEAD(descendants_attach_race_exit, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_attach_race_exit, tc)
+{
+ struct attach_thread_result attach;
+ struct group g;
+ struct live_target_counts after_exit, final, initial;
+ pthread_t attach_thread;
+ pid_t child, waited;
+ uint32_t group_id;
+ u_int owner_links, pause_request, paused_targets;
+ int ack_fd, command_fd, status, thread_error, wait_errno;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+ group_build_required(&g, true);
+ group_id = g.g_id;
+ child = spawn_gated_child(&command_fd, &ack_fd);
+
+ require_test_sysctl_write(TEST_PAUSE_DESCENDANTS_ATTACH, group_id);
+ memset(&attach, 0, sizeof(attach));
+ attach.atr_leader = g.g_ids[0];
+ attach.atr_target = getpid();
+ thread_error = pthread_create(&attach_thread, NULL, attach_group_thread,
+ &attach);
+ ATF_REQUIRE_MSG(thread_error == 0,
+ "creating attach thread failed: %s", strerror(thread_error));
+ wait_test_sysctl_ack(TEST_PAUSE_DESCENDANTS_ATTACH_ACK, group_id);
+ ATF_REQUIRE_MSG(sysctl_read_u32(TEST_PAUSE_DESCENDANTS_TARGET_COUNT,
+ &paused_targets) == 0,
+ "reading paused target count failed: errno %d (%s)", errno,
+ strerror(errno));
+
+ require_test_sysctl_write(TEST_DESCENDANTS_EXIT_PID, (u_int)child);
+ ATF_REQUIRE_MSG(kill(child, SIGCONT) == 0,
+ "could not release parked child: errno %d (%s)", errno,
+ strerror(errno));
+ run_child_phase(command_fd, ack_fd, CHILD_EXIT);
+ close(command_fd);
+ wait_test_sysctl_ack(TEST_DESCENDANTS_EXIT_ACK, (u_int)child);
+ ATF_REQUIRE_MSG(sysctl_read_u32(TEST_PAUSE_DESCENDANTS_ATTACH,
+ &pause_request) == 0,
+ "reading descendants pause request failed: errno %d (%s)", errno,
+ strerror(errno));
+
+ require_test_sysctl_write(TEST_PAUSE_DESCENDANTS_ATTACH, 0);
+ thread_error = pthread_join(attach_thread, NULL);
+ ATF_REQUIRE_MSG(thread_error == 0,
+ "joining attach thread failed: %s", strerror(thread_error));
+ require_pipe_eof(ack_fd, "exit-race child");
+ close(ack_fd);
+ read_live_target_counts(&after_exit);
+
+ /*
+ * The system assigns a committed non-MUX group at commit time.
+ * So the owner holds one row-link for each member.
+ * Capture this count before teardown clears g.g_n.
+ */
+ owner_links = g.g_n;
+
+ /*
+ * Release before waitpid() reaps the child.
+ * This stops a failing implementation from hiding a stale edge
+ * behind a freed process.
+ */
+ group_teardown(&g);
+ reset_fork_test_hooks();
+ waited = waitpid(child, &status, 0);
+ wait_errno = errno;
+ read_live_target_counts(&final);
+
+ ATF_CHECK_MSG(paused_targets == 2,
+ "exit race paused with %u targets, expected owner and child",
+ paused_targets);
+ ATF_CHECK_MSG(pause_request == group_id,
+ "descendants pause ended before child exit reached hwpmc: %u/%u",
+ pause_request, group_id);
+ ATF_CHECK_MSG(attach.atr_rc == 0,
+ "exit-race attach returned rc=%d errno=%d (%s)", attach.atr_rc,
+ attach.atr_errno, strerror(attach.atr_errno));
+ /*
+ * Only the owner's edge and descriptor survive the exit-race
+ * cleanup. For group_target and process, each count is initial+1.
+ * The owner correctly holds one row-link for each member.
+ * The child's links must be gone. This test case checks for that leak.
+ * The result must be exactly initial + g.g_n.
+ * A stranding bug would instead show initial + 2*g.g_n.
+ */
+ ATF_CHECK_MSG(
+ after_exit.ltc_pmc_targets == initial.ltc_pmc_targets + owner_links &&
+ after_exit.ltc_group_targets == initial.ltc_group_targets + 1 &&
+ after_exit.ltc_target_processes ==
+ initial.ltc_target_processes + 1 &&
+ after_exit.ltc_residual_entries == initial.ltc_residual_entries &&
+ after_exit.ltc_rotation_refs == initial.ltc_rotation_refs &&
+ after_exit.ltc_run_refs == initial.ltc_run_refs,
+ "exit cleanup left stale state before release: pmc_target %ju/%ju "
+ "group_target %ju/%ju process %ju/%ju residual %ju/%ju "
+ "rotation_ref %ju/%ju run_ref %ju/%ju",
+ (uintmax_t)after_exit.ltc_pmc_targets,
+ (uintmax_t)(initial.ltc_pmc_targets + owner_links),
+ (uintmax_t)after_exit.ltc_group_targets,
+ (uintmax_t)(initial.ltc_group_targets + 1),
+ (uintmax_t)after_exit.ltc_target_processes,
+ (uintmax_t)(initial.ltc_target_processes + 1),
+ (uintmax_t)after_exit.ltc_residual_entries,
+ (uintmax_t)initial.ltc_residual_entries,
+ (uintmax_t)after_exit.ltc_rotation_refs,
+ (uintmax_t)initial.ltc_rotation_refs,
+ (uintmax_t)after_exit.ltc_run_refs,
+ (uintmax_t)initial.ltc_run_refs);
+ ATF_REQUIRE_MSG(waited == child,
+ "waitpid for exit-race child failed: errno %d (%s)", wait_errno,
+ strerror(wait_errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "exit-race child exited abnormally: status %#x", status);
+ check_live_counts_equal("exit-race cleanup did not restore baseline",
+ &final, &initial);
+}
+ATF_TC_CLEANUP(descendants_attach_race_exit, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * The explicit target is only one member of the authoritative target set.
+ * When the explicit target exits, this must not stop a running group
+ * if an inherited child remains.
+ * Keep the group evicted through the exit.
+ * This isolates the anchor lifetime from stale row links.
+ * After a successful handoff, make the group resident.
+ * Then check that the surviving child still contributes.
+ */
+ATF_TC_WITH_CLEANUP(descendants_parent_target_exits_before_child);
+ATF_TC_HEAD(descendants_parent_target_exits_before_child, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_parent_target_exits_before_child, tc)
+{
+ struct child_read_result child_read;
+ struct group g;
+ struct live_target_counts child_gone, final, initial;
+ struct row_occupancy occupancy;
+ pmc_value_t post_exit_before, post_exit_control, post_exit_work;
+ pmc_value_t control_delta, work_delta;
+ pid_t target_parent;
+ char ready, red_failure[256];
+ int ack_pipe[2], child_command_pipe[2], parent_command_pipe[2];
+ int result_pipe[2], restart_errno, restart_rc, status;
+
+ red_failure[0] = '\0';
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+ row_occupancy_fill_required(&occupancy);
+ group_build_one_required(&g, true);
+ require_test_sysctl_write(TEST_HOLD_GROUP_EVICTED, g.g_id);
+ ATF_REQUIRE_MSG(pipe(parent_command_pipe) == 0,
+ "target-parent command pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(child_command_pipe) == 0,
+ "descendant command pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(ack_pipe) == 0,
+ "target acknowledgment pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(result_pipe) == 0,
+ "descendant result pipe failed: %s", strerror(errno));
+
+ target_parent = fork();
+ ATF_REQUIRE_MSG(target_parent != -1, "target fork failed: %s",
+ strerror(errno));
+ if (target_parent == 0) {
+ close(parent_command_pipe[1]);
+ close(child_command_pipe[1]);
+ close(ack_pipe[0]);
+ close(result_pipe[0]);
+ target_parent_with_child(parent_command_pipe[0],
+ child_command_pipe[0], ack_pipe[1], result_pipe[1],
+ g.g_ids[0], PMC_ID_INVALID);
+ }
+
+ close(parent_command_pipe[0]);
+ close(child_command_pipe[0]);
+ close(ack_pipe[1]);
+ close(result_pipe[1]);
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "target readiness read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, TARGET_READY);
+
+ ATF_REQUIRE_MSG(pmc_attach(g.g_ids[0], target_parent) == 0,
+ "group attach to target parent failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_start(g.g_ids[0]) == 0,
+ "group start failed: %s", strerror(errno));
+ g.g_started = true;
+ wait_test_sysctl_ack(TEST_HOLD_GROUP_EVICTED_ACK, g.g_id);
+
+ ATF_REQUIRE_MSG(write_byte(parent_command_pipe[1], TARGET_FORK) == 0,
+ "requesting descendant fork failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "descendant fork-attempt read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, TARGET_FORKING);
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "descendant readiness read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, CHILD_READY);
+ ATF_REQUIRE_MSG(read_full(result_pipe[0], &child_read,
+ sizeof(child_read)) == (ssize_t)sizeof(child_read),
+ "descendant handle-read result was incomplete");
+ close(result_pipe[0]);
+ ATF_REQUIRE_MSG(child_read.cr_leader_rc == 0,
+ "descendant could not resolve the inherited leader before anchor "
+ "exit: errno %d (%s)", child_read.cr_leader_errno,
+ strerror(child_read.cr_leader_errno));
+
+ ATF_REQUIRE_MSG(write_byte(parent_command_pipe[1], TARGET_EXIT) == 0,
+ "releasing target parent failed: %s", strerror(errno));
+ close(parent_command_pipe[1]);
+ ATF_REQUIRE_MSG(waitpid(target_parent, &status, 0) == target_parent,
+ "wait for target parent failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "target parent exited abnormally: status %#x", status);
+
+ /*
+ * Before the fix, parent exit drops the group's only scheduling
+ * anchor. So a fresh start fails with ESRCH before the
+ * group-layer lookup.
+ * With anchor handoff, start is a successful no-op, as documented.
+ */
+ errno = 0;
+ restart_rc = pmc_start(g.g_ids[0]);
+ restart_errno = errno;
+ if (restart_rc != 0) {
+ snprintf(red_failure, sizeof(red_failure),
+ "group lost its live descendant anchor after parent exit: "
+ "pmc_start rc=%d errno=%d (%s), expected success",
+ restart_rc, restart_errno, strerror(restart_errno));
+ goto cleanup;
+ }
+
+ require_test_sysctl_write(TEST_HOLD_GROUP_RESIDENT, g.g_id);
+ require_test_sysctl_write(TEST_HOLD_GROUP_EVICTED, 0);
+ row_occupancy_release_required(&occupancy);
+ wait_test_sysctl_ack(TEST_HOLD_GROUP_RESIDENT_ACK, g.g_id);
+
+ post_exit_before = leader_value(&g);
+ run_child_phase(child_command_pipe[1], ack_pipe[0], CHILD_CONTROL);
+ post_exit_control = leader_value(&g);
+ run_child_phase(child_command_pipe[1], ack_pipe[0], CHILD_WORK);
+ post_exit_work = leader_value(&g);
+ ATF_REQUIRE_MSG(post_exit_control >= post_exit_before &&
+ post_exit_work >= post_exit_control,
+ "post-parent-exit count regressed: %ju -> %ju -> %ju",
+ (uintmax_t)post_exit_before, (uintmax_t)post_exit_control,
+ (uintmax_t)post_exit_work);
+ control_delta = post_exit_control - post_exit_before;
+ work_delta = post_exit_work - post_exit_control;
+ ATF_CHECK_MSG(work_delta >= CHILD_MIN_DELTA &&
+ work_delta / 8 > control_delta,
+ "inherited child stopped contributing after its target parent "
+ "exited: work %ju control %ju", (uintmax_t)work_delta,
+ (uintmax_t)control_delta);
+
+cleanup:
+ run_child_phase(child_command_pipe[1], ack_pipe[0], CHILD_EXIT);
+ close(child_command_pipe[1]);
+ require_pipe_eof(ack_pipe[0], "descendant exit");
+ close(ack_pipe[0]);
+ read_live_target_counts(&child_gone);
+ if (occupancy.ro_n != 0)
+ row_occupancy_release_required(&occupancy);
+ group_teardown(&g);
+ reset_fork_test_hooks();
+ read_live_target_counts(&final);
+
+ check_live_counts_equal("last target exit did not restore live counts",
+ &child_gone, &initial);
+ check_live_counts_equal("group release did not preserve live-count baseline",
+ &final, &initial);
+ if (red_failure[0] != '\0')
+ atf_tc_fail("%s", red_failure);
+}
+ATF_TC_CLEANUP(descendants_parent_target_exits_before_child, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+static void
+release_running_group_with_live_child(bool resident)
+{
+ struct child_read_result child_read;
+ struct group g;
+ struct live_target_counts after_release, final, initial;
+ struct row_occupancy occupancy;
+ pid_t target_parent;
+ char ready;
+ int ack_pipe[2], child_command_pipe[2], parent_command_pipe[2];
+ int release_errno, release_rc, result_pipe[2], status;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+ row_occupancy_init(&occupancy);
+ if (!resident)
+ row_occupancy_fill_required(&occupancy);
+ group_build_with_mux_required(&g, true, true);
+ require_test_sysctl_write(resident ? TEST_HOLD_GROUP_RESIDENT :
+ TEST_HOLD_GROUP_EVICTED, g.g_id);
+
+ ATF_REQUIRE_MSG(pipe(parent_command_pipe) == 0,
+ "target-parent command pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(child_command_pipe) == 0,
+ "descendant command pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(ack_pipe) == 0,
+ "target acknowledgment pipe failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pipe(result_pipe) == 0,
+ "descendant result pipe failed: %s", strerror(errno));
+
+ target_parent = fork();
+ ATF_REQUIRE_MSG(target_parent != -1, "target fork failed: %s",
+ strerror(errno));
+ if (target_parent == 0) {
+ close(parent_command_pipe[1]);
+ close(child_command_pipe[1]);
+ close(ack_pipe[0]);
+ close(result_pipe[0]);
+ target_parent_with_child(parent_command_pipe[0],
+ child_command_pipe[0], ack_pipe[1], result_pipe[1],
+ g.g_ids[0], g.g_ids[1]);
+ }
+
+ close(parent_command_pipe[0]);
+ close(child_command_pipe[0]);
+ close(ack_pipe[1]);
+ close(result_pipe[1]);
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "target readiness read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, TARGET_READY);
+ ATF_REQUIRE_MSG(pmc_attach(g.g_ids[0], target_parent) == 0,
+ "group attach failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(pmc_start(g.g_ids[0]) == 0,
+ "group start failed: %s", strerror(errno));
+ g.g_started = true;
+ wait_test_sysctl_ack(resident ? TEST_HOLD_GROUP_RESIDENT_ACK :
+ TEST_HOLD_GROUP_EVICTED_ACK, g.g_id);
+
+ ATF_REQUIRE_MSG(write_byte(parent_command_pipe[1], TARGET_FORK) == 0,
+ "requesting descendant fork failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "descendant fork-attempt read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, TARGET_FORKING);
+ ATF_REQUIRE_MSG(read_byte(ack_pipe[0], &ready) == 0,
+ "descendant readiness read failed: %s", strerror(errno));
+ ATF_REQUIRE_EQ(ready, CHILD_READY);
+ ATF_REQUIRE_MSG(read_full(result_pipe[0], &child_read,
+ sizeof(child_read)) == (ssize_t)sizeof(child_read),
+ "descendant handle-read result was incomplete");
+ close(result_pipe[0]);
+ ATF_REQUIRE_MSG(child_read.cr_leader_rc == 0,
+ "live child could not read leader before release: errno %d (%s)",
+ child_read.cr_leader_errno, strerror(child_read.cr_leader_errno));
+ ATF_REQUIRE_MSG(child_read.cr_sibling_rc == 0,
+ "live child could not read sibling before release: errno %d (%s)",
+ child_read.cr_sibling_errno, strerror(child_read.cr_sibling_errno));
+
+ errno = 0;
+ release_rc = pmc_release(g.g_ids[0]);
+ release_errno = errno;
+ if (release_rc == 0)
+ group_init(&g);
+ read_live_target_counts(&after_release);
+
+ run_child_phase(child_command_pipe[1], ack_pipe[0], CHILD_EXIT);
+ close(child_command_pipe[1]);
+ ATF_REQUIRE_MSG(write_byte(parent_command_pipe[1], TARGET_EXIT) == 0,
+ "releasing target parent failed: %s", strerror(errno));
+ close(parent_command_pipe[1]);
+ ATF_REQUIRE_MSG(waitpid(target_parent, &status, 0) == target_parent,
+ "wait for target parent failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "target parent exited abnormally: status %#x", status);
+ require_pipe_eof(ack_pipe[0], "release descendants");
+ close(ack_pipe[0]);
+ if (occupancy.ro_n != 0)
+ row_occupancy_release_required(&occupancy);
+ reset_fork_test_hooks();
+ if (release_rc != 0)
+ group_teardown(&g);
+ read_live_target_counts(&final);
+
+ ATF_REQUIRE_MSG(release_rc == 0,
+ "direct release while %s returned rc=%d errno=%d (%s)",
+ resident ? "resident" : "evicted", release_rc, release_errno,
+ strerror(release_errno));
+ check_live_counts_equal("release with live child retained group state",
+ &after_release, &initial);
+ check_live_counts_equal("release with live child cleanup did not restore "
+ "baseline", &final, &initial);
+}
+
+ATF_TC_WITH_CLEANUP(descendants_release_running_resident_with_live_child);
+ATF_TC_HEAD(descendants_release_running_resident_with_live_child, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_release_running_resident_with_live_child, tc)
+{
+
+ release_running_group_with_live_child(true);
+}
+ATF_TC_CLEANUP(descendants_release_running_resident_with_live_child, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+ATF_TC_WITH_CLEANUP(descendants_release_running_evicted_with_live_child);
+ATF_TC_HEAD(descendants_release_running_evicted_with_live_child, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_release_running_evicted_with_live_child, tc)
+{
+
+ release_running_group_with_live_child(false);
+}
+ATF_TC_CLEANUP(descendants_release_running_evicted_with_live_child, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/*
+ * A descendants attach is one group-level transaction over the
+ * existing process tree.
+ * Park the child before the attach.
+ * Isolate a no-work control phase and a child-only work phase.
+ * Every member must show a large work-phase gain above the control noise.
+ */
+ATF_TC_WITHOUT_HEAD(descendants_existing_tree_attach_atomic);
+ATF_TC_BODY(descendants_existing_tree_attach_atomic, tc)
+{
+ struct group g;
+ pmc_value_t before[2], control[2], live_child[2], after_exit[2];
+ pmc_value_t control_delta, work_delta;
+ pid_t child;
+ int ack_fd, attach_errno, command_fd, error, status;
+ u_int i;
+
+ require_hwpmc();
+ group_build_required(&g, true);
+ child = spawn_gated_child(&command_fd, &ack_fd);
+
+ errno = 0;
+ error = pmc_attach(g.g_ids[0], getpid());
+ attach_errno = errno;
+ ATF_REQUIRE_MSG(kill(child, SIGCONT) == 0,
+ "could not release parked child: %s", strerror(errno));
+ ATF_REQUIRE_MSG(error == 0,
+ "existing-descendants attach failed at pmc_attach: errno %d (%s)",
+ attach_errno, strerror(attach_errno));
+ ATF_REQUIRE_MSG(pmc_start(g.g_ids[0]) == 0,
+ "group start failed: %s", strerror(errno));
+ g.g_started = true;
+
+ group_snapshot(&g, before);
+ run_child_phase(command_fd, ack_fd, CHILD_CONTROL);
+ group_snapshot(&g, control);
+ run_child_phase(command_fd, ack_fd, CHILD_WORK);
+ group_snapshot(&g, live_child);
+ ATF_REQUIRE_MSG(waitpid(child, &status, WNOHANG) == 0,
+ "child exited before the live-child snapshot");
+ run_child_phase(command_fd, ack_fd, CHILD_EXIT);
+ close(command_fd);
+ close(ack_fd);
+ ATF_REQUIRE_MSG(waitpid(child, &status, 0) == child,
+ "waitpid failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "child exited abnormally: status %#x", status);
+ group_snapshot(&g, after_exit);
+
+ for (i = 0; i < g.g_n; i++) {
+ ATF_REQUIRE_MSG(control[i] >= before[i],
+ "member %u regressed during control: %ju -> %ju", i,
+ (uintmax_t)before[i], (uintmax_t)control[i]);
+ ATF_REQUIRE_MSG(live_child[i] >= control[i],
+ "member %u regressed during child work: %ju -> %ju", i,
+ (uintmax_t)control[i], (uintmax_t)live_child[i]);
+ ATF_REQUIRE_MSG(after_exit[i] >= live_child[i],
+ "member %u regressed across child exit: %ju -> %ju", i,
+ (uintmax_t)live_child[i], (uintmax_t)after_exit[i]);
+ control_delta = control[i] - before[i];
+ work_delta = live_child[i] - control[i];
+ printf("member %u: control=%ju child-work=%ju post-exit=%ju\n", i,
+ (uintmax_t)control_delta, (uintmax_t)work_delta,
+ (uintmax_t)(after_exit[i] - live_child[i]));
+ ATF_CHECK_MSG(work_delta >= CHILD_MIN_DELTA &&
+ work_delta / 8 > control_delta,
+ "member %u child-work delta %ju is not substantially above "
+ "control delta %ju", i, (uintmax_t)work_delta,
+ (uintmax_t)control_delta);
+ }
+
+ ATF_REQUIRE_MSG(pmc_stop(g.g_ids[0]) == 0,
+ "group stop failed: %s", strerror(errno));
+ g.g_started = false;
+ ATF_REQUIRE_MSG(pmc_release(g.g_ids[0]) == 0,
+ "group release failed: %s", strerror(errno));
+ group_init(&g);
+}
+
+/*
+ * Repeat the existing-tree transaction.
+ * This time, no hardware row is available at commit or at start.
+ * The attachment must publish the full target set while it is deferred.
+ * Every member must count the parked child once the group is admitted.
+ */
+ATF_TC_WITH_CLEANUP(descendants_existing_tree_attach_deferred);
+ATF_TC_HEAD(descendants_existing_tree_attach_deferred, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(descendants_existing_tree_attach_deferred, tc)
+{
+ struct group g;
+ struct live_target_counts final, initial;
+ struct row_occupancy occupancy;
+ pmc_value_t after_exit[2], before[2], control[2], live_child[2];
+ pmc_value_t control_delta, work_delta;
+ pid_t child;
+ int ack_fd, attach_errno, attach_rc, command_fd, status;
+ u_int i;
+
+ require_hwpmc();
+ require_hwpmc_test_support();
+ reset_fork_test_hooks();
+ read_live_target_counts(&initial);
+ row_occupancy_fill_required(&occupancy);
+ group_build_with_mux_required(&g, true, true);
+ require_test_sysctl_write(TEST_HOLD_GROUP_EVICTED, g.g_id);
+ child = spawn_gated_child(&command_fd, &ack_fd);
+
+ errno = 0;
+ attach_rc = pmc_attach(g.g_ids[0], getpid());
+ attach_errno = errno;
+ if (attach_rc != 0) {
+ ATF_REQUIRE_MSG(kill(child, SIGCONT) == 0,
+ "could not release child after attach failure: %s",
+ strerror(errno));
+ run_child_phase(command_fd, ack_fd, CHILD_EXIT);
+ close(command_fd);
+ close(ack_fd);
+ ATF_REQUIRE_MSG(waitpid(child, &status, 0) == child,
+ "wait after attach failure failed: %s", strerror(errno));
+ row_occupancy_release_required(&occupancy);
+ group_teardown(&g);
+ reset_fork_test_hooks();
+ atf_tc_fail("deferred existing-descendants attach returned "
+ "rc=%d errno=%d (%s), expected success", attach_rc,
+ attach_errno, strerror(attach_errno));
+ }
+
+ ATF_REQUIRE_MSG(pmc_start(g.g_ids[0]) == 0,
+ "deferred group start failed: errno %d (%s)", errno,
+ strerror(errno));
+ g.g_started = true;
+ wait_test_sysctl_ack(TEST_HOLD_GROUP_EVICTED_ACK, g.g_id);
+ require_test_sysctl_write(TEST_HOLD_GROUP_RESIDENT, g.g_id);
+ require_test_sysctl_write(TEST_HOLD_GROUP_EVICTED, 0);
+ row_occupancy_release_required(&occupancy);
+ wait_test_sysctl_ack(TEST_HOLD_GROUP_RESIDENT_ACK, g.g_id);
+
+ ATF_REQUIRE_MSG(kill(child, SIGCONT) == 0,
+ "could not release parked child: %s", strerror(errno));
+ group_snapshot(&g, before);
+ run_child_phase(command_fd, ack_fd, CHILD_CONTROL);
+ group_snapshot(&g, control);
+ run_child_phase(command_fd, ack_fd, CHILD_WORK);
+ group_snapshot(&g, live_child);
+ ATF_REQUIRE_MSG(waitpid(child, &status, WNOHANG) == 0,
+ "child exited before the deferred live-child snapshot");
+ run_child_phase(command_fd, ack_fd, CHILD_EXIT);
+ close(command_fd);
+ close(ack_fd);
+ ATF_REQUIRE_MSG(waitpid(child, &status, 0) == child,
+ "waitpid failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "child exited abnormally: status %#x", status);
+ group_snapshot(&g, after_exit);
+
+ for (i = 0; i < g.g_n; i++) {
+ ATF_REQUIRE_MSG(control[i] >= before[i],
+ "deferred member %u regressed during control: %ju -> %ju",
+ i, (uintmax_t)before[i], (uintmax_t)control[i]);
+ ATF_REQUIRE_MSG(live_child[i] >= control[i],
+ "deferred member %u regressed during child work: %ju -> %ju",
+ i, (uintmax_t)control[i], (uintmax_t)live_child[i]);
+ ATF_REQUIRE_MSG(after_exit[i] >= live_child[i],
+ "deferred member %u regressed across child exit: %ju -> %ju",
+ i, (uintmax_t)live_child[i], (uintmax_t)after_exit[i]);
+ control_delta = control[i] - before[i];
+ work_delta = live_child[i] - control[i];
+ ATF_CHECK_MSG(work_delta >= CHILD_MIN_DELTA &&
+ work_delta / 8 > control_delta,
+ "deferred member %u child-work delta %ju is not "
+ "substantially above control delta %ju", i,
+ (uintmax_t)work_delta, (uintmax_t)control_delta);
+ }
+
+ ATF_REQUIRE_MSG(pmc_stop(g.g_ids[0]) == 0,
+ "deferred group stop failed: %s", strerror(errno));
+ g.g_started = false;
+ ATF_REQUIRE_MSG(pmc_release(g.g_ids[0]) == 0,
+ "deferred group release failed: %s", strerror(errno));
+ group_init(&g);
+ reset_fork_test_hooks();
+ read_live_target_counts(&final);
+ check_live_counts_equal("deferred attach cleanup did not restore baseline",
+ &final, &initial);
+}
+ATF_TC_CLEANUP(descendants_existing_tree_attach_deferred, tc)
+{
+
+ reset_fork_test_hooks();
+}
+
+/* A child exit mid-run must never lower the totals. */
+ATF_TC_WITHOUT_HEAD(child_exit_does_not_regress);
+ATF_TC_BODY(child_exit_does_not_regress, tc)
+{
+ struct group g;
+ pmc_value_t before, after;
+ int i;
+
+ require_hwpmc();
+ ATF_REQUIRE_MSG(group_build(&g, true) == 0, "commit failed: %s",
+ strerror(errno));
+ ATF_REQUIRE(pmc_attach(g.g_ids[0], getpid()) == 0);
+ ATF_REQUIRE(pmc_start(g.g_ids[0]) == 0);
+ g.g_started = true;
+
+ before = 0;
+ for (i = 0; i < 3; i++) {
+ fork_workload();
+ after = leader_value(&g);
+ ATF_CHECK_MSG(after >= before,
+ "totals fell from %ju to %ju across a child exit",
+ (uintmax_t)before, (uintmax_t)after);
+ before = after;
+ }
+ group_teardown(&g);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+
+ ATF_TP_ADD_TC(tp, descendants_counted);
+ ATF_TP_ADD_TC(tp, descendants_whole_group);
+ ATF_TP_ADD_TC(tp, descendants_child_reads_whole_group);
+ ATF_TP_ADD_TC(tp, descendants_fork_inherit_alloc_failure_atomic);
+ ATF_TP_ADD_TC(tp, descendants_fork_inherit_link_alloc_failure_atomic);
+ ATF_TP_ADD_TC(tp, descendants_fork_miss_has_no_false_detach);
+ ATF_TP_ADD_TC(tp, descendants_inherited_parent_fork_miss_is_identified);
+ ATF_TP_ADD_TC(tp, descendants_attach_alloc_rollback);
+ ATF_TP_ADD_TC(tp, descendants_attach_authorization_rollback);
+ ATF_TP_ADD_TC(tp, descendants_attach_race_fork);
+ ATF_TP_ADD_TC(tp, descendants_attach_race_exit);
+ ATF_TP_ADD_TC(tp, descendants_parent_target_exits_before_child);
+ ATF_TP_ADD_TC(tp, descendants_release_running_resident_with_live_child);
+ ATF_TP_ADD_TC(tp, descendants_release_running_evicted_with_live_child);
+ ATF_TP_ADD_TC(tp, descendants_existing_tree_attach_atomic);
+ ATF_TP_ADD_TC(tp, descendants_existing_tree_attach_deferred);
+ ATF_TP_ADD_TC(tp, child_exit_does_not_regress);
+
+ return (atf_no_error());
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Oct 18, 8:50 AM (44 m, 25 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40987893
Default Alt Text
D59372.id188478.diff (100 KB)
Attached To
Mode
D59372: tests: cover PMU group inheritance across fork
Attached
Detach File
Event Timeline
Log In to Comment