---
title: "CVE-2024-6387: How an OpenSSH Timeout Could Lead to Remote Root Access"
description: "A clear reconstruction of regreSSHion: how an unauthenticated connection triggered unsafe signal-time logging in sshd, why that could corrupt the heap, which systems were exposed, and how OpenSSH 9.8p1 fixed the root cause."
date: 2025-09-18
analysisType: "Root Cause Analysis"
cves: ["CVE-2024-6387"]
author: Sevban Dönmez (@jankesec)
canonical: https://jankesec.com/research/openssh-signal-race-cve-2024-6387/
---

## CVE-2024-6387 in 60 seconds

OpenSSH's `sshd` is the server process that accepts remote SSH connections. A client is allowed a
limited time to authenticate. If that time expires, the operating system delivers a `SIGALRM`
signal so `sshd` can terminate the unfinished session.

In Portable OpenSSH 8.5p1 through 9.7p1, that alarm handler called logging code. Logging looks
harmless, but on glibc-based Linux systems `syslog()` can call complex memory-management functions
such as `malloc()` and `free()`. A signal can interrupt the normal program at almost any instant —
including while the same heap allocator is halfway through updating its internal state.

If the alarm arrived during that narrow window, the handler could enter the allocator a second time
before the first operation finished. The immediate result might be a crash. With carefully shaped
SSH messages and many repeated timing attempts, however, Qualys demonstrated that this inconsistent
heap state could be turned into **unauthenticated remote code execution as root** on a 32-bit
Linux/glibc target.

The attack path was:

```text
remote client opens an SSH connection
  -> client deliberately does not complete authentication
  -> LoginGraceTime expires and SIGALRM interrupts sshd
  -> signal handler calls unsafe logging code
  -> syslog re-enters malloc/free while heap state may be incomplete
  -> repeated attempts try to win the timing race
  -> attacker-controlled heap corruption may reach code execution as root
```

No valid username, password, or SSH key was required to reach the vulnerable path. The difficulty
was timing: the attacker had to make the signal arrive during a tiny unsafe window and repeat the
attempt enough times to overcome ASLR and connection limits.

## What are sshd, LoginGraceTime, and SIGALRM?

`sshd` is OpenSSH's internet-facing server daemon. It starts processing a connection before the
remote user has authenticated because it must negotiate the protocol and examine authentication
messages. Some of this pre-authentication work occurs in a privileged process.

`LoginGraceTime` is the maximum time a client may spend authenticating. The usual upstream default
was 120 seconds. When the deadline expires, `sshd` receives `SIGALRM`, an asynchronous operating-
system signal.

“Asynchronous” is the important word. An ordinary function call happens at a point chosen by the
program. A signal handler can begin while an unrelated operation is temporarily holding a lock or
has only half-updated a data structure. POSIX therefore permits only a small set of
async-signal-safe operations inside a handler. General logging is not one of them.

The vulnerability did not exist because SSH had a timeout. It existed because timeout handling
performed work that was unsafe in an asynchronous and privileged execution context.

## What happened?

The public record supports the following chronology:

1. In 2006, OpenSSH fixed CVE-2006-5051 by making the fatal signal path terminate with `_exit(1)`
   instead of reaching unsafe logging behavior.
2. An October 2020 logging refactor accidentally removed the `DO_LOG_SAFE_IN_SIGHAND` condition
   that preserved that special behavior. The regression entered Portable OpenSSH 8.5p1.
3. When an unauthenticated client exceeded `LoginGraceTime`, `grace_alarm_handler()` could once
   again reach `sigdie()` and, on affected platforms, non-async-signal-safe logging functions.
4. Qualys reconstructed the old bug class and demonstrated exploitation against a current 32-bit
   Debian Linux/glibc environment. Their laboratory attack needed roughly 10,000 race attempts and,
   with the tested limits and ASLR, averaged six to eight hours for a root shell.
5. OpenSSH published version 9.8/9.8p1 on 1 July 2024. The new design performs minimal termination
   in the alarm handler and moves logging and per-source penalty work into normal listener context.

The name **regreSSHion** reflects this history: a security property fixed in 2006 was lost during a
later refactor. The old vulnerable source was not copied back verbatim; the guarantee established
by the old fix disappeared.

## Who was actually affected?

