jcs's openbsd hax
openbsd
1/* $OpenBSD: misc-agent.c,v 1.7 2026/02/11 17:05:32 dtucker Exp $ */
2/*
3 * Copyright (c) 2025 Damien Miller <djm@mindrot.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18#include <sys/types.h>
19#include <sys/socket.h>
20#include <sys/stat.h>
21#include <sys/un.h>
22
23#include <dirent.h>
24#include <errno.h>
25#include <fcntl.h>
26#include <netdb.h>
27#include <stdlib.h>
28#include <string.h>
29#include <time.h>
30#include <unistd.h>
31
32#include "digest.h"
33#include "log.h"
34#include "misc.h"
35#include "pathnames.h"
36#include "ssh.h"
37#include "xmalloc.h"
38
39/* stuff shared by agent listeners (ssh-agent and sshd agent forwarding) */
40
41#define SOCKET_HOSTNAME_HASHLEN 10 /* length of hostname hash in socket path */
42
43/* used for presenting random strings in unix_listener_tmp and hostname_hash */
44static const char presentation_chars[] =
45 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
46
47/* returns a text-encoded hash of the hostname of specified length (max 64) */
48static char *
49hostname_hash(size_t len)
50{
51 char hostname[NI_MAXHOST], p[65];
52 u_char hash[64];
53 int r;
54 size_t l, i;
55
56 l = ssh_digest_bytes(SSH_DIGEST_SHA512);
57 if (len > 64) {
58 error_f("bad length %zu >= max %zd", len, l);
59 return NULL;
60 }
61 if (gethostname(hostname, sizeof(hostname)) == -1) {
62 error_f("gethostname: %s", strerror(errno));
63 return NULL;
64 }
65 if ((r = ssh_digest_memory(SSH_DIGEST_SHA512,
66 hostname, strlen(hostname), hash, sizeof(hash))) != 0) {
67 error_fr(r, "ssh_digest_memory");
68 return NULL;
69 }
70 memset(p, '\0', sizeof(p));
71 for (i = 0; i < l; i++)
72 p[i] = presentation_chars[
73 hash[i] % (sizeof(presentation_chars) - 1)];
74 /* debug3_f("hostname \"%s\" => hash \"%s\"", hostname, p); */
75 p[len] = '\0';
76 return xstrdup(p);
77}
78
79char *
80agent_hostname_hash(void)
81{
82 return hostname_hash(SOCKET_HOSTNAME_HASHLEN);
83}
84
85/*
86 * Creates a unix listener at a mkstemp(3)-style path, e.g. "/dir/sock.XXXXXX"
87 * Supplied path is modified to the actual one used.
88 */
89static int
90unix_listener_tmp(char *path, int backlog)
91{
92 struct sockaddr_un sunaddr;
93 int good, sock = -1;
94 size_t i, xstart;
95 mode_t prev_mask;
96
97 /* Find first 'X' template character back from end of string */
98 xstart = strlen(path);
99 while (xstart > 0 && path[xstart - 1] == 'X')
100 xstart--;
101
102 memset(&sunaddr, 0, sizeof(sunaddr));
103 sunaddr.sun_family = AF_UNIX;
104 prev_mask = umask(0177);
105 for (good = 0; !good;) {
106 sock = -1;
107 /* Randomise path suffix */
108 for (i = xstart; path[i] != '\0'; i++) {
109 path[i] = presentation_chars[
110 arc4random_uniform(sizeof(presentation_chars)-1)];
111 }
112 debug_f("trying path \"%s\"", path);
113
114 if (strlcpy(sunaddr.sun_path, path,
115 sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
116 error_f("path \"%s\" too long for Unix domain socket",
117 path);
118 break;
119 }
120
121 if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
122 error_f("socket: %.100s", strerror(errno));
123 break;
124 }
125 if (bind(sock, (struct sockaddr *)&sunaddr,
126 sizeof(sunaddr)) == -1) {
127 if (errno == EADDRINUSE) {
128 error_f("bind \"%s\": %.100s",
129 path, strerror(errno));
130 close(sock);
131 sock = -1;
132 continue;
133 }
134 error_f("bind \"%s\": %.100s", path, strerror(errno));
135 break;
136 }
137 if (listen(sock, backlog) == -1) {
138 error_f("listen \"%s\": %s", path, strerror(errno));
139 break;
140 }
141 good = 1;
142 }
143 umask(prev_mask);
144 if (good) {
145 debug3_f("listening on unix socket \"%s\" as fd=%d",
146 path, sock);
147 } else if (sock != -1) {
148 close(sock);
149 sock = -1;
150 }
151 return sock;
152}
153
154/*
155 * Create a subdirectory under the supplied home directory if it
156 * doesn't already exist
157 */
158static int
159ensure_mkdir(const char *homedir, const char *subdir)
160{
161 char *path;
162
163 xasprintf(&path, "%s/%s", homedir, subdir);
164 if (mkdir(path, 0700) == 0)
165 debug("created directory %s", path);
166 else if (errno != EEXIST) {
167 error_f("mkdir %s: %s", path, strerror(errno));
168 free(path);
169 return -1;
170 }
171 free(path);
172 return 0;
173}
174
175static int
176agent_prepare_sockdir(const char *homedir)
177{
178 if (homedir == NULL || *homedir == '\0' ||
179 ensure_mkdir(homedir, _PATH_SSH_USER_DIR) != 0 ||
180 ensure_mkdir(homedir, _PATH_SSH_AGENT_SOCKET_DIR) != 0)
181 return -1;
182 return 0;
183}
184
185
186/* Get a path template for an agent socket in the user's homedir */
187static char *
188agent_socket_template(const char *homedir, const char *tag)
189{
190 char *hostnamehash, *ret;
191
192 if ((hostnamehash = hostname_hash(SOCKET_HOSTNAME_HASHLEN)) == NULL)
193 return NULL;
194 xasprintf(&ret, "%s/%s/s.%s.%s.XXXXXXXXXX",
195 homedir, _PATH_SSH_AGENT_SOCKET_DIR, hostnamehash, tag);
196 free(hostnamehash);
197 return ret;
198}
199
200int
201agent_listener(const char *homedir, const char *tag, int *sockp, char **pathp)
202{
203 int sock;
204 char *path;
205
206 *sockp = -1;
207 *pathp = NULL;
208
209 if (agent_prepare_sockdir(homedir) != 0)
210 return -1; /* error already logged */
211 if ((path = agent_socket_template(homedir, tag)) == NULL)
212 return -1; /* error already logged */
213 if ((sock = unix_listener_tmp(path, SSH_LISTEN_BACKLOG)) == -1) {
214 free(path);
215 return -1; /* error already logged */
216 }
217 /* success */
218 *sockp = sock;
219 *pathp = path;
220 return 0;
221}
222
223static int
224socket_is_stale(const char *path)
225{
226 int fd, r;
227 struct sockaddr_un sunaddr;
228 socklen_t l = sizeof(r);
229
230 /* attempt non-blocking connect on socket */
231 memset(&sunaddr, '\0', sizeof(sunaddr));
232 sunaddr.sun_family = AF_UNIX;
233 if (strlcpy(sunaddr.sun_path, path,
234 sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
235 debug_f("path for \"%s\" too long for sockaddr_un", path);
236 return 0;
237 }
238 if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
239 error_f("socket: %s", strerror(errno));
240 return 0;
241 }
242 set_nonblock(fd);
243 /* a socket without a listener should yield an error immediately */
244 if (connect(fd, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) {
245 debug_f("connect \"%s\": %s", path, strerror(errno));
246 close(fd);
247 return 1;
248 }
249 if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &r, &l) == -1) {
250 debug_f("getsockopt: %s", strerror(errno));
251 close(fd);
252 return 0;
253 }
254 if (r != 0) {
255 debug_f("socket error on %s: %s", path, strerror(errno));
256 close(fd);
257 return 1;
258 }
259 close(fd);
260 debug_f("socket %s seems still active", path);
261 return 0;
262}
263
264void
265agent_cleanup_stale(const char *homedir, int ignore_hosthash)
266{
267 DIR *d = NULL;
268 struct dirent *dp;
269 struct stat sb;
270 char *prefix = NULL, *dirpath = NULL, *path;
271 struct timespec now, sub;
272
273 /* Only consider sockets last modified > 1 hour ago */
274 if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
275 error_f("clock_gettime: %s", strerror(errno));
276 return;
277 }
278 sub.tv_sec = 60 * 60;
279 sub.tv_nsec = 0;
280 timespecsub(&now, &sub, &now);
281
282 /* Only consider sockets from the same hostname */
283 if (!ignore_hosthash) {
284 if ((path = agent_hostname_hash()) == NULL) {
285 error_f("couldn't get hostname hash");
286 return;
287 }
288 xasprintf(&prefix, "s.%s.", path);
289 free(path);
290 }
291
292 xasprintf(&dirpath, "%s/%s", homedir, _PATH_SSH_AGENT_SOCKET_DIR);
293 if ((d = opendir(dirpath)) == NULL) {
294 if (errno != ENOENT)
295 error_f("opendir \"%s\": %s", dirpath, strerror(errno));
296 goto out;
297 }
298 while ((dp = readdir(d)) != NULL) {
299 if (dp->d_type != DT_SOCK && dp->d_type != DT_UNKNOWN)
300 continue;
301 if (fstatat(dirfd(d), dp->d_name,
302 &sb, AT_SYMLINK_NOFOLLOW) != 0 && errno != ENOENT) {
303 error_f("stat \"%s/%s\": %s",
304 dirpath, dp->d_name, strerror(errno));
305 continue;
306 }
307 if (!S_ISSOCK(sb.st_mode))
308 continue;
309 if (timespeccmp(&sb.st_mtim, &now, >)) {
310 debug3_f("Ignoring recent socket \"%s/%s\"",
311 dirpath, dp->d_name);
312 continue;
313 }
314 if (!ignore_hosthash &&
315 strncmp(dp->d_name, prefix, strlen(prefix)) != 0) {
316 debug3_f("Ignoring socket \"%s/%s\" "
317 "from different host", dirpath, dp->d_name);
318 continue;
319 }
320 xasprintf(&path, "%s/%s", dirpath, dp->d_name);
321 if (socket_is_stale(path)) {
322 debug_f("cleanup stale socket %s", path);
323 unlinkat(dirfd(d), dp->d_name, 0);
324 }
325 free(path);
326 }
327 out:
328 if (d != NULL)
329 closedir(d);
330 free(dirpath);
331 free(prefix);
332}