[PATCH 1/2] ssh-keygen: close TOCTOU window when writing host keys in -A mode

Muhammad Bilal meatuni001 at gmail.com
Fri Aug 28 18:16:09 AEST 2026


do_gen_all_hostkeys() (used by 'ssh-keygen -A') used mkstemp() only
to reserve a unique filename, closing the returned fd immediately:

    fd = mkstemp(prv_tmp);
    (void)close(fd); /* just using mkstemp() to reserve a name */
    ... sshkey_generate() ...        /* can take a while, e.g. RSA */
    sshkey_save_private(private, prv_tmp, ...);   /* re-opens by path */

and identically for the public key via sshkey_save_public(). Both
sshkey_save_private() and sshkey_save_public() end up calling
open(path, O_WRONLY|O_CREAT|O_TRUNC, ...) with no O_EXCL/O_NOFOLLOW.
Between the close() and that re-open, an attacker able to write to
the destination directory could unlink the reserved file and replace
it with a symlink, causing the re-open to follow it and truncate an
arbitrary path the process can write to.

This keeps each mkstemp() fd open and writes the key straight
through it -- sshkey_private_to_fileblob() + atomicio() for the
private key (mirroring what sshkey_save_private()/sshbuf_write_file()
do internally, since mkstemp() already creates the file with the same
0600 mode sshkey_save_private() achieved via umask), and fdopen() +
sshkey_write() for the public key (mirroring sshkey_save_public()).
No second path-based open() of either temp file occurs, so there is
no window to exploit. On a write failure, prv_tmp is now unlinked to
match sshbuf_write_file()'s existing cleanup-on-failure behaviour,
which the initial version of this fix had dropped. This only touches
do_gen_all_hostkeys(); the shared sshkey_save_private()/
sshkey_save_public() helpers are untouched, since they're also used
for paths a user may legitimately have symlinked on purpose (e.g. an
identity file pointed at another volume), where following the link
is expected behaviour.

Verified with strace -e trace=openat,rename on 'ssh-keygen -A -f
<dir>': the unpatched binary shows, per key, an O_CREAT|O_EXCL open
(mkstemp) immediately followed by a *second* open of the same path
with O_CREAT|O_TRUNC and no O_EXCL -- the vulnerable re-open -- before
the rename. The patched binary shows only the single O_CREAT|O_EXCL
open per path before its rename; no second open occurs. All four
generated host key types (RSA, ECDSA, ED25519, MLDSA44-ED25519) still
verify correctly with 'ssh-keygen -l' after the change, with the
expected 0600/0644 modes on the private/public files.

Note on severity: in the default deployment (-A writing to a
root-owned, non-world-writable /etc/ssh) this requires the attacker
to already have write access to the destination directory, which is
not the case out of the box -- it is not exploitable by an arbitrary
unprivileged local user against a stock install. It is a real
TOCTOU/CWE-367 pattern worth closing as defense-in-depth regardless,
and matters more directly for non-default invocations (e.g. -f
pointing at a shared/staging directory during image or package
builds).
---
 ssh-keygen.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 47 insertions(+), 4 deletions(-)

diff --git a/ssh-keygen.c b/ssh-keygen.c
index 6667a5c1b..fab1612af 100644
--- a/ssh-keygen.c
+++ b/ssh-keygen.c
@@ -1015,6 +1015,8 @@ do_gen_all_hostkeys(struct passwd *pw)
 	struct sshkey *private, *public;
 	char comment[1024], *prv_tmp, *pub_tmp, *prv_file, *pub_file;
 	int i, type, fd, r;
+	struct sshbuf *keyblob;
+	FILE *pub_f;
 
 	for (i = 0; key_types[i].key_type; i++) {
 		public = private = NULL;
@@ -1056,21 +1058,49 @@ do_gen_all_hostkeys(struct passwd *pw)
 			    prv_tmp, strerror(errno));
 			goto failnext;
 		}
-		(void)close(fd); /* just using mkstemp() to reserve a name */
+		/*
+		 * Keep the fd mkstemp() gave us open and write the key
+		 * through it directly, rather than closing it and having
+		 * sshkey_save_private() re-open prv_tmp by name below: an
+		 * attacker who can replace prv_tmp with a symlink in the
+		 * window between the close and the re-open could redirect
+		 * the write to an arbitrary path.
+		 */
 		bits = 0;
 		type_bits_valid(type, NULL, &bits);
 		if ((r = sshkey_generate(type, bits, &private)) != 0) {
 			error_r(r, "sshkey_generate failed");
+			close(fd);
 			goto failnext;
 		}
 		if ((r = sshkey_from_private(private, &public)) != 0)
 			fatal_fr(r, "sshkey_from_private");
 		snprintf(comment, sizeof comment, "%s@%s", pw->pw_name,
 		    hostname);
-		if ((r = sshkey_save_private(private, prv_tmp, "",
+		if ((keyblob = sshbuf_new()) == NULL)
+			fatal_f("sshbuf_new failed");
+		if ((r = sshkey_private_to_fileblob(private, keyblob, "",
 		    comment, private_key_format, openssh_format_cipher,
 		    rounds)) != 0) {
 			error_r(r, "Saving key \"%s\" failed", prv_tmp);
+			sshbuf_free(keyblob);
+			close(fd);
+			goto failnext;
+		}
+		if (atomicio(vwrite, fd, sshbuf_mutable_ptr(keyblob),
+		    sshbuf_len(keyblob)) != sshbuf_len(keyblob)) {
+			error("Could not save your private key in %s: %s",
+			    prv_tmp, strerror(errno));
+			sshbuf_free(keyblob);
+			close(fd);
+			unlink(prv_tmp);
+			goto failnext;
+		}
+		sshbuf_free(keyblob);
+		if (close(fd) != 0) {
+			error("Could not save your private key in %s: %s",
+			    prv_tmp, strerror(errno));
+			unlink(prv_tmp);
 			goto failnext;
 		}
 		if ((fd = mkstemp(pub_tmp)) == -1) {
@@ -1079,10 +1109,23 @@ do_gen_all_hostkeys(struct passwd *pw)
 			goto failnext;
 		}
 		(void)fchmod(fd, 0644);
-		(void)close(fd);
-		if ((r = sshkey_save_public(public, pub_tmp, comment)) != 0) {
+		/* As above: write through the held-open fd, don't re-open. */
+		if ((pub_f = fdopen(fd, "w")) == NULL) {
+			error("fdopen %s failed: %s", pub_tmp,
+			    strerror(errno));
+			close(fd);
+			goto failnext;
+		}
+		if ((r = sshkey_write(public, pub_f)) != 0) {
 			error_r(r, "Unable to save public key to %s",
 			    identity_file);
+			fclose(pub_f);
+			goto failnext;
+		}
+		fprintf(pub_f, " %s\n", comment);
+		if (fclose(pub_f) != 0) {
+			error("Unable to save public key to %s: %s",
+			    identity_file, strerror(errno));
 			goto failnext;
 		}
 
-- 
2.55.0



More information about the openssh-unix-dev mailing list