The upstream affected range was Portable OpenSSH 8.5p1 through 9.7p1 inclusive. That version range
is a starting point, not a complete exposure decision. Practical risk also depended on:

- an internet-reachable `sshd` process;
- a platform whose logging path was not async-signal-safe, notably glibc-based Linux;
- a package without a vendor backport or downstream change that removed the vulnerable path;
- `LoginGraceTime` and `MaxStartups` settings that allowed repeated pre-authentication attempts;
- architecture, allocator behavior, ASLR, network stability, and attack time.

OpenSSH stated that successful root code execution had been demonstrated on 32-bit Linux/glibc with
ASLR. At the time of the 9.8 release, 64-bit exploitation was believed possible but had not been
demonstrated. Non-glibc systems had not been fully examined. OpenBSD was explicitly unaffected
because its signal-time logging path used a safer implementation.

Distribution package versions must be checked through the vendor, not only through the SSH banner.
For example, Ubuntu fixed affected supported releases with backported package updates whose version
numbers remained below 9.8p1. Conversely, an upstream-looking version does not prove that a specific
downstream build was exploitable with Qualys's demonstrated method.

## What I verified

- In `grace_alarm_handler()`, the vulnerable path called `sigdie()` and formatted a timeout message;
  the patch replaces it with `_exit(EXIT_LOGIN_GRACE)`.
- The listener later recognizes `EXIT_LOGIN_GRACE` in `child_reap()`, logs synchronously, and applies
  the source penalty outside the signal handler.
- I checked the 9.8 release notes and the public technical advisory against the patch to separate the
  code fact from platform-specific exploitability claims.
- This is a static reproduction of the fixed control flow. I did not perform timing attempts,
  allocator manipulation, or exploit testing against any SSH service.

## Deep dive: why logging from SIGALRM can corrupt the heap

Normal function calls are sequenced. A function updates its state, calls another function, and
returns when its invariants are restored. An asynchronous signal is different: the handler may run
while the interrupted code temporarily holds a lock, owns a half-updated allocator structure, or
has changed global state that is not yet consistent.

POSIX therefore defines a small set of async-signal-safe operations. General logging is not among
them. On affected glibc systems, `syslog()` can allocate and free memory. If the alarm interrupts
the main authentication path during its own allocator operation, the handler may enter the same
allocator again against transient state.

<figure class="diagram">
<svg viewBox="0 0 760 304" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The normal authentication path is updating heap state when LoginGraceTime expires. SIGALRM interrupts it and the handler calls logging, which re-enters the allocator before the original update is complete.">
<defs><marker id="ssh-a" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L8,4 L0,8 z" class="dg-arrow"/></marker></defs>
<text x="0" y="24" class="dg-crit">ASYNCHRONOUS RE-ENTRY</text>
<rect x="0" y="54" width="176" height="68" rx="8" class="dg-box"/><text x="16" y="82" class="dg-label">Auth parsing</text><text x="16" y="104" class="dg-muted">normal control flow</text>
<line x1="178" y1="88" x2="228" y2="88" class="dg-line" marker-end="url(#ssh-a)"/>
<rect x="232" y="54" width="176" height="68" rx="8" class="dg-box-crit"/><text x="248" y="82" class="dg-label">malloc / free</text><text x="248" y="104" class="dg-crit">state in transition</text>
<line x1="320" y1="124" x2="320" y2="182" class="dg-line dg-crit-line" marker-end="url(#ssh-a)"/>
<rect x="232" y="188" width="176" height="68" rx="8" class="dg-box-crit"/><text x="248" y="216" class="dg-label">SIGALRM</text><text x="248" y="238" class="dg-crit">deadline expires</text>
<line x1="410" y1="222" x2="466" y2="222" class="dg-line dg-crit-line" marker-end="url(#ssh-a)"/>
<rect x="470" y="188" width="132" height="68" rx="8" class="dg-box-crit"/><text x="486" y="216" class="dg-label">syslog</text><text x="486" y="238" class="dg-muted">allocates again</text>
<line x1="604" y1="222" x2="626" y2="222" class="dg-line dg-crit-line" marker-end="url(#ssh-a)"/>
<rect x="630" y="188" width="130" height="68" rx="8" class="dg-box-crit"/><text x="646" y="216" class="dg-label">Heap state</text><text x="646" y="238" class="dg-crit">corrupted</text>
<text x="470" y="82" class="dg-muted">The handler runs inside the privileged,</text><text x="470" y="104" class="dg-muted">pre-authentication server process.</text>
</svg>
<figcaption>The bug is not that a timeout exists. The bug is that timeout handling re-enters complex process state asynchronously while that state may be inconsistent.</figcaption>
</figure>

