- Identifier
- CVE-2026-73282
- Component
- OpenSSH SSH client
- Weakness
- CWE-416 · Use After Free
- Upstream fix
- OpenSSH 10.5 / 10.5p1 · August 11, 2026
- Upstream credit
- Report and fix: Brian Mingus of Cognatory
The affected workflow
Remote forwarding asks an SSH server to listen for connections and carry them through an existing SSH connection. The client keeps a record of each forwarding and later handles the server’s reply.
The release notes describe a specific overlap: an initial remote-forward request remains pending while another remote forwarding is added through the client’s local multiplexing socket. The CVE record identifies a client use-after-free before OpenSSH 10.5. Its version range is a published advisory statement; the source comparison below examines 10.4p1 and 10.5p1 specifically.
The multiplexing socket is a local control interface used for connection sharing. In the reviewed source, the listener checks the connecting process’s credentials and rejects a peer whose effective user ID is neither root nor the client’s user. ControlMaster and ControlPath configuration, socket access and the pending request all matter to this workflow. This note does not establish a way for an arbitrary unauthenticated network client to trigger the condition.
References: OpenSSH 10.5 release notes, CVE Program: CVE-2026-73282, OpenSSH 10.5p1: control-socket peer checks, OpenSSH 10.5p1: ControlMaster, ControlPath and ControlPersist.
How the reference becomes stale
In 10.4p1, ssh_init_forwarding passes the address of an entry in options.remote_forwards to client_register_global_confirm. The queue retains that callback context. When the server’s reply is processed, client_global_request_reply hands the saved context to ssh_confirm_remote_forward.
Meanwhile, mux_master_process_open_fwd can add another remote forwarding. Its call to add_remote_forward reaches xreallocarray, which grows the array and appends an entry. If growth moves the allocation, the earlier address still points into the old storage. Updating the array’s main pointer does not update the separate pointer already held by the confirmation queue.
The callback then treats its context as a Forward pointer and reads forwarding fields. The defect is the lifetime of that reference across deferred work. A program does not need two threads executing at once to have this problem; one operation can invalidate a reference between its creation and its later use.
| Stage | Pending confirmation | Forwarding table |
|---|---|---|
| Initial request | Retains a reference to an existing entry | Contains the initial forwarding |
| Another forwarding is added | Still waiting for the server’s reply | May move during growth |
| Reply is handled | Uses the retained context | Current storage may differ from the saved address |
References: OpenSSH 10.4p1: original remote-forward confirmation, OpenSSH 10.5p1: remote-forward array growth, OpenSSH 10.5p1: multiplexed forwarding requests, OpenSSH 10.5p1: deferred global confirmations.
What the upstream fix changes
Commit 9910d5ef53124ce1157d57bc11e222658aa41299 gives each initial confirmation its own small context containing a forwarding index. On callback, the code checks that index against the current count, obtains the entry from the current array and frees the context.
The change removes the queued dependency on the array’s previous address. An append preserves the positions of existing entries even if their storage moves. This is a focused ownership repair: the pending operation owns its context and resolves the current entry when needed.
An index is useful only while its meaning remains stable. In another design, removal, reordering or slot reuse could make an in-range index identify the wrong object. A review should examine that invariant as well as the allocation’s lifetime.
References: OpenSSH upstream fix and attribution, OpenSSH 10.5p1: repaired confirmation context, OpenSSH 10.5p1: remote-forward array growth.
An independent local model
The following Python example models allocation identity explicitly. Each append deliberately represents a move, so the outcome is repeatable. The saved address is a pair of numbers, and an exception represents detection of a stale reference. It neither accesses freed memory nor runs OpenSSH.
Run the example with Python 3. It checks the unchanged case, a moved allocation, repeated moves and invalid bounds. The final counterexample deliberately reorders two records: the slot remains in range but now identifies a different record.
| Check | Observed result |
|---|---|
| No modeled move | Address and slot both select the first record |
| One modeled move | Saved address is rejected; slot still selects the first record |
| 100 further modeled moves | The first three slots retain their original records |
| Negative and past-end slots | Both are rejected |
| Deliberate record reorder | Slot zero now selects the second record, exposing the identity assumption |
Inspect the complete Python example
"""Independent lifetime model. Does not run or reproduce OpenSSH."""
import json
class StaleAddress(Exception):
pass
class Table:
def __init__(self, rows):
self.rows = list(rows)
self.allocation = 0
def append_with_move(self, row):
# Deliberately model a move on every append, for a repeatable test.
self.rows = self.rows + [row]
self.allocation += 1
def address(self, slot):
return self.allocation, slot
def read_address(self, address):
allocation, slot = address
if allocation != self.allocation:
raise StaleAddress("the saved allocation is no longer current")
return self.read_slot(slot)
def read_slot(self, slot):
if slot < 0 or slot >= len(self.rows):
raise IndexError("slot outside the current table")
return self.rows[slot]
checks = []
def record(name, condition):
if not condition:
raise AssertionError(name)
checks.append({"name": name, "passed": True})
table = Table(["first", "second", "third"])
address = table.address(0)
record("no move: both references select first",
table.read_address(address) == table.read_slot(0) == "first")
table.append_with_move("fourth")
try:
table.read_address(address)
raise AssertionError("stale-address check did not run")
except StaleAddress:
record("move: old address rejected", True)
record("move: saved slot still selects first",
table.read_slot(0) == "first")
for i in range(100):
table.append_with_move("extra-" + str(i))
record("100 more moves: original slots retain their subjects",
[table.read_slot(i) for i in range(3)] == ["first", "second", "third"])
for bad_slot in (-1, len(table.rows)):
try:
table.read_slot(bad_slot)
raise AssertionError("bounds check did not run")
except IndexError:
record("invalid slot rejected: " + str(bad_slot), True)
table.rows[0], table.rows[1] = table.rows[1], table.rows[0]
record("counterexample: reordering breaks a slot's identity",
table.read_slot(0) == "second")
print(json.dumps({
"passed": True, "check_count": len(checks), "checks": checks,
"scope": "Allocation-identity model only; no OpenSSH or network execution."
}, indent=2))
Verification and its limits
On October 3, 2026, this editorial review passed 15 static source checks against the immutable 10.4p1 and 10.5p1 tag commits. They check the original pointer context, the replacement context and bounds check, array growth, deferred callback handling and local peer checks. The official 10.5p1 archive matched the SHA-256 published in the release notes, and its ssh.c matched the release-tag file byte for byte.
The independent model completed seven checks under Python 3.12.14 with exit status zero. These are observed model results and source comparisons. No vulnerable or repaired OpenSSH executable was run for this article; no exploitation, crash reproduction, distributor backport or installed system was validated. The review does not determine the first release that introduced the issue.
References: OpenSSH 10.5 release notes, OpenSSH 10.4p1: original remote-forward confirmation, OpenSSH 10.5p1: repaired confirmation context.
A worksheet for reviewing an installation
Start with the client that the workflow actually uses. A server inventory alone misses the component discussed here. Keep the package decision separate from the behavior check: a version label may not describe a distributor’s backported fixes.
OpenSSH documents that a ControlPersist master can remain running after the original client session closes. Updating a package on disk therefore does not, by itself, demonstrate that an existing master process has been replaced. Plan any restart around the connections it carries.
| Record | Evidence to collect |
|---|---|
| Client and owner | Executable path, package origin and the person or service responsible for it |
| Fix status | Maintainer advisory or patch record showing that the installed package includes this correction |
| Relevant workflow | Effective connection-sharing and remote-forwarding configuration; existing persistent masters |
| Change and process state | Installed package/build evidence and confirmation that affected long-lived client processes were replaced |
| Functional verification | Results for the intended connection and forwarding workflows in an authorized test environment, including success and failure handling |
| Unresolved items | Unknown package provenance, unverified backports or workflows that could not be tested |
References: OpenSSH 10.5p1: ControlMaster, ControlPath and ControlPersist, OpenSSH 10.5 release notes.
A practical review question
When a callback keeps a reference to mutable state, identify what can change before the callback runs. Record who owns the context, what can relocate or replace the referenced data, and how the callback proves it still refers to the intended object. Then test a state change between scheduling and completion. That question applies well beyond forwarding code.
Sources and reference editions
- OpenSSH 10.5 release notes10.5 / 10.5p1, August 11, 2026
- CVE Program: CVE-2026-73282CVE JSON record last updated August 11, 2026; retrieved October 3, 2026, America/Denver
- OpenSSH upstream fix and attribution9910d5ef53124ce1157d57bc11e222658aa41299
- OpenSSH 10.4p1: original remote-forward confirmationV_10_4_P1 at e8dd756725e8800fcd0b3fd71ee6b4382d1e8fab
- OpenSSH 10.5p1: repaired confirmation contextV_10_5_P1 at b3f7344209832eea8ece447d871ea748767c444b
- OpenSSH 10.5p1: remote-forward array growthb3f7344209832eea8ece447d871ea748767c444b
- OpenSSH 10.5p1: multiplexed forwarding requestsb3f7344209832eea8ece447d871ea748767c444b
- OpenSSH 10.5p1: deferred global confirmationsb3f7344209832eea8ece447d871ea748767c444b
- OpenSSH 10.5p1: control-socket peer checksb3f7344209832eea8ece447d871ea748767c444b
- OpenSSH 10.5p1: ControlMaster, ControlPath and ControlPersistb3f7344209832eea8ece447d871ea748767c444b