Top frame: __pthread_kill_implementation [libc.so.6]
highpackage: yakuakesource: yakuakescore: 98reports: 1patch attempt readypatched: 2026-07-09 15:33 UTCvalidation: ready
Attempt Summary
Patch proposal created locally. Review it and submit it upstream if it looks correct.
Suggested subject
Focus raised sessions through their active terminal
Commit message.
When raising a session, focus the session’s tracked active terminal widget directly instead of asking Qt to rediscover the focused child through `QWidget::focusWidget()`. Yakuake already tracks the active terminal for each session, and using that stored terminal widget keeps the intended focus behavior while avoiding a fragile focus-chain lookup during session switching.
Issue connection.
Fixer observed a Yakuake SIGSEGV in a coredump from `/usr/bin/yakuake`; the stack entered `QWidget::focusWidget()` while Qt was processing focus events. I did not independently reproduce that crash.
The cautious code-level connection is that `SessionStack::raiseSession()` used `session->widget()->focusWidget()` immediately after changing the current stacked widget. The focus chain is Qt’s record of which child widget currently owns focus inside a parent widget. In this path Yakuake does not need Qt to rediscover that child: the `Session` already records its active `Terminal`, and `Terminal::terminalWidget()` is a guarded pointer to the embedded terminal widget.
The patch changes `raiseSession()` to fetch the active terminal via `session->getTerminal(session->activeTerminalId())` and focus that terminal widget if it still exists. This addresses the review finding by avoiding the `focusWidget()` call entirely in this path, rather than just reducing duplicate calls. The expected effect is to preserve session-raise focus behavior while avoiding the crash-adjacent Qt focus-chain lookup.
Diff Excerpt
diff --git a/app/sessionstack.cpp b/app/sessionstack.cpp
index 0850720..2de2d28 100644
--- a/app/sessionstack.cpp
+++ b/app/sessionstack.cpp
@@ -110,8 +110,12 @@ void SessionStack::raiseSession(int sessionId)
setCurrentWidget(session->widget());
- if (session->widget()->focusWidget())
- session->widget()->focusWidget()->setFocus();
+ Terminal *terminal = session->getTerminal(session->activeTerminalId());
+ if (terminal) {
+ QWidget *terminalWidget = terminal->terminalWidget();
+ if (terminalWidget)
+ terminalWidget->setFocus();
+ }
if (session->hasTerminalsWithKeyboardInputDisabled())
m_visualEventOverlay->show();
Full published attempt: /issues/019f478f-3ab9-7aa2-af9f-76ffb768fd8b. Issue JSON: /v1/issues/019f478f-3ab9-7aa2-af9f-76ffb768fd8b
5.85% of sampled CPU in postgres went through polygonize_noded_linked_polygon (/home/<user>/proj/osgeo/h3-pg/build/h3_postgis/h3_postgis.so)
mediumpackage: postgresql-14source: postgresql-14score: 79reports: 1patch attempt readypatched: 2026-05-28 12:16 UTCvalidation: readyharvest: needs reviewlimited validation
Attempt Summary
Patch proposal created locally. Review it and submit it upstream if it looks correct.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Add independent reproduction or stronger project/runtime validation before upstream submission.
Suggested subject
h3_postgis: skip disjoint segment checks while polygonizing
Commit message.
`polygonize_noded_linked_polygon` builds a noded boundary graph, meaning boundary segments are split wherever they cross before faces are rebuilt. The graph builder compared every segment pair with full intersection math, even when their latitude/longitude ranges could not overlap.
Cache conservative bounds on each `NodedSegment` and skip impossible segment pairs before the existing intersection and collinear-overlap checks. This follows the saved plan, the local PostgreSQL allocation conventions (`palloc`/`pfree`) and H3 `VertexGraph` helper flow, and is intended for upstream `main` rather than a Debian-local workaround.
Issue connection.
Fixer observed a perf trace where `postgres` spent 5.85% of sampled CPU in `h3_postgis.so` under `polygonize_noded_linked_polygon`. This was observed by Fixer and not independently reproduced; the evidence is profiler-only, with no SQL reproducer or user-visible failure.
The cautious code-level inference is that the noded polygonizer can spend avoidable time in its all-pairs segment loop. Before this patch, `graph_add_noded_linked_polygon_edges()` called the detailed crossing and collinear-overlap routines for every pair of boundary segments, including pairs whose bounding boxes are disjoint.
The change adds cached min/max latitude and longitude bounds to each `NodedSegment`, plus a conservative `segments_bounds_overlap()` guard before the detailed pair checks. I added a short comment documenting the invariant: the reject must remain conservative because missed overlaps would drop graph nodes.
The expected effect is to reduce CPU spent in the observed polygonizer path for large split-boundary inputs while preserving the existing topology behavior for any segment pair that could actually overlap.
Diff Excerpt
diff --git a/h3_postgis/src/wkb_regions.c b/h3_postgis/src/wkb_regions.c
index 0dfb3f2..6dc78b4 100644
--- a/h3_postgis/src/wkb_regions.c
+++ b/h3_postgis/src/wkb_regions.c
@@ -62,6 +62,10 @@ typedef struct
{
LatLng from;
LatLng to;
+ double minLat;
+ double maxLat;
+ double minLng;
+ double maxLng;
int splitCount;
int splitCap;
double *splitTs;
@@ -236,6 +240,9 @@ static bool
static bool
segment_collinear_overlap_ts(const NodedSegment * a, const NodedSegment * b, double *aStart, double *aEnd, double *bStart, double *bEnd);
+static bool
+ segments_bounds_overlap(const NodedSegment * a, const NodedSegment * b);
+
static double
segment_project_t(const NodedSegment * segment, const LatLng * latlng);
@@ -1647,6 +1654,10 @@ collect_linked_polygon_segments(const LinkedGeoPolygon * multiPolygon, NodedSegm
segment->from = cur->vertex;
segment->to = next->vertex;
+ segment->minLat = fmin(segment->from.lat, segment->to.lat);
+ segment->maxLat = fmax(segment->from.lat, segment->to.lat);
+ segment->minLng = fmin(segment->from.lng, segment->to.lng);
+ segment->maxLng = fmax(segment->from.lng, segment->to.lng);
segment->splitCap = segmentCount + 2;
segment->splitCount = 0;
segment->splitTs = palloc(segment->splitCap * sizeof(*segment->splitTs));
@@ -1778,6 +1789,16 @@ segment_collinear_overlap_ts(const NodedSegment * a, const NodedSegment * b, dou
return true;
}
+bool
+segments_bounds_overlap(const NodedSegment * a, const NodedSegment * b)
+{
+ /* Keep this reject conservative: missed overlaps would drop graph nodes. */
+ return a->minLat <= b->maxLat + DBL_EPSILON
+ && b->minLat <= a->maxLat + DBL_EPSILON
+ && a->minLng <= b->maxLng + DBL_EPSILON
+ && b->minLng <= a->maxLng + DBL_EPSILON;
+}
+
double
segment_project_t(const NodedSegment * segment, const LatLng * latlng)
{
@@ -1832,6 +1853,9 @@ graph_add_noded_linked_polygon_edges(VertexGraph * graph, const LinkedGeoPolygon
double overlapStartJ;
double overlapEndJ;
+ if (!segments_bounds_overlap(&segments[i], &segments[j]))
+ continue;
+
if (segment_intersection_t(&segments[i], &segments[j], &ti, &tj))
{
segment_add_split_t(&segments[i], ti);
Full published attempt: /issues/019e6e8e-eb95-78e1-8a7a-559701878977. Issue JSON: /v1/issues/019e6e8e-eb95-78e1-8a7a-559701878977
smtpd is stuck in a likely unclassified userspace loop: 21.02% of sampled CPU passed through __memcg_slab_post_alloc_hook, with repeated thread backtraces show 1 thread(s) around 0x00007f059d1bc7d2 in ?? () from target:<path>.
highpackage: postfixsource: postfixscore: 106reports: 2patch attempt readypatched: 2026-05-26 18:48 UTCvalidation: readyupstream: review
Attempt Summary
smtpd likely remains stuck in an unclassified userspace loop. A diagnosis report and patch proposal were created locally.
Suggested subject
smtpd: disconnect after overlong SMTP commands
Commit message.
Change the normal SMTP server command reader so an overlong command is rejected and the session is closed instead of draining all excess input first. The command path still uses Postfix’s `smtp_get()` and `VSTREAM` helpers. SASL continuation reads keep the old drain behavior because those are caller-bounded and recoverable within AUTH.
Issue connection.
Fixer observed `smtpd` in `smtp_get()` through `vstring_get_flags_bound()` and `timed_read()`, with samples in `gettimeofday()` and kernel allocation paths. The likely root cause is that `smtpd_chat_query()` currently expands to `smtpd_chat_query_limit(..., var_line_limit)`, and that helper uses `SMTP_GET_FLAG_SKIP`, which keeps reading until newline after the command has already exceeded `line_length_limit`.
I reproduced the bounded helper-level failure signal with a temporary program linked against Postfix’s own libraries: `smtp_get_noexcept(..., SMTP_GET_FLAG_NONE)` returned immediately after the bound, while `smtp_get_noexcept(..., SMTP_GET_FLAG_SKIP)` timed out waiting for newline on an open pipe. That matches the collected stack’s read path.
The patch replaces the macro with an `int smtpd_chat_query()` that does not use `SMTP_GET_FLAG_SKIP`. The main SMTP command loop now treats a false return as a protocol error, replies `500 5.5.2 Error: command line too long`, and breaks the session. `smtpd_chat_query_limit()` still drains excess input for SASL continuation lines.
Security impact analysis: this is a resource-use hardening change for malformed SMTP input. It does not change authentication success/failure semantics, credential handling, authorization decisions, cryptography, sandboxing, or permissions. The expected effect is to bound work for overlong SMTP commands and disconnect instead of continuing to read client-controlled excess input.
Diff Excerpt
diff --git a/src/smtpd/smtpd.c b/src/smtpd/smtpd.c
index 5cf2478..e3f0636 100644
--- a/src/smtpd/smtpd.c
+++ b/src/smtpd/smtpd.c
@@ -6041,7 +6041,12 @@ static void smtpd_proto(SMTPD_STATE *state)
break;
}
watchdog_pat();
- smtpd_chat_query(state);
+ if (!smtpd_chat_query(state)) {
+ state->error_mask |= MAIL_ERROR_PROTOCOL;
+ smtpd_chat_reply(state,
+ "500 5.5.2 Error: command line too long");
+ break;
+ }
if (IS_BARE_LF_REPLY_REJECT(smtp_got_bare_lf)) {
log_whatsup(state, "reject", "bare <LF> received");
state->error_mask |= MAIL_ERROR_PROTOCOL;
diff --git a/src/smtpd/smtpd_chat.c b/src/smtpd/smtpd_chat.c
index 32ec770..ce764b0 100644
--- a/src/smtpd/smtpd_chat.c
+++ b/src/smtpd/smtpd_chat.c
@@ -13,7 +13,7 @@
/* SMTPD_STATE *state;
/* int limit;
/*
-/* void smtpd_chat_query(state)
+/* int smtpd_chat_query(state)
/* SMTPD_STATE *state;
/*
/* void smtpd_chat_reply(state, format, ...)
@@ -32,12 +32,15 @@
/* smtpd_chat_pre_jail_init() performs one-time initialization.
/*
/* smtpd_chat_query_limit() reads a line from the client that is
-/* at most "limit" bytes long. A copy is appended to the SMTP
-/* transaction log. The return value is non-zero for a complete
-/* line or else zero if the length limit was exceeded.
+/* at most "limit" bytes long, and skips over excess input.
+/* A copy is appended to the SMTP transaction log. The return
+/* value is non-zero for a complete line or else zero if the
+/* length limit was exceeded.
/*
/* smtpd_chat_query() receives a client request and appends a copy
-/* to the SMTP transaction log.
+/* to the SMTP transaction log. It does not skip over excess input.
+/* The return value is non-zero for a complete line or else zero
+/* if the length limit was exceeded.
/*
/* smtpd_chat_reply() formats a server reply, sends it to the
/* client, and appends a copy to the SMTP transaction log.
@@ -173,18 +176,13 @@ static void smtp_chat_append(SMTPD_STATE *state, char *direction,
myfree(line);
}
-/* smtpd_chat_query - receive and record an SMTP request */
+/* smtpd_chat_query_limit_flags - receive and record an SMTP request */
-int smtpd_chat_query_limit(SMTPD_STATE *state, int limit)
+static int smtpd_chat_query_limit_flags(SMTPD_STATE *state, int limit, int flags)
{
int last_char;
- /*
- * We can't parse or store input that exceeds var_li
[truncated]
Full published attempt: /issues/019e23ec-83a0-7823-9092-6f1fcbeea250. Issue JSON: /v1/issues/019e23ec-83a0-7823-9092-6f1fcbeea250
dockerd is stuck in a likely socket churn loop: 100.00% of sampled CPU passed through bpf_lsm_file_permission, with repeated thread backtraces show 1 thread(s) around 0x000056124fea7023 in ?? () and 1 thread(s) around 0x000056124fea7023 in ?? ().
highpackage: docker.iosource: docker.ioscore: 106reports: 2patch attempt readypatched: 2026-05-15 21:49 UTCvalidation: readyupstream: review
Attempt Summary
dockerd likely remains stuck in a socket churn loop. A diagnosis report and patch proposal were created locally.
Original harvest blockers
These blockers remain on the preserved local diff, but the upstream review above is the current handoff.
Suggested subject
libcontainerd: throttle event stream restarts
Commit message.
`dockerd` restarts the containerd task event stream after subscription errors. When containerd is already serving, the old code immediately spawned a replacement goroutine, so repeated transient subscription failures could retry without any pause. Keep the restart in the same goroutine and wait briefly before resubscribing.
Issue connection.
Fixer observed `dockerd` running with `--containerd=/run/containerd/containerd.sock`; the collected perf/strace/proc evidence showed a sleeping daemon with many futex waits and classified the profile as socket churn. This failure was observed by Fixer and not independently reproduced.
The cautious code-level connection is the libcontainerd event stream, which is the daemon’s subscription to containerd task events. On a non-canceled subscription error, `processEventStream` waited for containerd readiness and then immediately started another `processEventStream` goroutine. If the readiness probe succeeds while subscriptions keep failing transiently, that path can repeatedly resubscribe to the containerd socket without a bounded delay.
I changed that path to return through a one-shot helper and retry in the same goroutine after a 100 ms context-aware delay. I also added a short comment explaining the invariant: a successful readiness probe must not allow immediate resubscribe churn. The code follows nearby timer patterns and existing `containerd/log` usage; syscall wrappers such as `engine/internal/unix_noeintr` and netlink helpers such as `engine/internal/nlwrap` do not fit this gRPC/containerd subscription path.
The expected effect is to reduce rapid containerd socket retry churn after transient event subscription failures while preserving cancellation and normal restart behavior.
Diff Excerpt
diff --git a/engine/libcontainerd/remote/client.go b/engine/libcontainerd/remote/client.go
index 3d3ec63..c07c000 100644
--- a/engine/libcontainerd/remote/client.go
+++ b/engine/libcontainerd/remote/client.go
@@ -607,6 +607,28 @@ func (c *client) waitServe(ctx context.Context) bool {
}
func (c *client) processEventStream(ctx context.Context, ns string) {
+ for c.processEventStreamOnce(ctx, ns) {
+ if !waitEventStreamRestart(ctx, eventStreamRestartDelay) {
+ return
+ }
+ }
+}
+
+const eventStreamRestartDelay = 100 * time.Millisecond
+
+func waitEventStreamRestart(ctx context.Context, delay time.Duration) bool {
+ timer := time.NewTimer(delay)
+ defer timer.Stop()
+
+ select {
+ case <-ctx.Done():
+ return false
+ case <-timer.C:
+ return true
+ }
+}
+
+func (c *client) processEventStreamOnce(ctx context.Context, ns string) bool {
// Create a new context specifically for this subscription.
// The context must be cancelled to cancel the subscription.
// In cases where we have to restart event stream processing,
@@ -629,13 +651,15 @@ func (c *client) processEventStream(ctx context.Context, ns string) {
c.logger.WithError(err).Error("Failed to get event")
c.logger.Info("Waiting for containerd to be ready to restart event processing")
if c.waitServe(ctx) {
- go c.processEventStream(ctx, ns)
- return
+ // A failed subscription can be followed by an immediate
+ // successful readiness probe. Delay the resubscribe so
+ // transient errors cannot spin on containerd's socket.
+ return true
}
}
c.logger.WithError(ctx.Err()).Info("stopping event stream following graceful shutdown")
}
- return
+ return false
case ev := <-eventStream:
if ev.Event == nil {
c.logger.WithField("event", ev).Warn("invalid event")
diff --git a/engine/libcontainerd/remote/client_test.go b/engine/libcontainerd/remote/client_test.go
new file mode 100644
index 0000000..ee65c80
--- /dev/null
+++ b/engine/libcontainerd/remote/client_test.go
@@ -0,0 +1,26 @@
+package remote
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+)
+
+func TestWaitEventStreamRestartWaitsForDelay(t *testing.T) {
+ const delay = 20 * time.Millisecond
+
+ start := time.Now()
+ assert.Check(t, waitEventStreamRestart(context.Background(), delay))
+ assert.Check(t, time.Since(start) >= delay)
+}
+
+func TestW
[truncated]
Full published attempt: /issues/019dfd63-6d80-7570-856f-df901ff2167f. Issue JSON: /v1/issues/019dfd63-6d80-7570-856f-df901ff2167f
sshd-auth is stuck in a likely timer churn loop: 100.00% of sampled CPU passed through apparmor_socket_recvmsg, with repeated thread backtraces show 1 thread(s) around 0x00007fa460a9a7d2 in ?? () from target:<path>.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-15 21:38 UTCvalidation: readyupstream: closed-unmerged
Attempt Summary
sshd-auth likely remains stuck in a timer churn loop. A diagnosis report and patch proposal were created locally.
Suggested subject
auth2: do not amplify failed-auth delay after slow authentication
Commit message.
`auth2.c` adds a small per-user failed-authentication delay before sending an authentication failure. If authentication has already taken longer than that requested delay, return immediately instead of doubling the delay until it exceeds the elapsed time.
This keeps the intended minimum-delay behavior without adding seconds of extra sleep after a slow backend or monitor round trip.
Issue connection.
Fixer observed `sshd-auth` with trace evidence: `/proc` showed the process sleeping in `unix_stream_read_generic`, perf attributed sampled time to socket receive handling, and strace showed a `read(3, ...)` taking about 2.055919s followed by `clock_nanosleep(...)` for about 2.025614s. This failure was observed by Fixer and not independently reproduced as a live sshd session.
The cautious code-level cause is `ensure_minimum_time_since()` in `auth2.c`: after failed non-`none` authentication, it doubled a requested 5 to 9.2ms per-user delay until it exceeded elapsed authentication time. If authentication had already taken about two seconds, that could compute another multi-second sleep.
The change makes `ensure_minimum_time_since()` return when elapsed time already satisfies the requested minimum delay, and only sleep for the remaining difference when authentication was faster than the floor.
The expected effect is to avoid amplifying slow failed-authentication work into an additional long timer sleep while keeping the intended minimum failed-auth delay.
Diff Excerpt
diff --git a/auth2.c b/auth2.c
index 80f766e..58f3a11 100644
--- a/auth2.c
+++ b/auth2.c
@@ -224,7 +224,6 @@ input_service_request(int type, uint32_t seq, struct ssh *ssh)
}
#define MIN_FAIL_DELAY_SECONDS 0.005
-#define MAX_FAIL_DELAY_SECONDS 5.0
static double
user_specific_delay(const char *user)
{
@@ -248,22 +247,19 @@ static void
ensure_minimum_time_since(double start, double seconds)
{
struct timespec ts;
- double elapsed = monotime_double() - start, req = seconds, remain;
+ double elapsed = monotime_double() - start, remain;
- if (elapsed > MAX_FAIL_DELAY_SECONDS) {
- debug3_f("elapsed %0.3lfms exceeded the max delay "
- "requested %0.3lfms)", elapsed*1000, req*1000);
+ if (elapsed >= seconds) {
+ debug3_f("elapsed %0.3lfms already meets requested "
+ "delay %0.3lfms", elapsed*1000, seconds*1000);
return;
}
- /* if we've already passed the requested time, scale up */
- while ((remain = seconds - elapsed) < 0.0)
- seconds *= 2;
-
+ remain = seconds - elapsed;
ts.tv_sec = remain;
ts.tv_nsec = (remain - ts.tv_sec) * 1000000000;
- debug3_f("elapsed %0.3lfms, delaying %0.3lfms (requested %0.3lfms)",
- elapsed*1000, remain*1000, req*1000);
+ debug3_f("elapsed %0.3lfms, delaying %0.3lfms",
+ elapsed*1000, remain*1000);
nanosleep(&ts, NULL);
}
Full published attempt: /issues/019dfcf9-7385-7c01-ba59-9fce98cd0a8f. Issue JSON: /v1/issues/019dfcf9-7385-7c01-ba59-9fce98cd0a8f
sshd-auth is stuck in a likely timer churn loop: 100.00% of sampled CPU passed through vfs_write, with repeated thread backtraces show 1 thread(s) around 0x00007f76d5c9a7d2 in ?? () from target:<path>.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-15 18:00 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: closed-unmerged
Attempt Summary
sshd-auth likely remains stuck in a timer churn loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Rerun in an environment where the blocked runtime or test validation can complete before upstream submission.
- Do not open another source-path-family PR from this retained diff; gather new reproduction evidence or a materially distinct fix before rerunning upstream.
Suggested subject
auth2: cap failed auth delay backoff
Commit message.
Failed user authentication uses a timing pad to reduce username and backend timing leaks. When backend work already exceeds the requested pad, cap the scaled delay target at `MAX_FAIL_DELAY_SECONDS` so slow failed-auth paths do not add avoidable sleep beyond the intended maximum.
Issue connection.
Fixer observed an `sshd-auth` pre-authentication child with strace samples reaching `clock_nanosleep()` after failed auth traffic, while `/proc` showed the process sleeping with negligible CPU. This failure was observed by Fixer and not independently reproduced.
The likely code-level cause is `auth2.c:ensure_minimum_time_since()`: it checked `MAX_FAIL_DELAY_SECONDS` before scaling the failed-auth timing pad upward, but not after that scaling. I kept the existing timing-pad behavior, clamped the scaled target with OpenSSH’s existing `MINIMUM()` helper, and returned without sleeping when elapsed auth work has already reached the cap. The expected effect is to preserve the timing mitigation while preventing slow failed-auth backends from adding extra sleep past the maximum delay target.
For upstream style, I checked the available project docs (`README.md`; no `CONTRIBUTING`, `HACKING`, or `README-hacking` found) and kept to nearby auth2 conventions: `monotime_double()`, `debug3_f()`, direct `nanosleep()`, and `misc.h` helpers. The review finding was addressed by removing generated regress artifacts from validation; the final patch file set is only `auth2.c`.
Diff Excerpt
diff --git a/auth2.c b/auth2.c
index 80f766e..cca3af8 100644
--- a/auth2.c
+++ b/auth2.c
@@ -256,9 +256,15 @@ ensure_minimum_time_since(double start, double seconds)
return;
}
- /* if we've already passed the requested time, scale up */
+ /* If we've already passed the requested time, scale up to a cap. */
while ((remain = seconds - elapsed) < 0.0)
seconds *= 2;
+ seconds = MINIMUM(seconds, MAX_FAIL_DELAY_SECONDS);
+ if ((remain = seconds - elapsed) <= 0.0) {
+ debug3_f("elapsed %0.3lfms reached the max delay "
+ "(requested %0.3lfms)", elapsed*1000, req*1000);
+ return;
+ }
ts.tv_sec = remain;
ts.tv_nsec = (remain - ts.tv_sec) * 1000000000;
Full published attempt: /issues/019e2c59-6707-7471-b7b2-5329d3cdd34e. Issue JSON: /v1/issues/019e2c59-6707-7471-b7b2-5329d3cdd34e
sshd-session is stuck in a likely unclassified userspace loop: 100.00% of sampled CPU passed through nf_ct_get_tuple, with repeated thread backtraces show 1 thread(s) around 0x00007f9d21c9a7d2 in ?? () from target:<path>.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-15 15:36 UTCvalidation: readyupstream: review8 duplicate diffs collapsed
Attempt Summary
sshd-session likely remains stuck in an unclassified userspace loop. A diagnosis report and patch proposal were created locally.
Suggested subject
channels: arm poll events for channel socket fds
Commit message.
`channel_prepare_pollfd()` builds the `pollfd` array used by the SSH channel event loop. For channel socket fds, it computed the requested `POLLIN`/`POLLOUT` event mask but stored zero in `pollfd.events`, so `ppoll()` would not wait for the readiness the channel handlers expected.
Store the computed event mask for socket fds, matching the existing `rfd`, `wfd`, and `efd` paths.
Issue connection.
Fixer observed `sshd-session` processes with profiler samples in kernel conntrack, `strace` showing `restart_syscall`, and `/proc` state showing the process sleeping in `poll_schedule_timeout`. I did not independently reproduce that production signal.
The likely root cause in the channel event loop is that socket-only channel fds were registered in the `pollfd` array with `events = 0` even after the code computed `POLLIN` or `POLLOUT` from `SSH_CHAN_IO_SOCK_R/W`. In OpenSSH’s channel subsystem, `io_want` records which fd readiness a channel needs before its post-poll handler can run.
The patch changes the socket fd poll setup to store `ev`, the same computed event mask used by the other fd slots. The expected effect is that listener and connecting socket channels can actually wake `ppoll()` for the readiness they requested, preventing this event-loop path from parking a session without arming the socket events it needs.
No new helpers were needed. The touched subsystem already uses `xrecallocarray()` for pollfd allocation, `fatal_f()`/`debug3()` logging, and `ptimeout_*()` for `ppoll()` timeouts; this patch follows the existing local pattern. I checked `README.md`; no `CONTRIBUTING`/`HACKING` docs were present.
Diff Excerpt
diff --git a/channels.c b/channels.c
index d7c55fc..c9d4b50 100644
--- a/channels.c
+++ b/channels.c
@@ -2858,7 +2858,7 @@ channel_prepare_pollfd(Channel *c, u_int *next_pollfd,
if (ev != 0) {
c->pfds[3] = p;
pfd[p].fd = c->sock;
- pfd[p].events = 0;
+ pfd[p].events = ev;
dump_channel_poll(__func__, "sock", c, p, &pfd[p]);
p++;
}
Full published attempt: /issues/019dfd2e-6077-7a51-b6b8-e030a863fa21. Issue JSON: /v1/issues/019dfd2e-6077-7a51-b6b8-e030a863fa21
dockerd is stuck in a likely socket churn loop: 10.98% of sampled CPU passed through futex_wake_mark, with repeated thread backtraces show 1 thread(s) around 0x000056124fea7023 in ?? () and 1 thread(s) around 0x000056124fea7023 in ?? ().
highpackage: docker.iosource: docker.ioscore: 106reports: 2patch attempt readypatched: 2026-05-15 08:27 UTCvalidation: readyupstream: review
Attempt Summary
dockerd likely remains stuck in a socket churn loop. A diagnosis report and patch proposal were created locally.
Original harvest blockers
These blockers remain on the preserved local diff, but the upstream review above is the current handoff.
Suggested subject
daemon: bound startup goroutine creation
Commit message.
During daemon startup, the container restore paths used a semaphore to limit active startup work, but each loop still created one goroutine per container before acquiring that semaphore. A semaphore is a concurrency limiter; acquiring it inside each worker limits running work, but not the number of goroutines parked waiting for a slot.
This adds a small helper that acquires the existing startup semaphore before starting the worker goroutine, then releases it with `defer`. The startup limit now bounds both active work and goroutines waiting for startup work. I followed the plan, except I intentionally left the legacy-link restart phase’s goroutine creation order unchanged because those workers can wait on child container notifications.
Issue connection.
Fixer observed `dockerd` running with 185 threads, low CPU, process state `S (sleeping)`, a futex-heavy stack, and strace samples dominated by futex waits. This failure signal was observed by Fixer and not independently reproduced.
The cautious code-level inference is that daemon startup can contribute to this wait-heavy shape: several restore loops in `engine/daemon/daemon.go` create a goroutine for every container and only then acquire the startup semaphore. That means a large container set can still create many goroutines blocked on the semaphore, even though `adjustParallelLimit` is meant to cap startup parallelism.
The patch keeps the existing local patterns, specifically `adjustParallelLimit`, `sync.WaitGroup`, and `golang.org/x/sync/semaphore`. It adds `startParallelStartupTask`, which acquires the semaphore before launching each independent startup worker. I added a short source comment explaining the invariant being preserved.
The expected effect is fewer startup goroutines parked in semaphore/futex waits when many containers are restored or restarted, without increasing the amount of concurrent startup work.
Diff Excerpt
diff --git a/engine/daemon/daemon.go b/engine/daemon/daemon.go
index 2e0a36e..65a42e4 100644
--- a/engine/daemon/daemon.go
+++ b/engine/daemon/daemon.go
@@ -206,6 +206,21 @@ func (daemon *Daemon) UsesSnapshotter() bool {
return daemon.usesSnapshotter
}
+func startParallelStartupTask(ctx context.Context, sem *semaphore.Weighted, group *sync.WaitGroup, task func()) bool {
+ // Acquire before starting the goroutine so the startup limit bounds both
+ // active work and goroutines waiting for a startup slot.
+ if err := sem.Acquire(ctx, 1); err != nil {
+ return false
+ }
+ group.Add(1)
+ go func() {
+ defer group.Done()
+ defer sem.Release(1)
+ task()
+ }()
+ return true
+}
+
func (daemon *Daemon) restore(cfg *configStore) error {
var mapLock sync.Mutex
containers := make(map[string]*container.Container)
@@ -229,12 +244,8 @@ func (daemon *Daemon) restore(cfg *configStore) error {
sem := semaphore.NewWeighted(int64(parallelLimit))
for _, v := range dir {
- group.Add(1)
- go func(id string) {
- defer group.Done()
- _ = sem.Acquire(context.Background(), 1)
- defer sem.Release(1)
-
+ id := v.Name()
+ startParallelStartupTask(context.Background(), sem, &group, func() {
logger := log.G(context.TODO()).WithField("container", id)
c, err := daemon.load(id)
@@ -261,7 +272,7 @@ func (daemon *Daemon) restore(cfg *configStore) error {
mapLock.Lock()
containers[c.ID] = c
mapLock.Unlock()
- }(v.Name())
+ })
}
group.Wait()
@@ -270,12 +281,8 @@ func (daemon *Daemon) restore(cfg *configStore) error {
activeSandboxes := make(map[string]any)
for _, c := range containers {
- group.Add(1)
- go func(c *container.Container) {
- defer group.Done()
- _ = sem.Acquire(context.Background(), 1)
- defer sem.Release(1)
-
+ c := c
+ startParallelStartupTask(context.Background(), sem, &group, func() {
logger := log.G(context.TODO()).WithField("container", c.ID)
if err := daemon.registerName(c); err != nil {
@@ -292,17 +299,13 @@ func (daemon *Daemon) restore(cfg *configStore) error {
mapLock.Unlock()
return
}
- }(c)
+ })
}
group.Wait()
for _, c := range containers {
- group.Add(1)
- go func(c *container.Container) {
- defer group.Done()
- _ = sem.Acquire(context.Background(), 1)
- defer sem.Release(1)
-
+ c := c
+ startParallelStartupTask(context.Background(), sem, &group, func
[truncated]
Full published attempt: /issues/019dfdca-f6d4-72a1-be1c-18d5922e0bde. Issue JSON: /v1/issues/019dfdca-f6d4-72a1-be1c-18d5922e0bde
redis-check-rdb is stuck in a likely busy poll loop: 5.26% of sampled CPU passed through num_to_str, with repeated thread backtraces show 1 thread(s) around 0x00007fbc72ca5ffe in ?? () and 1 thread(s) around 0x00007fbc72ca5ffe in ?? ().
highpackage: redis-toolssource: redisscore: 106reports: 2patch attempt readypatched: 2026-05-15 00:54 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: alternative review
Attempt Summary
redis-check-rdb likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Suggested subject
server: sample memory stats less often in cron
Commit message.
Redis cron periodically refreshes cached memory statistics for `INFO memory`. On Linux, the RSS sample uses Redis’ existing `zmalloc_get_rss()` helper, which reads `/proc/self/stat` through `get_proc_stat_ll()`.
Run that slower memory-stat sample once per second instead of every 100ms. This keeps the existing Redis `run_with_period()` cron convention and avoids changing platform-specific file IO helpers.
Issue connection.
Fixer observed this failure signal directly, but I did not independently reproduce it locally. The collected perf, strace, and `/proc` evidence showed a process mapped as `redis-check-rdb` but running as `redis-server`, sleeping in repeated `epoll_wait()` timeouts and then opening, reading, and closing `/proc/self/stat` about every 100ms.
The code-level cause is the normal server cron path: `serverCron()` calls `cronUpdateMemoryStats()`, which calls `zmalloc_get_rss()`. On Linux, `zmalloc_get_rss()` uses the local helper `get_proc_stat_ll()` to read `/proc/self/stat`. The `redis-check-rdb` name is expected to be an installed alias of the Redis server binary, so the relevant path is the server cron memory-stat path rather than the RDB checker parser.
The patch changes only the memory-stat cron cadence from `run_with_period(100)` to `run_with_period(1000)`. The expected effect is to reduce the observed `/proc/self/stat` sampling work by roughly 10x while keeping RSS and allocator statistics refreshed at a reasonable reporting cadence.
I followed the saved plan and did not change course. I checked `CONTRIBUTING.md`, `README.md`, and `tests/README.md`; no separate coding-style rule was relevant beyond using the existing Redis cron convention and local memory helpers.
Diff Excerpt
diff --git a/src/server.c b/src/server.c
index 241fe69..9ac080b 100644
--- a/src/server.c
+++ b/src/server.c
@@ -1425,7 +1425,7 @@ void updatePeakMemory(void) {
void cronUpdateMemoryStats(void) {
updatePeakMemory();
- run_with_period(100) {
+ run_with_period(1000) {
/* Sample the RSS and other metrics here since this is a relatively slow call.
* We must sample the zmalloc_used at the same time we take the rss, otherwise
* the frag ratio calculate may be off (ratio of two samples at different times) */
Full published attempt: /issues/019e28a3-550a-70a1-9ff0-15b0c9603ee0. Issue JSON: /v1/issues/019e28a3-550a-70a1-9ff0-15b0c9603ee0
python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyObject_GC_Del, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.
highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt readypatched: 2026-05-14 15:13 UTCvalidation: readyupstream: closed-unmerged
Attempt Summary
python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Original harvest blockers
These blockers remain on the preserved local diff, but the upstream review above is the current handoff.
Suggested subject
Skip idle reaping without tracked child PIDs
Commit message.
`supervisord` tracks live managed child processes in `pidhistory`, a map from child PID to process object. When that map is empty, the main loop has no managed child to collect, so avoid the periodic `waitpid(-1)` sweep that otherwise reports “no children” from the kernel.
This keeps the existing one-second poll timeout, preserving signal, socket, and tick responsiveness. This also changes course from the earlier timeout-based plan because review showed that increasing the poll timeout could delay queued signal handling.
Issue connection.
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` with high CPU attribution in Python runtime frames. The concrete trace signal was repeated `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by `poll(..., 1000)`. This failure was observed by Fixer and not independently reproduced as high CPU locally.
The cautious code-level inference is that the relevant work is in supervisor’s application loop, not the Python runtime. `supervisord` already maintains `options.pidhistory` for live managed child PIDs, and uses the local `ServerOptions.waitpid()` wrapper for reaping. When `pidhistory` is empty, the periodic managed-child reap has nothing to collect, matching the no-child condition shown in the trace more closely than checking configured process groups.
The patch keeps the existing poll timeout and gates the main-loop `self.reap()` call on `self.options.pidhistory`. The test helper now counts `waitpid()` calls, and `test_supervisord` covers both a configured-but-idle process group with no tracked child PID and a tracked-child case that still reaps.
The expected effect is to reduce unnecessary idle `waitpid(ECHILD)` calls when there are no tracked managed child processes, without changing signal latency or normal reaping when child PIDs are present. A short source comment explains the `pidhistory` invariant.
Diff Excerpt
diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..42fdf6b 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -241,7 +241,10 @@ class Supervisor:
for group in pgroups:
group.transition()
- self.reap()
+ if self.options.pidhistory:
+ # pidhistory tracks live managed child PIDs; skip the
+ # periodic waitpid(-1) sweep when there are none to collect.
+ self.reap()
self.handle_signal()
self.tick()
diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py
index f608b2b..1f25b58 100644
--- a/supervisor/tests/base.py
+++ b/supervisor/tests/base.py
@@ -60,6 +60,7 @@ class DummyOptions:
self.pidfile_written = False
self.directory = None
self.waitpid_return = None, None
+ self.waitpid_calls = 0
self.kills = {}
self._signal = None
self.parent_pipes_closed = None
@@ -145,6 +146,7 @@ class DummyOptions:
self.pidfile_written = True
def waitpid(self):
+ self.waitpid_calls += 1
return self.waitpid_return
def kill(self, pid, sig):
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index 3d7b4ff..993cffd 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -665,6 +665,25 @@ class SupervisordTests(unittest.TestCase):
supervisord.runforever()
self.assertEqual(len(supervisord.ticks), 3)
+ def test_runforever_skips_reap_without_child_pids(self):
+ options = DummyOptions()
+ supervisord = self._makeOne(options)
+ gconfig = DummyPGroupConfig(options)
+ supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+ options.test = True
+ supervisord.runforever()
+ self.assertEqual(options.waitpid_calls, 0)
+
+ def test_runforever_reaps_with_child_pids(self):
+ options = DummyOptions()
+ options.pidhistory = {123: DummyProcess(None)}
+ supervisord = self._makeOne(options)
+ gconfig = DummyPGroupConfig(options)
+ supervisord.process_groups = {'foo': DummyProcessGroup(gconfig)}
+ options.test = True
+ supervisord.runforever()
+ self.assertEqual(options.waitpid_calls, 1)
+
def test_runfor
[truncated]
Full published attempt: /issues/019dd1e0-1f56-7862-9fd2-d20d576df8c4. Issue JSON: /v1/issues/019dd1e0-1f56-7862-9fd2-d20d576df8c4
sshd-session is stuck in a likely unclassified userspace loop: 20.65% of sampled CPU passed through get_pid_task, with repeated thread backtraces show 1 thread(s) around 0x00007f202289a7d2 in ?? () from target:<path>.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-14 13:41 UTCvalidation: readyupstream: review
Attempt Summary
sshd-session likely remains stuck in an unclassified userspace loop. A diagnosis report and patch proposal were created locally.
Suggested subject
auth: avoid duplicate passwd lookup in getpwnamallow
Commit message.
`getpwnamallow()` checked whether a user existed for `Match Invalid-User` by calling `getpwnam(user)`, then immediately called `getpwnam(user)` again for the actual admission decision.
Reuse the first passwd lookup on the normal path and keep the second lookup only for the existing AIX `setauthdb` case, where the lookup is intentionally performed after switching authentication databases.
Issue connection.
Fixer observed `sshd-session: gis [priv]` with perf samples in NSS/passwd-related code (`libnss_systemd.so.2` and `_nss_files_parse_pwent`) while `/proc` and strace showed the sampled process sleeping in `poll`/`restart_syscall`. This was observed by Fixer and not independently reproduced as a runaway CPU loop.
The cautious source-level cause is redundant NSS work in the server authentication admission path: `getpwnamallow()` performed two unconditional passwd database lookups for the same user on non-AIX systems. NSS is the Name Service Switch layer that resolves users/groups through files, systemd, LDAP, and similar backends.
The patch reuses the first `getpwnam(user)` result to set `ci->user_invalid` and to continue admission checks. It preserves the platform compatibility convention around `_AIX && HAVE_SETAUTHDB` by keeping the second lookup there. Existing OpenSSH helpers/conventions were followed: `pwcopy()` still owns the returned passwd data, existing logging style is unchanged, and no new file/process/allocation/locking APIs were introduced. No source comment was added because the control flow is direct.
The expected effect is to remove one unconditional NSS passwd lookup per user admission on the common path, reducing avoidable work in the same subsystem implicated by the collected perf evidence.
Diff Excerpt
diff --git a/auth.c b/auth.c
index 2a9f3b8..cc8482b 100644
--- a/auth.c
+++ b/auth.c
@@ -474,7 +474,8 @@ getpwnamallow(struct ssh *ssh, const char *user)
ci = server_get_connection_info(ssh, 1, options.use_dns);
ci->user = user;
- ci->user_invalid = getpwnam(user) == NULL;
+ pw = getpwnam(user);
+ ci->user_invalid = pw == NULL;
parse_server_match_config(&options, &includes, ci);
log_change_level(options.log_level);
log_verbose_reset();
@@ -484,11 +485,9 @@ getpwnamallow(struct ssh *ssh, const char *user)
#if defined(_AIX) && defined(HAVE_SETAUTHDB)
aix_setauthdb(user);
-#endif
pw = getpwnam(user);
-#if defined(_AIX) && defined(HAVE_SETAUTHDB)
aix_restoreauthdb();
#endif
if (pw == NULL) {
Full published attempt: /issues/019e2683-8ae8-7de2-ac7c-52a3f304c75f. Issue JSON: /v1/issues/019e2683-8ae8-7de2-ac7c-52a3f304c75f
dockerd is stuck in a likely socket churn loop: 100.00% of sampled CPU passed through do_epoll_wait, with repeated thread backtraces show 1 thread(s) around 0x000056124fea7023 in ?? () and 1 thread(s) around 0x000056124fea7023 in ?? ().
highpackage: docker.iosource: docker.ioscore: 106reports: 2patch attempt readypatched: 2026-05-14 05:38 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: review
Attempt Summary
dockerd likely remains stuck in a socket churn loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Suggested subject
libcontainerd: handle closed event stream channels
Commit message.
`dockerd` subscribes to containerd task events through a channel pair: one channel carries event envelopes and one carries stream errors. The event loop now checks whether either channel has closed before using the received value. A closed event channel now triggers the existing containerd readiness wait and subscription restart path, while a closed error channel stops the loop cleanly. The event dispatch logic was split into a small helper so closed-channel behavior can be unit tested without a live containerd daemon.
Issue connection.
Fixer observed `dockerd` in a sleeping state with many futex waits, an `epoll_pwait` hot path, and a strace sample dominated by futex waits with one epoll waiter. That failure signal was observed by Fixer and not independently reproduced; the raw artifact paths referenced by the evidence bundle were not present in the workspace, so the source connection is a targeted mitigation rather than a confirmed local reproducer.
The likely root cause is that `libcontainerd/remote.processEventStream` read from containerd subscription channels without checking the receive `ok` value. If the containerd event stream channel closes, Go returns the zero value immediately; the old loop could then treat that as another event instead of leaving or restarting the subscription. I changed the loop to handle closed channels explicitly, using the existing `c.logger` logging convention and existing `waitServe` restart helper. The expected effect is that a closed containerd task-event stream cannot keep `dockerd` churning through invalid event receives; it either restarts the subscription after containerd is ready or exits the stream loop cleanly.
I followed the saved plan. No relevant local wrappers for file IO, process spawning, allocation, locking, or platform compatibility were needed; the touched subsystem already uses `containerd/log`, `status.FromError`, and `waitServe`, so the patch stays with those conventions.
Diff Excerpt
diff --git a/engine/libcontainerd/remote/client.go b/engine/libcontainerd/remote/client.go
index 3d3ec63..46c15c0 100644
--- a/engine/libcontainerd/remote/client.go
+++ b/engine/libcontainerd/remote/client.go
@@ -14,6 +14,7 @@ import (
"time"
apievents "github.com/containerd/containerd/api/events"
+ eventtypes "github.com/containerd/containerd/api/services/events/v1"
"github.com/containerd/containerd/api/types"
runcoptions "github.com/containerd/containerd/api/types/runc/options"
containerd "github.com/containerd/containerd/v2/client"
@@ -620,96 +621,122 @@ func (c *client) processEventStream(ctx context.Context, ns string) {
c.logger.Debug("processing event stream")
+ if processEventStreamEvents(ctx, c.logger, eventStream, errC, func(ev *eventtypes.Envelope) {
+ c.processEventStreamEvent(ctx, ev)
+ }) == eventStreamRestart {
+ c.logger.Info("Waiting for containerd to be ready to restart event processing")
+ if c.waitServe(ctx) {
+ go c.processEventStream(ctx, ns)
+ }
+ }
+}
+
+type eventStreamAction int
+
+const (
+ eventStreamStop eventStreamAction = iota
+ eventStreamRestart
+)
+
+func processEventStreamEvents(ctx context.Context, logger *log.Entry, eventStream <-chan *eventtypes.Envelope, errC <-chan error, handleEvent func(*eventtypes.Envelope)) eventStreamAction {
for {
select {
- case err := <-errC:
+ case err, ok := <-errC:
+ if !ok {
+ logger.Info("containerd event error channel closed")
+ return eventStreamStop
+ }
if err != nil {
errStatus, ok := status.FromError(err)
if !ok || errStatus.Code() != codes.Canceled {
- c.logger.WithError(err).Error("Failed to get event")
- c.logger.Info("Waiting for containerd to be ready to restart event processing")
- if c.waitServe(ctx) {
- go c.processEventStream(ctx, ns)
- return
- }
+ logger.WithError(err).Error("Failed to get event")
+ return eventStreamRestart
}
- c.logger.WithError(ctx.Err()).Info("stopping event stream following graceful shutdown")
+ logger.WithError(ctx.Err()).Info("stopping event stream following graceful shutdown")
}
- return
- case ev := <-eventStream:
- if ev.Event == nil {
- c.logger.WithField("event", ev).Warn("invalid event")
- continue
+ return eventStreamStop
+ case ev, ok := <-eventStream:
+ if !ok {
+ logger.Warn("containerd event stream closed")
+ return
[truncated]
Full published attempt: /issues/019dee60-21e4-7d90-84c6-ac0d23f839ad. Issue JSON: /v1/issues/019dee60-21e4-7d90-84c6-ac0d23f839ad
sshd-auth is stuck in a likely timer churn loop: 100.00% of sampled CPU passed through dequeue_task_fair, with repeated thread backtraces show 1 thread(s) around 0x00007f251a09a7d2 in ?? () from target:<path>.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-14 04:47 UTCvalidation: readyrelated upstream: closed-unmerged
Attempt Summary
sshd-auth likely remains stuck in a timer churn loop. A diagnosis report and patch proposal were created locally.
Next proof/action
- Do not open another source-path-family PR from this retained diff; gather new reproduction evidence or a materially distinct fix before rerunning upstream.
Suggested subject
auth2: cap scaled userauth failure delay
Commit message.
Fixer observed `sshd-auth` sleeping in an authentication failure path: the trace shows a `clock_nanosleep` before encrypted failure-response writes. Bound the scaled userauth failure delay so the total time since the request started cannot exceed `MAX_FAIL_DELAY_SECONDS`.
The delay still preserves the existing timing-obfuscation behavior, but the scale-up bucket is now capped before sleeping.
Issue connection.
Fixer observed this failure signal directly but I did not independently reproduce it: `sshd-auth: root [net]` was in `S (sleeping)`, with strace showing a 1.612s `clock_nanosleep` followed by writes, and perf/proc evidence classifying the sample as timer-heavy.
The matching code path is `auth2.c` user authentication failure handling. `ensure_minimum_time_since()` applies a per-user delay before `SSH2_MSG_USERAUTH_FAILURE`; if authentication work has already passed the requested delay, it doubles the target delay bucket. Before this patch, that doubled bucket was not capped, so the total failure handling time could exceed the function’s own `MAX_FAIL_DELAY_SECONDS`.
The patch clamps the scaled bucket to `MAX_FAIL_DELAY_SECONDS` and avoids a zero or negative sleep. The expected effect is that authentication failure timing remains padded, but `sshd-auth` cannot add an oversized sleep past the intended maximum while preparing a failure response. I kept the short source comment explaining the cap invariant.
Review round 1 found generated regression byproducts in the workspace. I removed those unintended files; the final patch contains only `auth2.c`.
Local conventions checked: `README.md` and `regress/README.regress`; nearby code uses `monotime_double()`, `debug3_f()`, `nanosleep()`, `ssh_packet_write_wait()`, `atomicio()`, `xmalloc()`, and `freezero()`. No relevant local wrapper replaces this direct sleep path.
Diff Excerpt
diff --git a/auth2.c b/auth2.c
index 80f766e..787b410 100644
--- a/auth2.c
+++ b/auth2.c
@@ -250,15 +250,20 @@ ensure_minimum_time_since(double start, double seconds)
struct timespec ts;
double elapsed = monotime_double() - start, req = seconds, remain;
- if (elapsed > MAX_FAIL_DELAY_SECONDS) {
+ if (elapsed >= MAX_FAIL_DELAY_SECONDS) {
debug3_f("elapsed %0.3lfms exceeded the max delay "
"requested %0.3lfms)", elapsed*1000, req*1000);
return;
}
- /* if we've already passed the requested time, scale up */
- while ((remain = seconds - elapsed) < 0.0)
+ /* If we've passed the requested time, scale up within the cap. */
+ while (seconds < elapsed && seconds < MAX_FAIL_DELAY_SECONDS)
seconds *= 2;
+ if (seconds > MAX_FAIL_DELAY_SECONDS)
+ seconds = MAX_FAIL_DELAY_SECONDS;
+ remain = seconds - elapsed;
+ if (remain <= 0.0)
+ return;
ts.tv_sec = remain;
ts.tv_nsec = (remain - ts.tv_sec) * 1000000000;
Full published attempt: /issues/019e2474-6e4c-7d70-ab9b-e5f2b75443e0. Issue JSON: /v1/issues/019e2474-6e4c-7d70-ab9b-e5f2b75443e0
dockerd is stuck in a likely socket churn loop: 53.32% of sampled CPU passed through __futex_hash, with repeated thread backtraces show 1 thread(s) around 0x000056124fea7023 in ?? () and 1 thread(s) around 0x000056124fea7023 in ?? ().
highpackage: docker.iosource: docker.ioscore: 106reports: 2patch attempt readypatched: 2026-05-14 03:06 UTCvalidation: readyupstream: review
Attempt Summary
dockerd likely remains stuck in a socket churn loop. A diagnosis report and patch proposal were created locally.
Suggested subject
libnetwork: stop waiting after LinkSubscribe drain timeout
Commit message.
`waitForIfUpped` subscribes to Linux netlink link updates while waiting for an interface to become up. Netlink is the kernel socket API used here for network-interface events.
When the function exits, it closes the subscription and drains the update channel. That drain had a three-second timeout, but after logging the timeout it kept looping, so a subscription that failed to close its channel could keep the caller blocked indefinitely. Return after the timeout warning so the timeout is actually bounded.
Issue connection.
Fixer observed `dockerd` with a perf hotspot in kernel `__futex_hash`, many threads waiting in futex syscalls, and unsymbolized dockerd frames. I did not independently reproduce that live failure. After adjusting the sampled PCs for the PIE load base from the evidence maps, `go tool addr2line /usr/sbin/dockerd` resolved representative frames to `github.com/vishvananda/netlink.linkSubscribeAt.func2`, which is reached from Docker through `libnetwork/osl.waitForIfUpped()` and the local `internal/nlwrap.LinkSubscribeWithOptions` helper.
The cautious code-level cause is that `waitForIfUpped()` intended to bound cleanup of a netlink `LinkSubscribe` watcher, but its timeout case only logged and then continued waiting. If the netlink package did not close the update channel, that cleanup path could remain stuck while subscription goroutines remained around.
The change adds a `return` after the existing timeout warning. The expected effect is that this cleanup path stops waiting after its documented timeout instead of adding another stuck waiter around an already-failed netlink subscription. This follows existing local patterns: it keeps `nlwrap.LinkSubscribeWithOptions`, `github.com/containerd/log`, and the existing timeout-based control flow; no new helper or platform API was introduced. I changed course from the saved no-patch plan because local symbolization tied the observed frames to this specific netlink subscription path.
Diff Excerpt
diff --git a/engine/libnetwork/osl/interface_linux.go b/engine/libnetwork/osl/interface_linux.go
index 7ec9cdc..99d8f66 100644
--- a/engine/libnetwork/osl/interface_linux.go
+++ b/engine/libnetwork/osl/interface_linux.go
@@ -455,6 +455,7 @@ func waitForIfUpped(ctx context.Context, ns netns.NsHandle, ifIndex int) (bool,
}
case <-drainTimerC:
log.G(ctx).Warn("timeout while waiting for LinkSubscribe to terminate")
+ return
}
}
}()
Full published attempt: /issues/019dfdfe-8133-7900-be11-727e24964bda. Issue JSON: /v1/issues/019dfdfe-8133-7900-be11-727e24964bda
dockerd is stuck in a likely socket churn loop: 50.00% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 1 thread(s) around 0x000056124fea7023 in ?? () and 1 thread(s) around 0x000056124fea7023 in ?? ().
highpackage: docker.iosource: docker.ioscore: 106reports: 2patch attempt readypatched: 2026-05-13 19:29 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: review
Attempt Summary
dockerd likely remains stuck in a socket churn loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Suggested subject
libcontainerd: handle closed event stream channel
Commit message.
`libcontainerd/remote` subscribes to containerd task events and forwards them into dockerd. If containerd closes the event channel without an error, the old receive ignored the channel `ok` result and could keep reading zero-value events forever. Treat a closed event stream as a broken subscription, wait for containerd to be ready, and restart event processing.
Issue connection.
Fixer observed `dockerd` with profiler, strace, and `/proc` evidence showing a sleeping daemon with many futex waits and socket/IPC churn while running with `--containerd=/run/containerd/containerd.sock`. That means dockerd was using an external containerd client, not the managed containerd supervisor path. The failure was observed by Fixer and not independently reproduced from a live daemon.
The code-level issue is in `libcontainerd/remote.processEventStream`: a closed Go channel can still be received from, yielding the zero value repeatedly. The previous loop read from `eventStream` without checking `ok`, so a closed containerd event stream could be handled as repeated invalid empty events instead of reconnecting.
I changed the receive to check `ok`. When the stream closes during shutdown, it exits cleanly; otherwise it logs the closed stream, waits for containerd readiness using the existing `waitServe` helper, and restarts the subscription. I followed the existing local conventions: `containerd/log` via `c.logger`, context cancellation, and the existing containerd event subscription API. No relevant local wrapper replaces channel receive logic; process helpers such as `pkg/process` and `pkg/pidfile` were not relevant to this path. I added a short source comment explaining the closed-channel invariant.
The expected effect is that dockerd no longer spins on zero-value task events after a containerd event channel closes unexpectedly, reducing one plausible event-stream source of the observed churn.
Diff Excerpt
diff --git a/engine/libcontainerd/remote/client.go b/engine/libcontainerd/remote/client.go
index 3d3ec63..0bb0ee8 100644
--- a/engine/libcontainerd/remote/client.go
+++ b/engine/libcontainerd/remote/client.go
@@ -636,7 +636,20 @@ func (c *client) processEventStream(ctx context.Context, ns string) {
c.logger.WithError(ctx.Err()).Info("stopping event stream following graceful shutdown")
}
return
- case ev := <-eventStream:
+ case ev, ok := <-eventStream:
+ if !ok {
+ if ctx.Err() != nil {
+ c.logger.WithError(ctx.Err()).Info("stopping event stream following graceful shutdown")
+ return
+ }
+ // A closed event channel yields zero-value events forever; restart the subscription instead.
+ c.logger.Warn("containerd event stream closed")
+ c.logger.Info("Waiting for containerd to be ready to restart event processing")
+ if c.waitServe(ctx) {
+ go c.processEventStream(ctx, ns)
+ }
+ return
+ }
if ev.Event == nil {
c.logger.WithField("event", ev).Warn("invalid event")
continue
Full published attempt: /issues/019de62e-e606-78f1-bad7-ba4238222379. Issue JSON: /v1/issues/019de62e-e606-78f1-bad7-ba4238222379
dockerd is stuck in a likely socket churn loop: 100.00% of sampled CPU passed through __schedule, with repeated thread backtraces show 1 thread(s) around 0x000056124fea7023 in ?? () and 1 thread(s) around 0x000056124fea7023 in ?? ().
highpackage: docker.iosource: docker.ioscore: 106reports: 2patch attempt readypatched: 2026-05-13 18:14 UTCvalidation: readyrelated upstream: covered-by-review review
Attempt Summary
dockerd likely remains stuck in a socket churn loop. A diagnosis report and patch proposal were created locally.
Suggested subject
libcontainerd: bound containerd event stream reconnects
Commit message.
Keep containerd task event stream recovery inside one goroutine and handle closed event channels explicitly. If the stream drops, dockerd now cancels the old subscription, waits for containerd to report ready, and delays briefly before resubscribing instead of starting a replacement goroutine immediately.
Refinement round 1 addresses the review finding by rewording the source comment and patch explanation to describe reconnect goroutine churn, not accumulated event processors.
Issue connection.
Fixer observed `dockerd` with 185 threads, mostly blocked in futex waits, with strace showing repeated `futex`, `nanosleep`, `epoll_pwait`, `waitid`, and some nonblocking socket reads. This was observed by Fixer and not independently reproduced.
The cautious code-level connection is the libcontainerd task event stream, which is the daemon’s long-lived containerd event subscription for task lifecycle messages. Its recovery path spawned a replacement `processEventStream` goroutine on recoverable stream errors and did not distinguish closed event/error channels from real events.
This patch keeps reconnect handling in the existing goroutine, explicitly treats closed channels as a reconnect condition, preserves the existing `waitServe` readiness check, and adds a short timer before resubscribing. It follows the local `containerd/log`, `context.WithCancel`, and timer conventions already used in this subsystem. I added a short comment documenting that reconnects stay in one goroutine to avoid replacement-goroutine churn.
The expected effect is to reduce event-stream reconnect churn when the containerd event subscription repeatedly fails or closes.
Diff Excerpt
diff --git a/engine/libcontainerd/remote/client.go b/engine/libcontainerd/remote/client.go
index 3d3ec63..cf88ad2 100644
--- a/engine/libcontainerd/remote/client.go
+++ b/engine/libcontainerd/remote/client.go
@@ -574,6 +574,73 @@ func (c *client) processEvent(ctx context.Context, et libcontainerdtypes.EventTy
})
}
+func (c *client) processEventEnvelope(ctx context.Context, ev *apievents.Envelope) {
+ v, err := typeurl.UnmarshalAny(ev.Event)
+ if err != nil {
+ c.logger.WithError(err).WithField("event", ev).Warn("failed to unmarshal event")
+ return
+ }
+
+ c.logger.WithField("topic", ev.Topic).Debug("event")
+
+ switch t := v.(type) {
+ case *apievents.TaskCreate:
+ c.processEvent(ctx, libcontainerdtypes.EventCreate, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ ProcessID: t.ContainerID,
+ Pid: t.Pid,
+ })
+ case *apievents.TaskStart:
+ c.processEvent(ctx, libcontainerdtypes.EventStart, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ ProcessID: t.ContainerID,
+ Pid: t.Pid,
+ })
+ case *apievents.TaskExit:
+ c.processEvent(ctx, libcontainerdtypes.EventExit, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ ProcessID: t.ID,
+ Pid: t.Pid,
+ ExitCode: t.ExitStatus,
+ ExitedAt: protobuf.FromTimestamp(t.ExitedAt),
+ })
+ case *apievents.TaskOOM:
+ c.processEvent(ctx, libcontainerdtypes.EventOOM, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ })
+ case *apievents.TaskExecAdded:
+ c.processEvent(ctx, libcontainerdtypes.EventExecAdded, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ ProcessID: t.ExecID,
+ })
+ case *apievents.TaskExecStarted:
+ c.processEvent(ctx, libcontainerdtypes.EventExecStarted, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ ProcessID: t.ExecID,
+ Pid: t.Pid,
+ })
+ case *apievents.TaskPaused:
+ c.processEvent(ctx, libcontainerdtypes.EventPaused, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ })
+ case *apievents.TaskResumed:
+ c.processEvent(ctx, libcontainerdtypes.EventResumed, libcontainerdtypes.EventInfo{
+ ContainerID: t.ContainerID,
+ })
+ case *apievents.TaskDelete:
+ c.logger.WithFields(log.Fields{
+ "topic": ev.Topic,
+ "type": reflect.TypeOf(t),
+ "container": t.ContainerID,
+ }).Info("ignoring event")
+ default:
[truncated]
Full published attempt: /issues/019de7d3-3c01-71f1-9dbd-ce696226a958. Issue JSON: /v1/issues/019de7d3-3c01-71f1-9dbd-ce696226a958
sshd-session is stuck in a likely socket churn loop: 21.96% of sampled CPU passed through native_flush_tlb_one_user, with repeated munmap x17, read x4, getpid x3.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-13 14:03 UTCvalidation: readyupstream: review
Attempt Summary
sshd-session likely remains stuck in a socket churn loop. A diagnosis report and patch proposal were created locally.
Original harvest blockers
These blockers remain on the preserved local diff, but the upstream review above is the current handoff.
Suggested subject
log: avoid closing syslog after each message
Commit message.
On platforms without `openlog_r()`, keep the libc syslog connection open between daemon log messages instead of closing it after every write. OpenSSH still calls `openlog()` before each `syslog()` write so its program name and facility are restored if another library touched the process-global syslog state.
Issue connection.
Fixer observed `sshd-session` in a low-CPU sampled state with `/proc` showing it sleeping in `poll`, while strace showed pre-authentication invalid-user logging repeatedly opening a Unix datagram socket, connecting to `/dev/log`, sending syslog messages, and closing the socket. This failure was observed by Fixer and not independently reproduced.
The code-level cause is that the non-`openlog_r()` path in `log.c` called `openlog()`, `syslog()`, and `closelog()` for every daemon log message. I changed only that fallback path: `log_init()` still closes and reopens syslog when OpenSSH’s logging identity changes, and `do_log()` still calls `openlog()` before each write to preserve OpenSSH’s ident/facility after PAM, libwrap, or another library uses process-global syslog state, but it no longer calls `closelog()` after every message.
The expected effect is to reduce `/dev/log` socket close/reopen churn during bursts of monitor logging while preserving the existing protection against external-library syslog state changes. This refinement addresses both review findings: the code now refreshes OpenSSH’s syslog identity before each write, and the stale “open/close” comment was corrected. I used the existing OpenSSH logging helpers and monitor logging convention (`log_init()`, `do_log()`, `sshlogdirect()`, `mm_log_handler()` / `monitor_read_log()`), and added a short comment for the process-global syslog invariant.
Diff Excerpt
diff --git a/log.c b/log.c
index 21a3a3c..59ea3b4 100644
--- a/log.c
+++ b/log.c
@@ -63,6 +63,10 @@ static log_handler_fn *log_handler;
static void *log_handler_ctx;
static char **log_verbose;
static size_t nlog_verbose;
+#if !(defined(HAVE_OPENLOG_R) && defined(SYSLOG_DATA_INIT))
+/* log_init() closes/reopens syslog when program identity or facility changes. */
+static int syslog_open;
+#endif
extern char *__progname;
#define LOG_SYSLOG_VIS (VIS_CSTYLE|VIS_NL|VIS_TAB|VIS_OCTAL)
@@ -210,6 +214,12 @@ log_init(const char *av0, LogLevel level, SyslogFacility facility,
log_handler = NULL;
log_handler_ctx = NULL;
+#if !(defined(HAVE_OPENLOG_R) && defined(SYSLOG_DATA_INIT))
+ if (syslog_open) {
+ closelog();
+ syslog_open = 0;
+ }
+#endif
log_on_stderr = on_stderr;
if (on_stderr)
return;
@@ -263,14 +273,14 @@ log_init(const char *av0, LogLevel level, SyslogFacility facility,
/*
* If an external library (eg libwrap) attempts to use syslog
* immediately after reexec, syslog may be pointing to the wrong
- * facility, so we force an open/close of syslog here.
+ * facility, so we force a syslog reinitialisation here.
*/
#if defined(HAVE_OPENLOG_R) && defined(SYSLOG_DATA_INIT)
openlog_r(argv0 ? argv0 : __progname, LOG_PID, log_facility, &sdata);
closelog_r(&sdata);
#else
openlog(argv0 ? argv0 : __progname, LOG_PID, log_facility);
- closelog();
+ syslog_open = 1;
#endif
}
@@ -417,9 +427,10 @@ do_log(LogLevel level, int force, const char *suffix, const char *fmt,
syslog_r(pri, &sdata, "%.500s", fmtbuf);
closelog_r(&sdata);
#else
+ /* Reset process-global syslog state without closing the socket. */
openlog(progname, LOG_PID, log_facility);
+ syslog_open = 1;
syslog(pri, "%.500s", fmtbuf);
- closelog();
#endif
}
errno = saved_errno;
Full published attempt: /issues/019e20f0-8427-7c41-8b40-f8f9aaebb2ae. Issue JSON: /v1/issues/019e20f0-8427-7c41-8b40-f8f9aaebb2ae
sshd-auth is stuck in a likely timer churn loop: 50.00% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 1 thread(s) around 0x00007f51e0e9a7d2 in ?? () from target:<path>.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-13 06:10 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: closed-unmerged
Attempt Summary
sshd-auth likely remains stuck in a timer churn loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Rerun in an environment where the blocked runtime or test validation can complete before upstream submission.
- Do not open another source-path-family PR from this retained diff; gather new reproduction evidence or a materially distinct fix before rerunning upstream.
Suggested subject
auth2: bound failed-auth delay after slow auth
Commit message.
Failed user authentication applies a small per-user delay to reduce timing leaks. When authentication work already exceeded that delay, `ensure_minimum_time_since()` scaled the delay upward until it exceeded the elapsed time, then slept the difference. Slow monitor or authentication backend work could therefore turn a millisecond-scale delay into a much longer `nanosleep()`.
Keep a bounded per-user delay for slow failed authentications instead of scaling it to match the elapsed backend time. Fast failures still wait until the requested per-user delay has elapsed.
Issue connection.
Fixer observed `sshd-auth` sleeping with traces dominated by `read`, `read`, and `clock_nanosleep`; sampled backtraces included `clock_nanosleep()`/`nanosleep()`, and the strace excerpt showed sleeps of about 0.46s and 1.78s after monitor/socket reads. This failure was observed by Fixer and not independently reproduced.
The source-level connection is a cautious match from inspection: `auth2.c` computes a per-user failed-auth delay, and the old slow-auth path repeatedly doubled that delay until it exceeded elapsed authentication work before calling `nanosleep()`. I changed that path to keep one bounded per-user delay when elapsed time already exceeds the target, rather than scaling the delay up to the slow backend time. I also added a short comment because this timing tradeoff is non-obvious.
The expected effect is to reduce long post-authentication sleeps in `sshd-auth` while preserving a per-user delay for failed authentication attempts instead of returning immediately after slow backend work.
This refinement addresses the review findings by replacing the first pass’s early return with a bounded delay and by describing the `auth2.c` mapping as an inspection-based connection, not a locally reproduced root cause. I found only `README.md` among the requested style docs, no `CONTRIBUTING`, `HACKING`, `README-hacking`, `docs/`, or `dev-docs/`. The patch uses existing OpenSSH conventions in this file: `monotime_double()`, `debug3_f()`, and portable `nanosleep()` support.
Diff Excerpt
diff --git a/auth2.c b/auth2.c
index 80f766e..5bb007e 100644
--- a/auth2.c
+++ b/auth2.c
@@ -256,9 +256,14 @@ ensure_minimum_time_since(double start, double seconds)
return;
}
- /* if we've already passed the requested time, scale up */
- while ((remain = seconds - elapsed) < 0.0)
- seconds *= 2;
+ if ((remain = seconds - elapsed) <= 0.0) {
+ /*
+ * Authentication work already exceeded the target delay.
+ * Keep a bounded per-user delay without scaling it up to
+ * match slow backends.
+ */
+ remain = seconds;
+ }
ts.tv_sec = remain;
ts.tv_nsec = (remain - ts.tv_sec) * 1000000000;
Full published attempt: /issues/019dfdca-f6fb-77c3-987d-954b3b228857. Issue JSON: /v1/issues/019dfdca-f6fb-77c3-987d-954b3b228857
python3.13 is stuck in a likely file not found retry loop: 9.65% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 2 thread(s) around 0x00007f6145cf24be in ?? () from target:<path> and 1 thread(s) around 0x00007f614384e594 in ?? () from target:<path>.
highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt readypatched: 2026-05-13 03:31 UTCvalidation: readyupstream: merged
Attempt Summary
python3.13 likely remains stuck in a file-not-found retry loop. A diagnosis report and patch proposal were created locally.
Suggested subject
Reset dependency walks between package marking passes
Commit message.
`transitive_dependencies()` used a mutable default `set`, so independent dependency walks could share package names from previous calls. Use a fresh set for each top-level walk and add a unit test to keep package-marking dependency state isolated.
Issue connection.
Fixer observed `/usr/bin/unattended-upgrade` consuming CPU, with perf/backtrace samples in `apt_pkg`/`libapt-pkg` resolving APT index files and `strace` showing repeated `newfstatat()` probes, including missing APT list files. This failure was observed by Fixer and not independently reproduced as the full high-CPU/newfstatat loop.
The relevant script-side code repeatedly asks APT about package candidates while marking upgrades. In that path, `transitive_dependencies()` used `acc=set()` as a default argument; in Python that set is created once and reused, so dependency names from one package can leak into later dependency walks. That can make later package-marking passes inspect and adjust unrelated stale dependencies, increasing calls into APT metadata lookup.
The patch changes `transitive_dependencies()` to create a fresh accumulator when no caller supplies one, while preserving the explicit accumulator used by recursion. I followed the project’s `HACKING.md` test convention and the existing package-marking helpers; no relevant local file/process/platform wrapper was needed for this pure-Python state fix.
The expected effect is to prevent unrelated dependency walks from accumulating stale package names and reduce unnecessary APT candidate/index lookups during unattended-upgrades package marking.
Diff Excerpt
diff --git a/test/test_dependencies.py b/test/test_dependencies.py
new file mode 100644
index 0000000..9eae5c5
--- /dev/null
+++ b/test/test_dependencies.py
@@ -0,0 +1,33 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+
+import unittest
+from unittest.mock import Mock
+
+from unattended_upgrade import transitive_dependencies
+
+
+class TestDependencies(unittest.TestCase):
+
+ def _get_pkg_with_deps(self, *dep_names):
+ pkg = Mock()
+ pkg.candidate = Mock()
+ pkg.candidate.dependencies = [
+ [Mock(name=name, rawtype="Depends")] for name in dep_names]
+ for dep, dep_name in zip(pkg.candidate.dependencies, dep_names):
+ dep[0].name = dep_name
+ return pkg
+
+ def test_transitive_dependencies_keeps_independent_calls_separate(self):
+ cache = {}
+ first_pkg = self._get_pkg_with_deps("first-dependency")
+ second_pkg = self._get_pkg_with_deps("second-dependency")
+
+ self.assertEqual(
+ {"first-dependency"}, transitive_dependencies(first_pkg, cache))
+ self.assertEqual(
+ {"second-dependency"}, transitive_dependencies(second_pkg, cache))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/unattended-upgrade b/unattended-upgrade
index 053b5f5..ce3a533 100755
--- a/unattended-upgrade
+++ b/unattended-upgrade
@@ -1265,7 +1265,7 @@ def is_pkg_change_allowed(pkg, blacklist, whitelist, strict_whitelist):
def transitive_dependencies(pkg, # type: apt.Package
cache, # type: apt.Cache
- acc=set(), # type AbstractSet[str]
+ acc=None, # type: Optional[Set[str]]
valid_types=None, # type: Optional[AbstractSet[str]]
level=None # type: Optional[int]
):
@@ -1274,6 +1274,9 @@ def transitive_dependencies(pkg, # type: apt.Package
Note that alternative (|) dependencies are collected, too
"""
+ if acc is None:
+ acc = set()
+
if not pkg.candidate or level is not None and level < 1:
return acc
Full published attempt: /issues/019debfe-f9e1-7572-9d4f-5a69220afb30. Issue JSON: /v1/issues/019debfe-f9e1-7572-9d4f-5a69220afb30
redis-check-rdb is stuck in a likely busy poll loop: 5.11% of sampled CPU passed through __snprintf_chk@plt, with repeated thread backtraces show 1 thread(s) around 0x00007fbc72ca5ffe in ?? () and 1 thread(s) around 0x00007fbc72ca5ffe in ?? ().
highpackage: redis-toolssource: redisscore: 106reports: 2patch attempt readypatched: 2026-05-13 01:48 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: review
Attempt Summary
redis-check-rdb likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Suggested subject
zmalloc: reuse proc stat fd for Linux RSS sampling
Commit message.
Redis samples resident set size (RSS, the process memory resident in RAM) from `serverCron()` every 100 ms. On Linux that goes through `get_proc_stat_ll()`, which reopened `/proc/self/stat` for every sample.
Keep a cached `/proc/self/stat` descriptor and read it with `pread()`. Track the pid that opened the descriptor so forked children reopen it for their own `/proc/self/stat`. This keeps the existing sampling cadence and parsing logic, but avoids repeated open/close work in the idle cron path.
Issue connection.
Fixer observed a Redis process with repeated `epoll_wait`, `openat`, and `read` activity, where the proc evidence showed periodic reads of `/proc/self/stat`. This was observed by Fixer and not independently reproduced locally because ptrace-based tracing is blocked in this sandbox.
The cautious code-level cause is Redis’ Linux RSS sampling path: `serverCron()` calls `cronUpdateMemoryStats()` under the existing `run_with_period(100)` convention, and `zmalloc_get_rss()` reads field 24 through `get_proc_stat_ll()`. Before this patch, every sample opened, read, and closed `/proc/self/stat`.
The change keeps the existing helper and return contract, uses Redis’ existing direct Linux procfs style with `O_CLOEXEC`, and adds a short comment for the fork invariant: the cached descriptor must be reopened if `getpid()` changes. No relevant local procfs file-IO wrapper was found; `anetCloexec()` exists for networking fds, but this block already uses direct libc procfs APIs.
The expected effect is to reduce repeated procfs open/close churn during idle memory sampling while preserving the same RSS value parsing and cron frequency.
Diff Excerpt
diff --git a/src/zmalloc.c b/src/zmalloc.c
index 7d9fe6e..fddb7c6 100644
--- a/src/zmalloc.c
+++ b/src/zmalloc.c
@@ -642,15 +642,35 @@ void zmadvise_dontneed(void *ptr) {
int get_proc_stat_ll(int i, long long *res) {
#if defined(HAVE_PROC_STAT)
char buf[4096];
- int fd, l;
+ static int proc_stat_fd = -1;
+ static pid_t proc_stat_pid = -1;
+ ssize_t l = -1;
+ pid_t pid = getpid();
char *p, *x;
- if ((fd = open("/proc/self/stat",O_RDONLY)) == -1) return 0;
- if ((l = read(fd,buf,sizeof(buf)-1)) <= 0) {
- close(fd);
- return 0;
+ /* The cached fd belongs to the process that opened it. After fork(),
+ * reopen it so /proc/self/stat keeps referring to the current process. */
+ if (proc_stat_fd != -1 && proc_stat_pid != pid) {
+ close(proc_stat_fd);
+ proc_stat_fd = -1;
+ proc_stat_pid = -1;
}
- close(fd);
+
+ for (int attempts = 0; attempts < 2; attempts++) {
+ if (proc_stat_fd == -1) {
+ proc_stat_fd = open("/proc/self/stat",O_RDONLY|O_CLOEXEC);
+ if (proc_stat_fd == -1) return 0;
+ proc_stat_pid = pid;
+ }
+
+ l = pread(proc_stat_fd,buf,sizeof(buf)-1,0);
+ if (l > 0) break;
+
+ close(proc_stat_fd);
+ proc_stat_fd = -1;
+ proc_stat_pid = -1;
+ }
+ if (l <= 0) return 0;
buf[l] = '\0';
if (buf[l-1] == '\n') buf[l-1] = '\0';
Full published attempt: /issues/019df672-9134-74c0-a830-9c987d0858ad. Issue JSON: /v1/issues/019df672-9134-74c0-a830-9c987d0858ad
python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyUnicode_FromWideChar, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.
highpackage: python3.13-minimalsource: supervisorscore: 106reports: 2patch attempt readypatched: 2026-05-12 20:13 UTCvalidation: ready
Attempt Summary
python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Suggested subject
supervisord: skip idle reap calls without child activity
Commit message.
`supervisord` reaped children on every main-loop tick, even when it had no tracked child PIDs and no queued SIGCHLD signal. SIGCHLD is the Unix signal delivered when a child process exits. The loop now handles pending signals first and only calls the existing `ServerOptions.waitpid()` wrapper when SIGCHLD was seen or `pidhistory` contains tracked children.
Issue connection.
Fixer observed `/usr/bin/supervisord` running under Python 3.13 with strace samples repeatedly showing `wait4(-1, WNOHANG) = -1 ECHILD` followed by a one-second `poll`. `/proc` evidence showed the process sleeping in `poll`, and the interpreter evidence identified `supervisord` as the script entrypoint, so this patch targets Supervisor rather than the Python runtime.
The code-level cause is that `Supervisor.runforever()` called `reap()` on every loop. `reap()` delegates to the local `ServerOptions.waitpid()` helper, so an idle supervisor with no child processes still asks the kernel for child status each tick and receives `ECHILD`.
I changed the loop to call `handle_signal()` first, have it return the handled signal, and call `reap()` only when the handled signal is SIGCHLD or Supervisor has tracked child PIDs in `options.pidhistory`. The expected effect is to avoid idle `waitpid`/`ECHILD` churn while preserving reaping for known children and for SIGCHLD-driven unknown child exits. This failure was observed by Fixer and not independently reproduced with strace locally because ptrace is blocked in this sandbox.
I followed the existing maintainer conventions in `docs/development.rst` and nearby subsystem code: no new platform API was introduced, and the patch keeps using `SignalReceiver` and `ServerOptions.waitpid()`.
Diff Excerpt
diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..294c05d 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -241,8 +241,9 @@ class Supervisor:
for group in pgroups:
group.transition()
- self.reap()
- self.handle_signal()
+ sig = self.handle_signal()
+ if sig == signal.SIGCHLD or self.options.pidhistory:
+ self.reap()
self.tick()
if self.options.mood < SupervisorStates.RUNNING:
@@ -312,6 +313,7 @@ class Supervisor:
else:
self.options.logger.blather(
'received %s indicating nothing' % signame(sig))
+ return sig
def get_state(self):
return self.options.mood
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index 3d7b4ff..ec34d42 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -123,6 +123,18 @@ class SupervisordTests(unittest.TestCase):
def _makeOne(self, options):
return self._getTargetClass()(options)
+ def _makeWaitpidCountingOptions(self):
+ options = DummyOptions()
+ calls = []
+
+ def waitpid():
+ calls.append(1)
+ return options.waitpid_return
+
+ options.waitpid = waitpid
+ options.test = True
+ return options, calls
+
def test_main_first(self):
options = DummyOptions()
pconfig = DummyPConfig(options, 'foo', '/bin/foo', '/tmp')
@@ -224,6 +236,29 @@ class SupervisordTests(unittest.TestCase):
self.assertEqual(options.logger.data[0],
'reaped unknown pid 2 (exit status 0)')
+ def test_runforever_does_not_reap_without_pids_or_sigchld(self):
+ options, waitpid_calls = self._makeWaitpidCountingOptions()
+ supervisord = self._makeOne(options)
+
+ supervisord.runforever()
+ self.assertEqual(waitpid_calls, [])
+
+ def test_runforever_reaps_after_sigchld_without_pids(self):
+ options, waitpid_calls = self._makeWaitpidCountingOptions()
+ options._signal = signal.SIGCHLD
+ supervisord = self._makeOne(options)
+
+ supervisord.runforever()
+ self.assertEqual(waitpid_calls, [1])
+
+ def test_runforever_reaps_with_tracked_pids(sel
[truncated]
Full published attempt: /issues/019e176a-65b7-7d92-a20c-8c276f7ca31a. Issue JSON: /v1/issues/019e176a-65b7-7d92-a20c-8c276f7ca31a
python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through _PyObject_GetMethod, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.
highpackage: python3.13-minimalsource: supervisorscore: 106reports: 2patch attempt readypatched: 2026-05-11 18:48 UTCvalidation: readyharvest: needs reviewblocked validation
Attempt Summary
python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Rerun in an environment where the blocked runtime or test validation can complete before upstream submission.
Suggested subject
supervisord: skip idle reaps without child activity
Commit message.
`supervisord` no longer asks the kernel for exited children on every idle main-loop pass when it has no tracked child PIDs and has not received `SIGCHLD`, the child-exit signal. The loop now handles any pending signal first and only calls the existing `options.waitpid()` wrapper when Supervisor is tracking children or when `SIGCHLD` indicates a child may need reaping.
Issue connection.
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` alternating one-second `poll()` timeouts with `wait4(-1, WNOHANG) = -1 ECHILD`, meaning the process was asking for exited children when the kernel reported that there were none. This was observed by Fixer and not independently reproduced as a syscall trace because ptrace/`strace` is blocked in this sandbox.
The hot process was Python, but the evidence identified the script entrypoint as `supervisord`, so the patch targets Supervisor application logic rather than the Python runtime. In `Supervisor.runforever()`, the code previously called `self.reap()` unconditionally after every poll; `reap()` uses Supervisor’s local `options.waitpid()` helper. The patch makes `handle_signal()` return the signal it consumed, and `runforever()` only reaps when `options.pidhistory` has tracked child PIDs or that signal is `SIGCHLD`.
The expected effect is to remove the unnecessary idle `waitpid` path when there are no known children to reap, while preserving normal cleanup for Supervisor-managed children and unknown children announced by `SIGCHLD`. I checked `README.rst`, `docs/development.rst`, `tox.ini`, and the touched subsystem; the relevant local helpers are `options.waitpid()`, `options.get_signal()`/`SignalReceiver`, and `options.poller`, and the patch keeps using them. Review round 1 only flagged EOF whitespace churn in `supervisor/tests/test_supervisord.py`; that incidental diff has been removed.
Diff Excerpt
diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..aa455e5 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -241,8 +241,9 @@ class Supervisor:
for group in pgroups:
group.transition()
- self.reap()
- self.handle_signal()
+ sig = self.handle_signal()
+ if self.options.pidhistory or sig == signal.SIGCHLD:
+ self.reap()
self.tick()
if self.options.mood < SupervisorStates.RUNNING:
@@ -312,6 +313,7 @@ class Supervisor:
else:
self.options.logger.blather(
'received %s indicating nothing' % signame(sig))
+ return sig
def get_state(self):
return self.options.mood
diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py
index f608b2b..1f25b58 100644
--- a/supervisor/tests/base.py
+++ b/supervisor/tests/base.py
@@ -60,6 +60,7 @@ class DummyOptions:
self.pidfile_written = False
self.directory = None
self.waitpid_return = None, None
+ self.waitpid_calls = 0
self.kills = {}
self._signal = None
self.parent_pipes_closed = None
@@ -145,6 +146,7 @@ class DummyOptions:
self.pidfile_written = True
def waitpid(self):
+ self.waitpid_calls += 1
return self.waitpid_return
def kill(self, pid, sig):
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index 3d7b4ff..10cebef 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -228,7 +228,8 @@ class SupervisordTests(unittest.TestCase):
options = DummyOptions()
options._signal = signal.SIGTERM
supervisord = self._makeOne(options)
- supervisord.handle_signal()
+ sig = supervisord.handle_signal()
+ self.assertEqual(sig, signal.SIGTERM)
self.assertEqual(supervisord.options.mood,
SupervisorStates.SHUTDOWN)
self.assertEqual(options.logger.data[0],
@@ -284,7 +285,8 @@ class SupervisordTests(unittest.TestCase):
options = DummyOptions()
options._signal = signal.SIGCHLD
supervisord = self._makeOne(options)
- supervisord.handle_signal()
+ sig = supervisord.handle_signal()
+ self.a
[truncated]
Full published attempt: /issues/019dcfd5-048f-7803-a659-fce9174999e2. Issue JSON: /v1/issues/019dcfd5-048f-7803-a659-fce9174999e2
redis-check-rdb is stuck in a likely busy poll loop: 9.10% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 1 thread(s) around 0x00007fbc72ca5ffe in ?? () and 1 thread(s) around 0x00007fbc72ca5ffe in ?? ().
highpackage: redis-toolssource: redisscore: 106reports: 2patch attempt readypatched: 2026-05-11 18:00 UTCvalidation: readyupstream: review
Attempt Summary
redis-check-rdb likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Suggested subject
zmalloc: cache the Linux proc stat fd
Commit message.
Redis samples RSS, the resident set size memory reported by the OS, through `/proc/self/stat` on Linux. Keep a cached proc stat file descriptor and read it with `pread()` so the periodic memory sampler does not reopen the same proc file on every server cron tick.
The cached descriptor is reopened after fork, because an inherited fd refers to the parent process’ concrete `/proc/<pid>/stat` file.
Issue connection.
Fixer observed a sleeping Redis process repeatedly returning from `epoll_wait` and then doing `openat`, `read`, and `close` on `/proc/self/stat` about every 100 ms. This failure signal was observed by Fixer and not independently reproduced.
The code-level cause is the Linux RSS helper path: `serverCron()` calls `cronUpdateMemoryStats()`, which calls `zmalloc_get_rss()`, which used `get_proc_stat_ll()` to open and close `/proc/self/stat` for each sample. I changed `get_proc_stat_ll()` to use a small cached fd opened with `O_CLOEXEC`, read via `pread()`, and reopened when `getpid()` changes after fork. I added a short source comment explaining that fork invariant.
This improves the prior Fixer patch by preserving Redis’ existing memory-stat sampling cadence instead of making cached memory stats staler. The expected effect is to remove the repeated `/proc/self/stat` open/close churn while keeping RSS samples updated as before.
Diff Excerpt
diff --git a/src/zmalloc.c b/src/zmalloc.c
index 7d9fe6e..7bce574 100644
--- a/src/zmalloc.c
+++ b/src/zmalloc.c
@@ -636,21 +636,47 @@ void zmadvise_dontneed(void *ptr) {
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
+
+static int proc_stat_fd = -1;
+static pid_t proc_stat_pid = -1;
+
+static void close_proc_stat_fd(void) {
+ if (proc_stat_fd != -1) close(proc_stat_fd);
+ proc_stat_fd = -1;
+ proc_stat_pid = -1;
+}
+
+static int get_proc_stat_fd(void) {
+ pid_t pid = getpid();
+
+ /* The fd refers to a concrete /proc/<pid>/stat file, so a forked child
+ * must not reuse the parent's cached descriptor. */
+ if (proc_stat_fd != -1 && proc_stat_pid != pid) close_proc_stat_fd();
+ if (proc_stat_fd == -1) {
+ proc_stat_fd = open("/proc/self/stat", O_RDONLY | O_CLOEXEC);
+ if (proc_stat_fd != -1) proc_stat_pid = pid;
+ }
+ return proc_stat_fd;
+}
#endif
/* Get the i'th field from "/proc/self/stat" note i is 1 based as appears in the 'proc' man page */
int get_proc_stat_ll(int i, long long *res) {
#if defined(HAVE_PROC_STAT)
char buf[4096];
- int fd, l;
+ ssize_t l = -1;
char *p, *x;
- if ((fd = open("/proc/self/stat",O_RDONLY)) == -1) return 0;
- if ((l = read(fd,buf,sizeof(buf)-1)) <= 0) {
- close(fd);
- return 0;
+ for (int attempts = 0; attempts < 2; attempts++) {
+ int fd = get_proc_stat_fd();
+ if (fd == -1) return 0;
+
+ l = pread(fd, buf, sizeof(buf)-1, 0);
+ if (l > 0) break;
+
+ close_proc_stat_fd();
}
- close(fd);
+ if (l <= 0) return 0;
buf[l] = '\0';
if (buf[l-1] == '\n') buf[l-1] = '\0';
Full published attempt: /issues/019dd8e9-fd09-7ce2-ad17-85a44beba62d. Issue JSON: /v1/issues/019dd8e9-fd09-7ce2-ad17-85a44beba62d
reviewer-reduced upstream
perl is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through Perl_runops_standard, with repeated pselect6 x4.
highpackage: perl-basesource: perlscore: 106reports: 3patch attempt readypatched: 2026-05-11 17:44 UTCvalidation: readyupstream: reviewer-reduced
Attempt Summary
perl likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Suggested subject
pp_sselect: preserve tiny positive select timeouts
Commit message.
Perl's four-argument `select` converts a Perl timeout value to `struct timeval`, the seconds/microseconds structure passed to the platform `select` call. A strictly positive timeout below one microsecond was truncated to `0s, 0us`, making it behave like an explicit non-blocking poll. Preserve that distinction by rounding such tiny positive timeouts up to one microsecond.
Issue connection.
Fixer observed `/usr/bin/perl` using CPU with samples in `Perl_runops_standard` and repeated `pselect6`, consistent with a busy poll loop. That failure was observed by Fixer and not independently reproduced because the evidence bundle has no script entrypoint, command line, or full strace body.
The cautious runtime-side cause addressed here is in `pp_sselect`: a positive Perl timeout smaller than one microsecond was truncated to a zero `struct timeval`, so `PerlSock_select` received a zero-time poll. I changed the conversion to keep the original non-negative timeout and set `tv_usec` to `1` when truncation would otherwise erase a positive timeout. I also fixed the local `Time::HiRes` test guard and added a regression in `t/op/sselect.t`.
The expected effect is that tiny positive `select` timeouts no longer spin as immediate polls. The patch follows the existing subsystem helpers and conventions already in `pp_sselect`, including `SvGETMAGIC`, `SvNV_nomg`, `PerlSock_select`, `Zero`, `Newx`, `Safefree`, `SvSETMAGIC`, and `SvSetMagicSV`; no new platform or allocation API was introduced. The prior Fixer patch was improved by using a more realistic timing threshold for the one-microsecond minimum.
Diff Excerpt
diff --git a/pp_sys.c b/pp_sys.c
index ed0ec79..000fb43 100644
--- a/pp_sys.c
+++ b/pp_sys.c
@@ -1356,6 +1356,7 @@ PP_wrapped(pp_sselect, 4, 0)
char *s;
SV *sv;
NV value;
+ NV orig_timeout;
I32 maxlen = 0;
I32 nfound;
struct timeval timebuf;
@@ -1439,12 +1440,15 @@ PP_wrapped(pp_sselect, 4, 0)
sv = SP[4];
SvGETMAGIC(sv);
if (SvOK(sv)) {
- value = SvNV_nomg(sv);
- if (value < 0.0)
- value = 0.0;
- timebuf.tv_sec = (time_t)value;
- value -= (NV)timebuf.tv_sec;
+ orig_timeout = SvNV_nomg(sv);
+ if (orig_timeout < 0.0)
+ orig_timeout = 0.0;
+ timebuf.tv_sec = (time_t)orig_timeout;
+ value = orig_timeout - (NV)timebuf.tv_sec;
timebuf.tv_usec = (long)(value * 1000000.0);
+ /* Keep a positive Perl timeout from becoming a zero-time poll. */
+ if (orig_timeout > 0.0 && timebuf.tv_sec == 0 && timebuf.tv_usec == 0)
+ timebuf.tv_usec = 1;
}
else
tbuf = NULL;
diff --git a/t/op/sselect.t b/t/op/sselect.t
index 32420cc..449fb2d 100644
--- a/t/op/sselect.t
+++ b/t/op/sselect.t
@@ -7,13 +7,13 @@ BEGIN {
chdir 't' if -d 't';
require './test.pl';
set_up_inc('.', '../lib');
- $hires = eval 'use Time::HiResx "time"; 1';
+ $hires = eval 'use Time::HiRes "time"; 1';
}
skip_all("Win32 miniperl has no socket select")
if $^O eq "MSWin32" && is_miniperl();
-plan (23);
+plan (24);
my $blank = "";
eval {select undef, $blank, $blank, 0};
@@ -79,6 +79,27 @@ $diff = $t1-$t0;
ok($diff >= $sleep-$under, "select(\$e,u,u,\$sleep): at least $sleep seconds have passed");
note("diff=$diff under=$under");
+if ($hires) {
+ my $tiny = 0.0000005;
+ my $iters = 10_000;
+ my $min_extra = 0.002;
+
+ $t0 = time;
+ select undef, undef, undef, 0 for 1..$iters;
+ my $zero_time = time - $t0;
+
+ $t0 = time;
+ select undef, undef, undef, $tiny for 1..$iters;
+ my $tiny_time = time - $t0;
+
+ ok($tiny_time >= $zero_time + $min_extra,
+ "select(u,u,u,small positive timeout): not a non-blocking poll");
+ note("zero_time=$zero_time tiny_time=$tiny_time min_extra=$min_extra");
+}
+else {
+ skip("Need Time::HiRes for tiny-timeout behavior check", 1);
+}
+
# [perl #120102] CORE::select ignoring timeout var's magic
{
Full published attempt: /issues/019d4017-20f8-7752-9734-0bcd71623118. Issue JSON: /v1/issues/019d4017-20f8-7752-9734-0bcd71623118
sshd-auth is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through unresolved offset, with repeated read x3, write x2, clock_nanosleep x1.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-11 17:06 UTCvalidation: readyharvest: needs reviewblocked validationmanual: fixer defect
Attempt Summary
sshd-auth likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Manual disposition
fixer defect: Do not submit retained LoginGraceTime patch; current upstream already arms the parent-side LoginGraceTime alarm unless a reproducer proves bypass.
Original harvest blockers
These blockers remain on the preserved local diff, but the manual disposition above is the current handoff.
Next proof/action
- Do not submit this retained diff; fix the Fixer collector/proposal rule before rerunning this issue family.
Suggested subject
sshd-auth: enforce LoginGraceTime during pre-authentication
Commit message.
Fixer observed `sshd-auth`, the split helper that handles pre-authentication SSH key exchange and user authentication, in a short trace around sleep, monitor writes, and `ppoll`. The trace does not prove an infinite spin, but it does show the pre-auth helper in the area that should be bounded by `LoginGraceTime`.
`sshd-session.c` already arms that timer before pre-authentication. Mirror that bounded timer setup in `sshd-auth.c`, and clear it after authentication succeeds.
Issue connection.
Fixer observed an `sshd-auth: root [net]` process with profiler evidence in `sshd-auth`, a dominant syscall sequence of `clock_nanosleep`, `write`, and `ppoll`, `/proc` state sleeping in `unix_stream_read_generic`, and a captured strace ending with `ECONNRESET` and exit. This was observed by Fixer and not independently reproduced.
The cautious code-level cause is that split `sshd-auth.c` reset `SIGALRM` to the default action and then entered key exchange and authentication without arming the configured `LoginGraceTime` alarm. I improved the prior Fixer patch by keeping the same bounded linear control flow, cleaning up style, and adding only the timer setup/clear logic already used by the nearby session path.
The change uses existing OpenSSH conventions and helpers: `ssh_signal`, `setitimer`, `timerclear`, `arc4random_uniform`, `fatal`, and `_exit(EXIT_LOGIN_GRACE)`. `README.md` was the only contribution/style document found; no relevant local file-I/O, path, process-spawn, allocation, or locking helper was needed for this change. The expected effect is that slow or wedged `sshd-auth` pre-authentication workers are terminated by the configured login grace timeout instead of being allowed to remain in pre-authentication indefinitely.
Diff Excerpt
diff --git a/sshd-auth.c b/sshd-auth.c
index 502a342..07384ca 100644
--- a/sshd-auth.c
+++ b/sshd-auth.c
@@ -175,6 +175,17 @@ mm_is_monitor(void)
return 0;
}
+/*
+ * Signal handler for the alarm after the login grace period has expired.
+ * As usual, this may only take signal-safe actions, even though it is
+ * terminal.
+ */
+static void
+grace_alarm_handler(int sig)
+{
+ _exit(EXIT_LOGIN_GRACE);
+}
+
static void
privsep_child_demote(void)
{
@@ -445,6 +456,7 @@ main(int ac, char **av)
Authctxt *authctxt;
struct connection_info *connection_info = NULL;
sigset_t sigmask;
+ struct itimerval itv;
uint64_t timing_secret = 0;
closefrom(PRIVSEP_MIN_FREE_FD);
@@ -725,6 +737,27 @@ main(int ac, char **av)
ssh_signal(SIGQUIT, SIG_DFL);
ssh_signal(SIGCHLD, SIG_DFL);
+ /*
+ * We don't want to listen forever unless the other side
+ * successfully authenticates itself. So we set up an alarm which is
+ * cleared after successful authentication. A limit of zero
+ * indicates no limit. Note that we don't set the alarm in debugging
+ * mode; it is just annoying to have the server exit just when you
+ * are about to discover the bug.
+ */
+ ssh_signal(SIGALRM, grace_alarm_handler);
+ if (!debug_flag && options.login_grace_time > 0) {
+ int ujitter = arc4random_uniform(4 * 1000000);
+
+ timerclear(&itv.it_interval);
+ itv.it_value.tv_sec = options.login_grace_time;
+ itv.it_value.tv_sec += ujitter / 1000000;
+ itv.it_value.tv_usec = ujitter % 1000000;
+
+ if (setitimer(ITIMER_REAL, &itv, NULL) == -1)
+ fatal("login grace time setitimer failed");
+ }
+
/* Prepare the channels layer */
channel_init_channels(ssh);
channel_set_af(ssh, options.address_family);
@@ -761,6 +794,16 @@ main(int ac, char **av)
do_ssh2_kex(ssh);
do_authentication2(ssh);
+ /*
+ * Cancel the alarm we set to limit the time taken for
+ * authentication.
+ */
+ timerclear(&itv.it_interval);
+ timerclear(&itv.it_value);
+ if (setitimer(ITIMER_REAL, &itv, NULL) == -1)
+ fatal("login grace time clear failed");
+ ssh_signal(SIGALRM, SIG_DFL);
+
/*
* The unprivileged child now transfers the current keystate and exits.
*/
Full published attempt: /issues/019e1519-78c6-7813-bebb-aeb6177e1897. Issue JSON: /v1/issues/019e1519-78c6-7813-bebb-aeb6177e1897
python3.13 is stuck in a likely busy poll loop: 100.00% of sampled CPU passed through PyDict_SetItem, with repeated thread backtraces show 1 thread(s) around 0x00007f1c6e7efe92 in pthread_attr_destroy () from target:<path>.
highpackage: python3.13-minimalsource: supervisorscore: 106reports: 2patch attempt readypatched: 2026-05-11 16:21 UTCvalidation: readyharvest: needs reviewblocked validation
Attempt Summary
python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Rerun in an environment where the blocked runtime or test validation can complete before upstream submission.
Suggested subject
Skip idle child reaping unless child work is pending
Commit message.
Supervisor’s main loop reaped children on every idle tick. Reaping uses `waitpid`, the POSIX call that checks for exited child processes, so idle configurations could repeatedly ask the kernel for child status even when Supervisor had no child PID to track. Only run the periodic reap when Supervisor has tracked child PIDs or has received `SIGCHLD`, the child-exited signal.
Issue connection.
Fixer observed a `supervisord` Python entrypoint with high CPU attribution, a sleeping process in `poll_schedule_timeout`, and a trace alternating between `poll(..., 1000)` and `wait4(-1, ..., WNOHANG) = -1 ECHILD`; `ECHILD` means the kernel had no child process status to return. The failure signal was observed by Fixer and not independently reproduced as a CPU runaway.
The cautious code-level inference is that `Supervisor.runforever()` called `self.reap()` after every poll cycle. `reap()` correctly goes through Supervisor’s local `ServerOptions.waitpid()` wrapper, but doing that while `pidhistory` is empty causes the no-child `wait4` path seen in the trace.
The change keeps the local wrappers and main-loop conventions: it reads the pending signal once through `options.get_signal()`, reaps only when `pidhistory` has tracked child PIDs or the pending signal is `SIGCHLD`, and passes that already-read signal into `handle_signal()`. I kept the prior Fixer patch’s useful direction, including the private sentinel that prevents `handle_signal(None)` from reading the signal queue a second time, and added a short comment explaining why `SIGCHLD` still reaps with empty `pidhistory`.
The expected effect is to remove unnecessary idle `waitpid`/`wait4` calls without suppressing normal managed-child or unknown-child cleanup.
Diff Excerpt
diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..d74bac6 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -45,6 +45,8 @@ from supervisor import events
from supervisor.states import SupervisorStates
from supervisor.states import getProcessStateDescription
+_NO_SIGNAL = object()
+
class Supervisor:
stopping = False # set after we detect that we are handling a stop request
lastshutdownreport = 0 # throttle for delayed process error reports at stop
@@ -241,8 +243,12 @@ class Supervisor:
for group in pgroups:
group.transition()
- self.reap()
- self.handle_signal()
+ sig = self.options.get_signal()
+ # A SIGCHLD can refer to a child missing from pidhistory; still
+ # reap so the existing unknown-pid cleanup path can run.
+ if self.options.pidhistory or sig == signal.SIGCHLD:
+ self.reap()
+ self.handle_signal(sig)
self.tick()
if self.options.mood < SupervisorStates.RUNNING:
@@ -285,8 +291,9 @@ class Supervisor:
# infinitely
self.reap(once=False, recursionguard=recursionguard+1)
- def handle_signal(self):
- sig = self.options.get_signal()
+ def handle_signal(self, sig=_NO_SIGNAL):
+ if sig is _NO_SIGNAL:
+ sig = self.options.get_signal()
if sig:
if sig in (signal.SIGTERM, signal.SIGINT, signal.SIGQUIT):
self.options.logger.warn(
diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py
index 3d7b4ff..7dea393 100644
--- a/supervisor/tests/test_supervisord.py
+++ b/supervisor/tests/test_supervisord.py
@@ -665,6 +665,58 @@ class SupervisordTests(unittest.TestCase):
supervisord.runforever()
self.assertEqual(len(supervisord.ticks), 3)
+ def test_runforever_skips_reap_without_child_pids(self):
+ options = DummyOptions()
+ options.test = True
+ waitpid_calls = []
+
+ def waitpid():
+ waitpid_calls.append(None)
+ return None, None
+
+ options.waitpid = waitpid
+ supervisord = self._makeOne(options)
+ supervisord.runforever()
+ self.assertEqual(waitpid_calls, [])
+
+ def test_runforever_reads_signal_once_without_chil
[truncated]
Full published attempt: /issues/019e16c2-04ff-7192-8ffd-8e77fa3407df. Issue JSON: /v1/issues/019e16c2-04ff-7192-8ffd-8e77fa3407df
python3.13 is stuck in a likely busy poll loop: 6.41% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 1 thread(s) around 0x00007fdd05c9b72e in ?? () and 1 thread(s) around 0x00007fdd05c9b72e in ?? ().
highpackage: python3.13-minimalsource: python3.13score: 106reports: 3patch attempt readypatched: 2026-05-11 15:58 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: closed-unmerged
Attempt Summary
python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Rerun in an environment where the blocked runtime or test validation can complete before upstream submission.
- Do not open another source-path-family PR from this retained diff; gather new reproduction evidence or a materially distinct fix before rerunning upstream.
Suggested subject
Avoid idle waitpid polling without dropping SIGCHLD reaping
Commit message.
Avoid an unnecessary idle `waitpid()` call in `supervisord` when there are no tracked child processes. Reaping means collecting an exited child process status with `waitpid()`. Supervisor records managed child PIDs in `options.pidhistory` when they are forked, so the periodic idle loop can skip reaping when that map is empty.
When `SIGCHLD`, the child-exit signal, is received, `supervisord` still forces one reap attempt so unknown children can be collected and logged.
Issue connection.
Fixer observed `/usr/bin/python3 /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf` repeatedly doing `wait4(-1, ..., WNOHANG) = -1 ECHILD` followed by a one-second `poll()` timeout. This failure was observed by Fixer and not independently reproduced as the full daemon/perf trace in this sandbox because `strace` is blocked by ptrace restrictions.
The code-level cause is that `Supervisor.runforever()` calls `reap()` every loop, and `reap()` called Supervisor’s local `Options.waitpid()` wrapper even when `options.pidhistory` had no managed child PIDs. A bounded local harness confirmed the targeted call path: the original source called real `os.waitpid(-1, os.WNOHANG)` with empty `pidhistory` and received `ECHILD`; the patched source made zero calls on the same idle path.
The patch makes ordinary idle `reap()` return before calling `waitpid()` when no managed child PID is tracked. To avoid dropping unknown-child cleanup, `SIGCHLD` handling calls `reap(force=True)`, and recursive reaping keeps draining after one child status is returned. I followed existing local conventions: `options.pidhistory`, `Supervisor.reap()`, `Options.waitpid()`, `SignalReceiver`, and `options.logger`.
The expected effect is that idle supervisord stops making pointless once-per-second nonblocking `waitpid()` calls that can only return `ECHILD`, while preserving child collection when a child-exit signal indicates there may be a status to reap. The short comments document the `pidhistory` invariant and the forced recursive drain case.
Diff Excerpt
diff --git a/supervisor/supervisord.py b/supervisor/supervisord.py
index 0a4f3e6..c2137bd 100755
--- a/supervisor/supervisord.py
+++ b/supervisor/supervisord.py
@@ -268,9 +268,12 @@ class Supervisor:
self.ticks[period] = this_tick
events.notify(event(this_tick, self))
- def reap(self, once=False, recursionguard=0):
+ def reap(self, once=False, recursionguard=0, force=False):
if recursionguard == 100:
return
+ if not force and not self.options.pidhistory:
+ # Managed child PIDs are recorded in pidhistory at fork time.
+ return
pid, sts = self.options.waitpid()
if pid:
process = self.options.pidhistory.get(pid, None)
@@ -282,8 +285,12 @@ class Supervisor:
del self.options.pidhistory[pid]
if not once:
# keep reaping until no more kids to reap, but don't recurse
- # infinitely
- self.reap(once=False, recursionguard=recursionguard+1)
+ # infinitely. Once waitpid() returns a child, keep draining
+ # even if pidhistory becomes empty.
+ self.reap(
+ once=False,
+ recursionguard=recursionguard+1,
+ force=True)
def handle_signal(self):
sig = self.options.get_signal()
@@ -303,6 +310,7 @@ class Supervisor:
elif sig == signal.SIGCHLD:
self.options.logger.debug(
'received %s indicating a child quit' % signame(sig))
+ self.reap(once=True, force=True)
elif sig == signal.SIGUSR2:
self.options.logger.info(
'received %s indicating log reopen request' % signame(sig))
diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py
index f608b2b..1f25b58 100644
--- a/supervisor/tests/base.py
+++ b/supervisor/tests/base.py
@@ -60,6 +60,7 @@ class DummyOptions:
self.pidfile_written = False
self.directory = None
self.waitpid_return = None, None
+ self.waitpid_calls = 0
self.kills = {}
self._signal = None
self.parent_pipes_closed = None
@@ -145,6 +146,7 @@ class DummyOptions:
self.pidfile_written = True
def waitpid(self):
+ self.waitpid_calls += 1
return self.wai
[truncated]
Full published attempt: /issues/019d7cef-46dd-7541-b939-9ba6872fbf76. Issue JSON: /v1/issues/019d7cef-46dd-7541-b939-9ba6872fbf76
redis-check-rdb is stuck in a likely busy poll loop: 6.51% of sampled CPU passed through unresolved offset, with repeated thread backtraces show 1 thread(s) around 0x00007f89688a74be in ?? () and 1 thread(s) around 0x00007f89688a74be in ?? ().
highpackage: redis-toolssource: redisscore: 106reports: 2patch attempt readypatched: 2026-05-11 14:55 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: alternative review
Attempt Summary
redis-check-rdb likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Suggested subject
Reduce idle RSS polling in memory cron stats
Commit message.
Redis samples process RSS, resident set size, from `serverCron()`, the periodic maintenance loop. On Linux this calls `zmalloc_get_rss()`, which reads `/proc/self/stat`; doing that on the 100 ms allocator-stat cadence creates steady procfs traffic even when Redis is idle.
Sample process RSS once per second instead, while keeping allocator stats on the existing 100 ms cadence. Keep `process_rss` and `zmalloc_used` sampled together because Redis uses their ratio for total memory fragmentation reporting.
Issue connection.
Fixer observed a `redis-server` process repeatedly returning from `epoll_wait` and then doing `openat`/`read`/`close` on `/proc/self/stat`. This failure signal was observed by Fixer and not independently reproduced locally because `strace`/ptrace is blocked in this sandbox.
The code-level cause matches the collected trace: `serverCron()` calls `cronUpdateMemoryStats()`, and that function sampled `zmalloc_get_rss()` every 100 ms. On Linux, `zmalloc_get_rss()` reads `/proc/self/stat`.
I changed `src/server.c` so process RSS sampling uses Redis’ existing `run_with_period(...)` cron convention on a 1000 ms cadence, while allocator stats stay on the existing 100 ms cadence. The code continues using the local `zmalloc_get_rss()`, `zmalloc_used_memory()`, and allocator-stat helpers; I found no relevant local procfs file-IO wrapper to use instead. I also added a short comment explaining the invariant that RSS and `zmalloc_used` must stay paired for the fragmentation ratio.
The expected effect is to reduce idle `/proc/self/stat` reads from Redis’ memory telemetry path without changing the generic RSS reader or Debian packaging. I reviewed `CONTRIBUTING.md`, `README.md`, the prior Fixer patch, and the local memory/cron helpers before editing.
Diff Excerpt
diff --git a/src/server.c b/src/server.c
index 241fe69..56f42d4 100644
--- a/src/server.c
+++ b/src/server.c
@@ -1425,12 +1425,18 @@ void updatePeakMemory(void) {
void cronUpdateMemoryStats(void) {
updatePeakMemory();
- run_with_period(100) {
- /* Sample the RSS and other metrics here since this is a relatively slow call.
- * We must sample the zmalloc_used at the same time we take the rss, otherwise
- * the frag ratio calculate may be off (ratio of two samples at different times) */
+ int update_process_rss = server.cron_malloc_stats.zmalloc_used == 0;
+ run_with_period(1000) {
+ update_process_rss = 1;
+ }
+ if (update_process_rss) {
+ /* Keep process RSS and zmalloc_used paired; INFO uses their ratio
+ * as total process memory fragmentation. */
server.cron_malloc_stats.process_rss = zmalloc_get_rss();
server.cron_malloc_stats.zmalloc_used = zmalloc_used_memory();
+ }
+
+ run_with_period(100) {
/* Sampling the allocator info can be slow too.
* The fragmentation ratio it'll show is potentially more accurate
* it excludes other RSS pages such as: shared libraries, LUA and other non-zmalloc
Full published attempt: /issues/019ddbca-7201-7d22-ba77-0c6c084821ee. Issue JSON: /v1/issues/019ddbca-7201-7d22-ba77-0c6c084821ee
sshd-auth is stuck in a likely timer churn loop: 100.00% of sampled CPU passed through intel_iommu_map_pages, with repeated thread backtraces show 1 thread(s) around 0x00007f4ff3a9a7d2 in ?? () from target:<path>.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-11 14:45 UTCvalidation: readyharvest: needs reviewblocked validationrelated upstream: closed-unmerged
Attempt Summary
sshd-auth likely remains stuck in a timer churn loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Rerun in an environment where the blocked runtime or test validation can complete before upstream submission.
- Do not open another source-path-family PR from this retained diff; gather new reproduction evidence or a materially distinct fix before rerunning upstream.
Suggested subject
auth2: cap failure-delay catch-up after slow auth attempts
Commit message.
The pre-authentication failure delay groups failed authentication attempts into coarse timing buckets. If an authentication backend was already slow, the catch-up loop could still double the target delay until it passed elapsed time, then add a large extra sleep.
Keep the bucketed timing behavior, but cap extra catch-up padding after slow backend work. This preserves the existing timing mitigation while avoiding second-scale sleeps after already slow failed authentication attempts.
Issue connection.
Fixer observed `sshd-auth: root [net]` sleeping with a backtrace through `nanosleep()`/`clock_nanosleep()` and strace showing a `read, read, clock_nanosleep` sequence with sleeps around 1.5-1.8 seconds. This failure was observed by Fixer and not independently reproduced.
The cautious code-level connection is `auth2.c`’s pre-authentication failure delay. “Pre-authentication” here means the server-side phase before a login session is accepted. `ensure_minimum_time_since()` already grouped failed auth attempts into timing buckets, but its catch-up loop could add a large delay after backend work had already exceeded the initial 5-9ms per-user delay.
The patch adds a 250ms cap for that extra catch-up padding while keeping the existing bucket loop. The expected effect is to reduce avoidable long post-auth sleeps while retaining coarse timing equalization for failed authentication. I kept the refined prior Fixer approach, avoided new `goto` flow, and added a short comment describing the invariant.
I checked `README`, `README.md`, and the touched auth subsystem. The change follows local OpenSSH conventions and existing helpers: `monotime_double()`, `debug3_f()`, `fatal_f()`, `xmalloc()`, `freezero()`, and the existing direct portable `nanosleep()` use in this helper. No relevant higher-level sleep helper was found, and no new file, process, allocation, locking, networking, or platform APIs were introduced.
Diff Excerpt
diff --git a/auth2.c b/auth2.c
index 80f766e..334b564 100644
--- a/auth2.c
+++ b/auth2.c
@@ -225,6 +225,7 @@ input_service_request(int type, uint32_t seq, struct ssh *ssh)
#define MIN_FAIL_DELAY_SECONDS 0.005
#define MAX_FAIL_DELAY_SECONDS 5.0
+#define MAX_FAIL_DELAY_PAD_SECONDS 0.250
static double
user_specific_delay(const char *user)
{
@@ -256,9 +257,18 @@ ensure_minimum_time_since(double start, double seconds)
return;
}
- /* if we've already passed the requested time, scale up */
- while ((remain = seconds - elapsed) < 0.0)
+ /*
+ * Keep failures in coarse timing buckets, but don't let catch-up
+ * padding after a slow backend add a large extra delay.
+ */
+ while ((remain = seconds - elapsed) < 0.0) {
seconds *= 2;
+ if (seconds - elapsed > MAX_FAIL_DELAY_PAD_SECONDS) {
+ seconds = elapsed + MAX_FAIL_DELAY_PAD_SECONDS;
+ break;
+ }
+ }
+ remain = seconds - elapsed;
ts.tv_sec = remain;
ts.tv_nsec = (remain - ts.tv_sec) * 1000000000;
Full published attempt: /issues/019e1618-a319-7110-8975-2781a193d675. Issue JSON: /v1/issues/019e1618-a319-7110-8975-2781a193d675
sshd-auth is stuck in a likely busy poll loop: 50.00% of sampled CPU passed through sched_balance_newidle, with repeated read x3, write x2, clock_nanosleep x1.
highpackage: openssh-serversource: opensshscore: 106reports: 2patch attempt readypatched: 2026-05-10 19:18 UTCvalidation: readyharvest: needs reviewlimited validationrelated upstream: closed-unmerged
Attempt Summary
sshd-auth likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Next proof/action
- Add independent reproduction or stronger project/runtime validation before upstream submission.
- Do not open another source-path-family PR from this retained diff; gather new reproduction evidence or a materially distinct fix before rerunning upstream.
Suggested subject
sshd-auth: interrupt auth failure delay on peer close
Commit message.
After a failed authentication request, `sshd-auth` enforces a delay before sending failure. Make that delay wake when the client connection is closed, while preserving the delay for live clients.
Issue connection.
Fixer observed `sshd-auth` sleeping in `clock_nanosleep`, then writing to the client fd, polling it, and finally seeing `ECONNRESET`. `/proc` showed the process was sleeping with low CPU, so the likely root cause is delayed cleanup after the peer disconnects during the authentication failure delay, not a reproduced CPU runaway. This failure was observed by Fixer and not independently reproduced.
The code-level cause is that `ensure_minimum_time_since()` used unconditional `nanosleep()`, so a preauth child could wait out the whole delay before discovering that the client had already gone away. I changed it to wait with `ppoll()` on `ssh_packet_get_connection_in(ssh)`, request `POLLIN` for portable `ppoll()` compatibility, and use `recv(..., MSG_PEEK)` to distinguish EOF from queued data without consuming SSH packet input. The expected effect is that a peer close during the auth failure delay returns through the existing dispatch error path instead of waiting and then writing to a reset socket.
Review findings addressed: the patch no longer relies on unsupported compat `POLLHUP`/`POLLERR` reporting with no requested events, and `make tests` was run and its environment blocker is reported. I also kept the short comment explaining the `POLLIN` plus peek invariant.
Local conventions checked: `README.md` documents `./configure && make tests`; nearby code uses `ppoll()`, `monotime_double()`, `ssh_packet_get_connection_in()`, `debug3_f()`, and `SSH_ERR_*`. `misc.c` has `waitrfd()` and `ptimeout` helpers, but `waitrfd()` would spin on already queued live-client input here, so the local bounded loop is used instead.
Diff Excerpt
diff --git a/auth2.c b/auth2.c
index 80f766e..55b2b71 100644
--- a/auth2.c
+++ b/auth2.c
@@ -31,6 +31,7 @@
#include <fcntl.h>
#include <limits.h>
+#include <poll.h>
#include <pwd.h>
#include <stdarg.h>
#include <string.h>
@@ -244,27 +245,76 @@ user_specific_delay(const char *user)
return MIN_FAIL_DELAY_SECONDS + delay;
}
-static void
-ensure_minimum_time_since(double start, double seconds)
+static int
+ensure_minimum_time_since(struct ssh *ssh, double start, double seconds)
{
struct timespec ts;
+ struct pollfd pfd;
double elapsed = monotime_double() - start, req = seconds, remain;
+ ssize_t n;
+ char c;
if (elapsed > MAX_FAIL_DELAY_SECONDS) {
debug3_f("elapsed %0.3lfms exceeded the max delay "
"requested %0.3lfms)", elapsed*1000, req*1000);
- return;
+ return 0;
}
/* if we've already passed the requested time, scale up */
while ((remain = seconds - elapsed) < 0.0)
seconds *= 2;
- ts.tv_sec = remain;
- ts.tv_nsec = (remain - ts.tv_sec) * 1000000000;
debug3_f("elapsed %0.3lfms, delaying %0.3lfms (requested %0.3lfms)",
elapsed*1000, remain*1000, req*1000);
- nanosleep(&ts, NULL);
+
+ pfd.fd = ssh_packet_get_connection_in(ssh);
+ /*
+ * Portable ppoll() compatibility reports requested events only.
+ * EOF is readable, so request POLLIN and peek without consuming data.
+ */
+ pfd.events = POLLIN;
+ while (remain > 0.0) {
+ ts.tv_sec = remain;
+ ts.tv_nsec = (remain - ts.tv_sec) * 1000000000;
+ pfd.revents = 0;
+ if (ppoll(&pfd, 1, &ts, NULL) == -1) {
+ if (errno != EAGAIN && errno != EINTR &&
+ errno != EWOULDBLOCK)
+ return SSH_ERR_SYSTEM_ERROR;
+ } else if (pfd.revents & POLLNVAL) {
+ errno = EBADF;
+ return SSH_ERR_SYSTEM_ERROR;
+ } else if (pfd.revents & (POLLIN|POLLERR|POLLHUP)) {
+ n = recv(pfd.fd, &c, 1, MSG_PEEK);
+ if (n == 0) {
+ debug3_f("peer disconnected during auth "
+ "failure delay");
+ return SSH_ERR_CONN_CLOSED;
+ }
+ if (n == -1) {
+ if (errno != EAGAIN && errno != EINTR &&
+ errno != EWOULDBLOCK)
+ return SSH_ERR_SYSTEM_ERROR;
+ } else {
+ /*
+ * Input is queued from a live client. Leave
+ * it queued and finish the remaining delay
+ * without polling it again, otherwise we would
+ * spin on the same byte.
+ */
+ remain = seconds - (monotime_double() - start);
+ if (remain > 0.0) {
+ ts.tv_sec = r
[truncated]
Full published attempt: /issues/019e1143-27cd-7d43-a2e8-5b81b4f87038. Issue JSON: /v1/issues/019e1143-27cd-7d43-a2e8-5b81b4f87038
python3.13 is stuck in a likely busy poll loop: 50.00% of sampled CPU passed through _PyEval_EvalFrameDefault, with repeated thread backtraces show 1 thread(s) around 0x00007f091ce537d2 in ?? () from target:<path>.
highpackage: python3.13-minimalsource: python3.13score: 106reports: 2patch attempt readypatched: 2026-04-30 22:10 UTCvalidation: readyupstream: coverage: merged
Attempt Summary
python3.13 likely remains stuck in a busy-poll loop. A diagnosis report and patch proposal were created locally.
Original harvest blockers
These blockers remain on the preserved local diff, but the upstream review above is the current handoff.
Suggested subject
subprocess: keep clamped pidfd waits looping
Commit message.
Use pidfd polling for POSIX `Popen.wait(timeout=...)` when available, and keep very large or infinite Python timeouts compatible by clamping each `poll()` call while continuing to recompute the Python deadline.
Issue connection.
The user-visible symptom was a `python3.13` process repeatedly waking in `poll` and `wait4`, with CPU samples in Python frame evaluation. A plausible code-level cause is the POSIX `subprocess.Popen._wait(timeout)` loop, which uses repeated nonblocking `waitpid(..., WNOHANG)` calls with short sleeps.
The patch adds a Linux pidfd-backed wait path for timeout waits. This refinement addresses the review finding by treating an empty result from a clamped `poll()` as one elapsed kernel-sized wait, then continuing the loop while the Python timeout still has time remaining. The regression test now covers the case where the first clamped poll returns no events and a later poll observes process exit.
The expected effect is fewer avoidable userspace wakeups for normal timeout waits on Linux with pidfds, while preserving behavior for very large and infinite timeouts. The code includes short comments for the non-obvious timeout clamping and pidfd/readiness remapping logic.
Diff Excerpt
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 3a8c743..b41920b 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -2049,6 +2049,74 @@ class Popen:
sts = 0
return (pid, sts)
+ def _wait_pidfd(self, endtime, orig_timeout):
+ """Wait for process exit using a pidfd."""
+ if not hasattr(os, "pidfd_open") or not hasattr(select, "poll"):
+ return False
+
+ if self._waitpid_lock.acquire(False):
+ try:
+ if self.returncode is not None:
+ return True # Another thread waited.
+ (pid, sts) = self._try_wait(os.WNOHANG)
+ assert pid == self.pid or pid == 0
+ if pid == self.pid:
+ self._handle_exitstatus(sts)
+ return True
+ finally:
+ self._waitpid_lock.release()
+ else:
+ return False
+
+ try:
+ pidfd = os.pidfd_open(self.pid)
+ except OSError:
+ return False
+
+ try:
+ poller = select.poll()
+ poller.register(pidfd, select.POLLIN)
+ while self.returncode is None:
+ remaining = self._remaining_time(endtime)
+ if remaining <= 0:
+ raise TimeoutExpired(self.args, orig_timeout)
+ # Preserve the legacy timeout loop's handling of NaN.
+ if remaining != remaining:
+ return False
+
+ # select.poll() accepts a signed int millisecond
+ # timeout. Clamp longer waits and recheck the Python
+ # deadline after each poll returns.
+ max_timeout = 2_147_483_647
+ clamped = remaining >= max_timeout / 1000
+ if clamped:
+ timeout = max_timeout
+ else:
+ timeout = max(1, int(remaining * 1000 + 0.999))
+ if not poller.poll(timeout):
+ if clamped:
+ continue
+ raise TimeoutExpired(self.args, orig_timeout)
+
+ if self._waitpid_lock.acquire(False):
+
[truncated]
Full published attempt: /issues/019dda71-4897-7d62-853d-31f4899d9f8e. Issue JSON: /v1/issues/019dda71-4897-7d62-853d-31f4899d9f8e
KDE keyboard layout config enables 3 layout(s) with Spare Layouts loop count 2 and Caps Lock is configured as a layout switch, while /etc/default/keyboard still describes a different XKB layout set, which points at plasma-desktop keyboard layout handling rather than a generic Wayland graphics failure.
mediumpackage: plasma-desktopsource: plasma-desktopscore: 98reports: 1patch attempt readypatched: 2026-04-04 19:47 UTCvalidation: readyupstream: review
Attempt Summary
KDE Wayland keyboard layout stack likely remains stuck in a desktop input config mismatch loop. A diagnosis report and patch proposal were created locally.
Original harvest blockers
These blockers remain on the preserved local diff, but the upstream review above is the current handoff.
Suggested subject
kcms/keyboard: preserve last-used layout across spare-layout swaps
Commit message.
Keep Plasma's public layout order aligned with the order saved in its keyboard settings, and remember the previous layout before rebuilding the live layout list for Spare Layouts.
Issue connection.
Plasma stores three layouts here, but only two should stay in the normal switching cycle while the third remains spare. The bug was that Plasma leaked its temporary live reordering back into the layout list and indices seen by the applet and shortcuts, so selecting a spare layout could confuse direct selection. This patch keeps the public order in the configured order while letting the internal live switcher keep doing its temporary spare-layout swap. It also remembers the previous layout before rebuilding the live list, so the 'last used layout' action still returns to the layout the user actually came from.
Diff Excerpt
diff --git a/kcms/keyboard/keyboard_daemon.cpp b/kcms/keyboard/keyboard_daemon.cpp
index ebf2d4e..767b9dc 100644
--- a/kcms/keyboard/keyboard_daemon.cpp
+++ b/kcms/keyboard/keyboard_daemon.cpp
@@ -114,7 +114,7 @@ void KeyboardDaemon::registerShortcut()
QAction *lastUsedLayoutAction = actionCollection->getLastUsedLayoutAction();
connect(lastUsedLayoutAction, &QAction::triggered, this, [this]() {
- auto layoutsList = X11Helper::getLayoutsList();
+ const auto layoutsList = logicalLayouts();
if (!lastUsedLayout.has_value() || layoutsList.count() <= *lastUsedLayout) {
switchToPreviousLayout();
} else {
@@ -222,46 +222,40 @@ bool KeyboardDaemon::setLayout(QAction *action)
bool KeyboardDaemon::setLayout(uint index)
{
- if (keyboardSettings->layoutLoopCount() != KeyboardConfig::NO_LOOPING && index >= uint(keyboardSettings->layoutLoopCount())) {
- QList<LayoutUnit> layouts = X11Helper::getLayoutsList();
- const uint indexOfLastMainLayoutInConfig = keyboardConfig->layouts().lastIndexOf(layouts.takeLast());
- const uint indexOfLastMainLayoutInXKB = layouts.size();
-
- // Re-calculate indexes for layout switching Actions
- const auto &actions = actionCollection->actions();
- for (const auto &action : actions) {
- // clang-format off
- if (action->data().toUInt() == indexOfLastMainLayoutInXKB) {
- action->setData(indexOfLastMainLayoutInConfig < index ?
- indexOfLastMainLayoutInConfig + 1 :
- indexOfLastMainLayoutInConfig);
- } else if (action->data().toUInt() == index) {
- action->setData(indexOfLastMainLayoutInXKB);
- } else if (index < indexOfLastMainLayoutInConfig
- && index < action->data().toUInt() && action->data().toUInt() <= indexOfLastMainLayoutInConfig) {
- action->setData(action->data().toUInt() - 1);
- } else if (indexOfLastMainLayoutInConfig < index
- && indexOfLastMainLayoutInConfig < action->data().toUInt() && action->data().toUInt() < index) {
- action->setData(action->data().toUInt() + 1);
- }
- // clang-format on
+ const auto configuredLayouts = keyboardConfig->layouts();
[truncated]
Full published attempt: /issues/019d5954-5300-75b1-b0a0-16d9cf5259e1. Issue JSON: /v1/issues/019d5954-5300-75b1-b0a0-16d9cf5259e1
python3.13 is stuck in a likely busy poll loop: 50.00% of sampled CPU passed through __vdso_clock_gettime, with repeated poll x5, wait4 x5, restart_syscall x1.
highpackage: python3.13-minimalsource: python3.13score: 106reports: 1patch attempt readypatched: 2026-04-01 23:33 UTCvalidation: readyharvest: needs reviewmissing patch metadatarelated upstream: merged
Attempt Summary
python3.13 likely remains stuck in a busy poll loop. A diagnosis report and patch proposal were created locally.
Harvest review needed
The diff is preserved for inspection, but Fixer should not treat it as upstream-ready until these blockers are cleared.
Suggested subject
subprocess: reduce wakeups in timed POSIX `Popen._wait` loop
Commit message.
Reduce wakeup churn in the POSIX `Popen._wait(timeout=...)` path by keeping a bounded, monotonically increasing poll delay and checking completion in a single explicit loop condition. This lowers the number of `waitpid`/sleep wakeups under timeouts while preserving `TimeoutExpired` behavior and lock-based returncode handoff semantics. A focused regression test was added to validate that the timeout path uses backoff progression instead of immediate repeated checks.
Issue connection.
The evidence shows a user-space timed loop pattern (`poll`/`wait4` alternating, `wchan=poll_schedule_timeout`, `__vdso_clock_gettime` hot) consistent with a high-frequency poll-and-sleep wait strategy rather than efficient blocking waits. In CPython’s `_wait` implementation this is exactly the `timeout` branch that loops with `waitpid(WNOHANG)` and short sleeps, which had been labeled a “busy loop” in-source. The patch tightens that exact branch: it keeps the same deadline calculation and lock gate, but moves the flow into a single `while self.returncode is None` loop with an explicit timeout check and exponential backoff capped by both the remaining timeout and 50 ms. This directly reduces repeated short wakeups that match the perf signature and keeps the process from cycling too quickly.
The added test verifies that the timed loop performs two increasingly spaced sleeps (`0.001` then `0.002`) before handling a terminal `waitpid` result, confirming the new behavior is bounded and backoff-driven without introducing timing flakiness.
Diff Excerpt
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 3a8c743..25ddcb2 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -2057,21 +2057,20 @@ class Popen:
if timeout is not None:
endtime = _time() + timeout
- # Enter a busy loop if we have a timeout. This busy loop was
- # cribbed from Lib/threading.py in Thread.wait() at r71065.
- delay = 0.0005 # 500 us -> initial delay of 1 ms
- while True:
+ # Enter a timed polling loop if we have a timeout.
+ # This follows Thread.wait()'s bounded backoff strategy.
+ delay = 0.001
+ while self.returncode is None:
if self._waitpid_lock.acquire(False):
try:
- if self.returncode is not None:
- break # Another thread waited.
(pid, sts) = self._try_wait(os.WNOHANG)
assert pid == self.pid or pid == 0
if pid == self.pid:
self._handle_exitstatus(sts)
- break
finally:
self._waitpid_lock.release()
+ if self.returncode is not None:
+ break
remaining = self._remaining_time(endtime)
if remaining <= 0:
raise TimeoutExpired(self.args, timeout)
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index a9f0ce2..3d54ba6 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -1427,6 +1427,32 @@ class ProcessTestCase(BaseTestCase):
self.assertIn("0.0001", str(c.exception)) # For coverage of __str__.
self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0)
+ @unittest.skipIf(mswindows, "POSIX-only waitpid backoff behavior")
+ def test_wait_timeout_backoff(self):
+ proc = subprocess.Popen.__new__(subprocess.Popen)
+ proc.args = ("fake",)
+ proc.pid = 99999
+ proc.returncode = None
+ proc._waitpid_lock = threading.Lock()
+ proc._try_wait = mock.Mock(side_effect=[
+ (0, 0),
+ (0, 0),
+ (proc.pid, 0),
+ ])
+ proc._h
[truncated]
Full published attempt: /issues/019d3b72-5616-7453-a770-da8708ebd78d. Issue JSON: /v1/issues/019d3b72-5616-7453-a770-da8708ebd78d
postgres is stuck in a likely file not found retry loop: 5.83% of sampled CPU passed through next_uptodate_folio, with repeated read x4620, lseek x2688, close x1428.
highpackage: postgresql-18source: postgresql-18score: 106reports: 2patch attempt readypatched: 2026-03-29 16:13 UTCvalidation: readyharvest: needs reviewmissing patch metadatamanual: semantic risk
Attempt Summary
postgres likely remains stuck in a file not found retry loop. A diagnosis report and patch proposal were created locally.
Manual disposition
semantic risk: Retained diff changes visible $libdir/foo lookup precedence when both unsuffixed and .so files exist; keep as evidence for a fresh semantics-preserving patch.
Original harvest blockers
These blockers remain on the preserved local diff, but the manual disposition above is the current handoff.
Next proof/action
- Do not submit this retained diff; design a semantics-preserving replacement with a failing/passing proof first.
Suggested subject
postgresql-18: update dfmgr.c for file not found retry loop
Commit message.
MODULE_PATHNAME expands to "$libdir/foo" without DLSUFFIX. Prefer the suffixed shared library name first to avoid a guaranteed failed probe for the unsuffixed name, but preserve compatibility by falling back to the existing lookup path if the suffixed file is not present.
Issue connection.
Fixer observed file not found retry loop. This patch updates src/backend/utils/fmgr/dfmgr.c so mODULE_PATHNAME expands to "$libdir/foo" without DLSUFFIX. Prefer the suffixed shared library name first to avoid a guaranteed failed probe for the unsuffixed name, but preserve compatibility by falling back to the existing lookup path if the suffixed file is not present.
Diff Excerpt
--- a/src/backend/utils/fmgr/dfmgr.c 2026-02-24 01:56:43.000000000 +0400
+++ b/src/backend/utils/fmgr/dfmgr.c 2026-03-29 20:11:53.195108902 +0400
@@ -20,6 +20,7 @@
#include <dlfcn.h>
#endif /* !WIN32 */
+#include "common/string.h"
#include "fmgr.h"
#include "lib/stringinfo.h"
#include "miscadmin.h"
@@ -96,6 +97,7 @@
bool signalNotFound, void **filehandle)
{
char *fullname;
+ char *filename_with_suffix = NULL;
void *lib_handle;
void *retval;
@@ -111,12 +113,36 @@
if (strncmp(filename, "$libdir/", 8) == 0)
{
if (first_dir_separator(filename + 8) == NULL)
+ {
filename += 8;
+
+ /*
+ * MODULE_PATHNAME expands to "$libdir/foo" without DLSUFFIX.
+ * Prefer the suffixed shared library name first to avoid a
+ * guaranteed failed probe for the unsuffixed name, but preserve
+ * compatibility by falling back to the existing lookup path if the
+ * suffixed file is not present.
+ */
+ if (!pg_str_endswith(filename, DLSUFFIX))
+ {
+ filename_with_suffix = psprintf("%s%s", filename, DLSUFFIX);
+ fullname = find_in_path(filename_with_suffix,
+ Dynamic_library_path,
+ "dynamic_library_path",
+ "$libdir", pkglib_path);
+ if (fullname != NULL)
+ goto have_fullname;
+ }
+ }
}
/* Expand the possibly-abbreviated filename to an exact path name */
fullname = expand_dynamic_library_name(filename);
+have_fullname:
+ if (filename_with_suffix)
+ pfree(filename_with_suffix);
+
/* Load the shared library, unless we already did */
lib_handle = internal_load_library(fullname);
Full published attempt: /issues/019d3a17-b958-7a60-86fd-5304e697f42c. Issue JSON: /v1/issues/019d3a17-b958-7a60-86fd-5304e697f42c