Crashes are the easiest visible outcome, but not the security boundary. The Qualys research showed
that carefully repeated connections could shape and interrupt allocator activity on a 32-bit
Linux/glibc target, eventually reaching unauthenticated code execution as root. OpenSSH's 9.8
notes report an average of six to eight hours in the demonstrated laboratory configuration and
state that 64-bit exploitation was believed possible but had not been demonstrated at release.

These conditions should be preserved rather than simplified into “all SSH servers are instantly
rootable.” Platform libc, architecture, ASLR behavior, downstream patches, `LoginGraceTime`, and
connection limits change practical reachability. They do not remove the underlying unsafe handler.

## The regression was a lost invariant, not a reverted patch

The 2006 correction made the fatal signal path call `_exit(1)` rather than unsafe logging logic.
Qualys traced the regression to an October 2020 logging infrastructure change. The refactor removed
the `DO_LOG_SAFE_IN_SIGHAND` guard from the function reached by the alarm handler.

This is an important patch-analysis pattern. A security fix often establishes an invariant that is
larger than the exact diff: “the signal handler performs only safe termination.” Years later, a
maintainer can reorganize logging correctly for ordinary callers while unknowingly reconnecting an
exceptional caller with stricter rules.

Tests that assert only a historical line or macro survives are fragile. The durable regression test
must identify the special calling context and prove that every path from it remains async-signal-safe.

## Why privilege separation did not contain this path

OpenSSH uses privilege separation extensively, but the vulnerable operation occurred before
authentication completed in a privileged server process. A remote client did not need a valid
account or credential to keep a connection open until the grace timer expired.

This illustrates a common assessment error: a product can have excellent sandboxing and still
retain a narrow privileged path for setup, monitoring, or teardown. Security analysis must map the
specific process and lifecycle phase executing the vulnerable code. “The application is sandboxed”
is not evidence until the affected instruction is placed inside that sandbox.

## The 9.8p1 fix moves work out of the handler

The public fix was part of a broader server restructuring and per-source penalty work. Instead of
performing unsafe cleanup and logging inside the alarm handler, the design moves meaningful work to
the listener, where it can run synchronously. A minimal backport can also make `sshsigdie()` terminate
with `_exit(1)` and omit the unsafe formatting and logging path.

<figure class="diagram">
<svg viewBox="0 0 760 288" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The vulnerable handler formats and logs during SIGALRM, re-entering process state. The fixed handler performs safe termination while the listener processes penalties and logging synchronously.">
<text x="0" y="24" class="dg-crit">BEFORE</text>
<rect x="0" y="48" width="164" height="66" rx="8" class="dg-box"/><text x="16" y="76" class="dg-label">SIGALRM</text><text x="16" y="98" class="dg-muted">async context</text>
<rect x="200" y="48" width="164" height="66" rx="8" class="dg-box-crit"/><text x="216" y="76" class="dg-label">Format + log</text><text x="216" y="98" class="dg-crit">unsafe calls</text>
<rect x="400" y="48" width="164" height="66" rx="8" class="dg-box-crit"/><text x="416" y="76" class="dg-label">Allocator</text><text x="416" y="98" class="dg-crit">re-entered</text>
<line x1="0" y1="142" x2="760" y2="142" class="dg-line dg-dash"/>
<text x="0" y="176" class="dg-accent">9.8P1 PROPERTY</text>
<rect x="0" y="196" width="164" height="66" rx="8" class="dg-box-accent"/><text x="16" y="224" class="dg-label">SIGALRM</text><text x="16" y="246" class="dg-accent">safe exit</text>
<rect x="298" y="196" width="194" height="66" rx="8" class="dg-box"/><text x="314" y="224" class="dg-label">Listener event</text><text x="314" y="246" class="dg-muted">normal context</text>
<rect x="596" y="196" width="164" height="66" rx="8" class="dg-box-accent"/><text x="612" y="224" class="dg-label">Log / penalize</text><text x="612" y="246" class="dg-accent">synchronous</text>
<line x1="164" y1="229" x2="298" y2="229" class="dg-line"/><line x1="492" y1="229" x2="596" y2="229" class="dg-line"/>
</svg>
<figcaption>The robust fix restores context separation: an asynchronous handler does the minimum safe action, while ordinary process logic performs logging and policy work.</figcaption>
</figure>

The fix is architectural, not a timing adjustment. Increasing or decreasing `LoginGraceTime` may
alter attack economics, but it does not make an unsafe signal handler safe. Connection throttling
is useful defense in depth; patching removes the root cause.

## Evidence matrix

| Question | What I checked | Confidence | Limit |
| --- | --- | ---: | --- |
| Which modern versions regressed? | I matched OpenSSH and Qualys records identifying Portable OpenSSH 8.5p1 through 9.7p1. | High | Vendor backports and platform-specific changes must be checked separately. |
| What triggers the path? | I traced `LoginGraceTime` expiration to the `SIGALRM` handler before authentication completes. | High | Practical timing depends on configuration and network behavior. |
| Why is logging dangerous there? | I confirmed the old handler reaches logging that is not async-signal-safe and may re-enter allocator operations. | High | Exact libc internals vary by platform. |
| Was remote root execution demonstrated? | I reviewed Qualys's 32-bit Linux/glibc lab result; I did not repeat it. | High | The release record did not demonstrate the same result on 64-bit targets. |
| Why is OpenBSD excluded? | I checked OpenSSH's note that OpenBSD uses a safer logging path and is not vulnerable. | High | Other non-glibc systems require their own analysis. |
| What does 9.8p1 change? | Unsafe handler work is removed or shifted into synchronous listener processing. | High | Distribution packages may express the fix with an older-looking version. |

## Safe validation without racing sshd

Start with the vendor package record. Capture the server's Portable OpenSSH version, operating
system, architecture, libc, package revision, and the distribution's CVE status. Do not decide from
the SSH banner alone: vendors can hide versions, backport fixes, or apply downstream patches that
change reachability.

For source validation, inspect the vendor's `sshsigdie()` or equivalent patch. The fixed assertion
is structural: the alarm handler must not format messages, call general logging, allocate memory, or
perform other non-async-signal-safe work. It should terminate safely or communicate minimal state
to code that handles it synchronously.

A controlled regression test can launch an instrumented, non-production build in an isolated lab,
allow an unauthenticated connection to exceed the grace period, and observe the handler's call set.
The test passes when the signal path reaches only allowed operations. There is no need to tune
packet timing, corrupt a heap, or seek code execution.

Use these negative controls:

- the same observation against 9.8p1;
- a vendor build with a documented backport despite an older upstream version;
- a platform whose advisory marks it unaffected;
- a normal synchronous logging event, proving that logging still works outside the handler;
- a configuration with the deadline disabled, treated only as reachability comparison—not a fix.

Repeated unauthenticated connections against an internet-facing service are not an appropriate
verification method. They can exhaust resources and resemble exploitation. Static patch proof plus
one bounded lab observation provides clearer evidence with less operational risk.

## My conclusion

Upgrade to OpenSSH 9.8p1 or the distribution package that explicitly backports CVE-2024-6387.
Before maintenance completes, connection-rate controls and reduced exposure can lower risk, but
they should be documented as temporary compensating controls rather than remediation.

My conclusion is that the durable fix is the smaller signal handler, not a more careful logger.
The handler now exits with a minimal signal-safe operation, while ordinary code performs logging
later. Refactors that touch shared logging or cleanup code must explicitly re-check exceptional
callers such as signal handlers instead of assuming ordinary call rules still apply.

## Public sources

- [OpenSSH 9.8 release notes](https://www.openssh.com/txt/release-9.8)
- [Qualys: regreSSHion technical advisory](https://www.qualys.com/2024/07/01/cve-2024-6387/regresshion.txt)
- [OpenSSH portable fix commit](https://github.com/openssh/openssh-portable/commit/81c1099d22b81ebfd20a334ce986c4f753b0db29)
- [Ubuntu CVE record and fixed package status](https://ubuntu.com/security/CVE-2024-6387)
- [CVE.org record for CVE-2024-6387](https://www.cve.org/CVERecord?id=CVE-2024-6387)