/src/openssh/misc-agent.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* $OpenBSD: misc-agent.c,v 1.6 2025/06/17 01:19:27 djm 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 "includes.h" |
19 | | |
20 | | #include <sys/types.h> |
21 | | #include <sys/socket.h> |
22 | | #include <sys/stat.h> |
23 | | #include <sys/un.h> |
24 | | |
25 | | #include <dirent.h> |
26 | | #include <errno.h> |
27 | | #include <fcntl.h> |
28 | | #include <netdb.h> |
29 | | #include <stdlib.h> |
30 | | #include <string.h> |
31 | | #ifdef HAVE_TIME_H |
32 | | # include <time.h> |
33 | | #endif |
34 | | #include <unistd.h> |
35 | | |
36 | | #include "digest.h" |
37 | | #include "log.h" |
38 | | #include "misc.h" |
39 | | #include "pathnames.h" |
40 | | #include "ssh.h" |
41 | | #include "xmalloc.h" |
42 | | |
43 | | /* stuff shared by agent listeners (ssh-agent and sshd agent forwarding) */ |
44 | | |
45 | 0 | #define SOCKET_HOSTNAME_HASHLEN 10 /* length of hostname hash in socket path */ |
46 | | |
47 | | /* used for presenting random strings in unix_listener_tmp and hostname_hash */ |
48 | | static const char presentation_chars[] = |
49 | | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; |
50 | | |
51 | | /* returns a text-encoded hash of the hostname of specified length (max 64) */ |
52 | | static char * |
53 | | hostname_hash(size_t len) |
54 | 0 | { |
55 | 0 | char hostname[NI_MAXHOST], p[65]; |
56 | 0 | u_char hash[64]; |
57 | 0 | int r; |
58 | 0 | size_t l, i; |
59 | |
|
60 | 0 | l = ssh_digest_bytes(SSH_DIGEST_SHA512); |
61 | 0 | if (len > 64) { |
62 | 0 | error_f("bad length %zu >= max %zd", len, l); |
63 | 0 | return NULL; |
64 | 0 | } |
65 | 0 | if (gethostname(hostname, sizeof(hostname)) == -1) { |
66 | 0 | error_f("gethostname: %s", strerror(errno)); |
67 | 0 | return NULL; |
68 | 0 | } |
69 | 0 | if ((r = ssh_digest_memory(SSH_DIGEST_SHA512, |
70 | 0 | hostname, strlen(hostname), hash, sizeof(hash))) != 0) { |
71 | 0 | error_fr(r, "ssh_digest_memory"); |
72 | 0 | return NULL; |
73 | 0 | } |
74 | 0 | memset(p, '\0', sizeof(p)); |
75 | 0 | for (i = 0; i < l; i++) |
76 | 0 | p[i] = presentation_chars[ |
77 | 0 | hash[i] % (sizeof(presentation_chars) - 1)]; |
78 | | /* debug3_f("hostname \"%s\" => hash \"%s\"", hostname, p); */ |
79 | 0 | p[len] = '\0'; |
80 | 0 | return xstrdup(p); |
81 | 0 | } |
82 | | |
83 | | char * |
84 | | agent_hostname_hash(void) |
85 | 0 | { |
86 | 0 | return hostname_hash(SOCKET_HOSTNAME_HASHLEN); |
87 | 0 | } |
88 | | |
89 | | /* |
90 | | * Creates a unix listener at a mkstemp(3)-style path, e.g. "/dir/sock.XXXXXX" |
91 | | * Supplied path is modified to the actual one used. |
92 | | */ |
93 | | static int |
94 | | unix_listener_tmp(char *path, int backlog) |
95 | 0 | { |
96 | 0 | struct sockaddr_un sunaddr; |
97 | 0 | int good, sock = -1; |
98 | 0 | size_t i, xstart; |
99 | 0 | mode_t prev_mask; |
100 | | |
101 | | /* Find first 'X' template character back from end of string */ |
102 | 0 | xstart = strlen(path); |
103 | 0 | while (xstart > 0 && path[xstart - 1] == 'X') |
104 | 0 | xstart--; |
105 | |
|
106 | 0 | memset(&sunaddr, 0, sizeof(sunaddr)); |
107 | 0 | sunaddr.sun_family = AF_UNIX; |
108 | 0 | prev_mask = umask(0177); |
109 | 0 | for (good = 0; !good;) { |
110 | 0 | sock = -1; |
111 | | /* Randomise path suffix */ |
112 | 0 | for (i = xstart; path[i] != '\0'; i++) { |
113 | 0 | path[i] = presentation_chars[ |
114 | 0 | arc4random_uniform(sizeof(presentation_chars)-1)]; |
115 | 0 | } |
116 | 0 | debug_f("trying path \"%s\"", path); |
117 | |
|
118 | 0 | if (strlcpy(sunaddr.sun_path, path, |
119 | 0 | sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) { |
120 | 0 | error_f("path \"%s\" too long for Unix domain socket", |
121 | 0 | path); |
122 | 0 | break; |
123 | 0 | } |
124 | | |
125 | 0 | if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) { |
126 | 0 | error_f("socket: %.100s", strerror(errno)); |
127 | 0 | break; |
128 | 0 | } |
129 | 0 | if (bind(sock, (struct sockaddr *)&sunaddr, |
130 | 0 | sizeof(sunaddr)) == -1) { |
131 | 0 | if (errno == EADDRINUSE) { |
132 | 0 | error_f("bind \"%s\": %.100s", |
133 | 0 | path, strerror(errno)); |
134 | 0 | close(sock); |
135 | 0 | sock = -1; |
136 | 0 | continue; |
137 | 0 | } |
138 | 0 | error_f("bind \"%s\": %.100s", path, strerror(errno)); |
139 | 0 | break; |
140 | 0 | } |
141 | 0 | if (listen(sock, backlog) == -1) { |
142 | 0 | error_f("listen \"%s\": %s", path, strerror(errno)); |
143 | 0 | break; |
144 | 0 | } |
145 | 0 | good = 1; |
146 | 0 | } |
147 | 0 | umask(prev_mask); |
148 | 0 | if (good) { |
149 | 0 | debug3_f("listening on unix socket \"%s\" as fd=%d", |
150 | 0 | path, sock); |
151 | 0 | } else if (sock != -1) { |
152 | 0 | close(sock); |
153 | 0 | sock = -1; |
154 | 0 | } |
155 | 0 | return sock; |
156 | 0 | } |
157 | | |
158 | | /* |
159 | | * Create a subdirectory under the supplied home directory if it |
160 | | * doesn't already exist |
161 | | */ |
162 | | static int |
163 | | ensure_mkdir(const char *homedir, const char *subdir) |
164 | 0 | { |
165 | 0 | char *path; |
166 | |
|
167 | 0 | xasprintf(&path, "%s/%s", homedir, subdir); |
168 | 0 | if (mkdir(path, 0700) == 0) |
169 | 0 | debug("created directory %s", path); |
170 | 0 | else if (errno != EEXIST) { |
171 | 0 | error_f("mkdir %s: %s", path, strerror(errno)); |
172 | 0 | free(path); |
173 | 0 | return -1; |
174 | 0 | } |
175 | 0 | free(path); |
176 | 0 | return 0; |
177 | 0 | } |
178 | | |
179 | | static int |
180 | | agent_prepare_sockdir(const char *homedir) |
181 | 0 | { |
182 | 0 | if (homedir == NULL || *homedir == '\0' || |
183 | 0 | ensure_mkdir(homedir, _PATH_SSH_USER_DIR) != 0 || |
184 | 0 | ensure_mkdir(homedir, _PATH_SSH_AGENT_SOCKET_DIR) != 0) |
185 | 0 | return -1; |
186 | 0 | return 0; |
187 | 0 | } |
188 | | |
189 | | |
190 | | /* Get a path template for an agent socket in the user's homedir */ |
191 | | static char * |
192 | | agent_socket_template(const char *homedir, const char *tag) |
193 | 0 | { |
194 | 0 | char *hostnamehash, *ret; |
195 | |
|
196 | 0 | if ((hostnamehash = hostname_hash(SOCKET_HOSTNAME_HASHLEN)) == NULL) |
197 | 0 | return NULL; |
198 | 0 | xasprintf(&ret, "%s/%s/s.%s.%s.XXXXXXXXXX", |
199 | 0 | homedir, _PATH_SSH_AGENT_SOCKET_DIR, hostnamehash, tag); |
200 | 0 | free(hostnamehash); |
201 | 0 | return ret; |
202 | 0 | } |
203 | | |
204 | | int |
205 | | agent_listener(const char *homedir, const char *tag, int *sockp, char **pathp) |
206 | 0 | { |
207 | 0 | int sock; |
208 | 0 | char *path; |
209 | |
|
210 | 0 | *sockp = -1; |
211 | 0 | *pathp = NULL; |
212 | |
|
213 | 0 | if (agent_prepare_sockdir(homedir) != 0) |
214 | 0 | return -1; /* error already logged */ |
215 | 0 | if ((path = agent_socket_template(homedir, tag)) == NULL) |
216 | 0 | return -1; /* error already logged */ |
217 | 0 | if ((sock = unix_listener_tmp(path, SSH_LISTEN_BACKLOG)) == -1) { |
218 | 0 | free(path); |
219 | 0 | return -1; /* error already logged */ |
220 | 0 | } |
221 | | /* success */ |
222 | 0 | *sockp = sock; |
223 | 0 | *pathp = path; |
224 | 0 | return 0; |
225 | 0 | } |
226 | | |
227 | | static int |
228 | | socket_is_stale(const char *path) |
229 | 0 | { |
230 | 0 | int fd, r; |
231 | 0 | struct sockaddr_un sunaddr; |
232 | 0 | socklen_t l = sizeof(r); |
233 | | |
234 | | /* attempt non-blocking connect on socket */ |
235 | 0 | memset(&sunaddr, '\0', sizeof(sunaddr)); |
236 | 0 | sunaddr.sun_family = AF_UNIX; |
237 | 0 | if (strlcpy(sunaddr.sun_path, path, |
238 | 0 | sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) { |
239 | 0 | debug_f("path for \"%s\" too long for sockaddr_un", path); |
240 | 0 | return 0; |
241 | 0 | } |
242 | 0 | if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) { |
243 | 0 | error_f("socket: %s", strerror(errno)); |
244 | 0 | return 0; |
245 | 0 | } |
246 | 0 | set_nonblock(fd); |
247 | | /* a socket without a listener should yield an error immediately */ |
248 | 0 | if (connect(fd, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) { |
249 | 0 | debug_f("connect \"%s\": %s", path, strerror(errno)); |
250 | 0 | close(fd); |
251 | 0 | return 1; |
252 | 0 | } |
253 | 0 | if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &r, &l) == -1) { |
254 | 0 | debug_f("getsockopt: %s", strerror(errno)); |
255 | 0 | close(fd); |
256 | 0 | return 0; |
257 | 0 | } |
258 | 0 | if (r != 0) { |
259 | 0 | debug_f("socket error on %s: %s", path, strerror(errno)); |
260 | 0 | close(fd); |
261 | 0 | return 1; |
262 | 0 | } |
263 | 0 | close(fd); |
264 | 0 | debug_f("socket %s seems still active", path); |
265 | 0 | return 0; |
266 | 0 | } |
267 | | |
268 | | #ifndef HAVE_FSTATAT |
269 | | # define fstatat(x, y, buf, z) lstat(path, buf) |
270 | | #endif |
271 | | #ifndef HAVE_UNLINKAT |
272 | | # define unlinkat(x, y, z) unlink(path) |
273 | | #endif |
274 | | |
275 | | void |
276 | | agent_cleanup_stale(const char *homedir, int ignore_hosthash) |
277 | 0 | { |
278 | 0 | DIR *d = NULL; |
279 | 0 | struct dirent *dp; |
280 | 0 | struct stat sb; |
281 | 0 | char *prefix = NULL, *dirpath = NULL, *path = NULL; |
282 | 0 | struct timespec now, sub, *mtimp = NULL; |
283 | | |
284 | | /* Only consider sockets last modified > 1 hour ago */ |
285 | 0 | if (clock_gettime(CLOCK_REALTIME, &now) != 0) { |
286 | 0 | error_f("clock_gettime: %s", strerror(errno)); |
287 | 0 | return; |
288 | 0 | } |
289 | 0 | sub.tv_sec = 60 * 60; |
290 | 0 | sub.tv_nsec = 0; |
291 | 0 | timespecsub(&now, &sub, &now); |
292 | | |
293 | | /* Only consider sockets from the same hostname */ |
294 | 0 | if (!ignore_hosthash) { |
295 | 0 | if ((path = agent_hostname_hash()) == NULL) { |
296 | 0 | error_f("couldn't get hostname hash"); |
297 | 0 | return; |
298 | 0 | } |
299 | 0 | xasprintf(&prefix, "s.%s.", path); |
300 | 0 | free(path); |
301 | 0 | path = NULL; |
302 | 0 | } |
303 | | |
304 | 0 | xasprintf(&dirpath, "%s/%s", homedir, _PATH_SSH_AGENT_SOCKET_DIR); |
305 | 0 | if ((d = opendir(dirpath)) == NULL) { |
306 | 0 | if (errno != ENOENT) |
307 | 0 | error_f("opendir \"%s\": %s", dirpath, strerror(errno)); |
308 | 0 | goto out; |
309 | 0 | } |
310 | | |
311 | 0 | path = NULL; |
312 | 0 | while ((dp = readdir(d)) != NULL) { |
313 | 0 | free(path); |
314 | 0 | xasprintf(&path, "%s/%s", dirpath, dp->d_name); |
315 | | #ifdef HAVE_DIRENT_D_TYPE |
316 | | if (dp->d_type != DT_SOCK && dp->d_type != DT_UNKNOWN) |
317 | | continue; |
318 | | #endif |
319 | 0 | if (fstatat(dirfd(d), dp->d_name, |
320 | 0 | &sb, AT_SYMLINK_NOFOLLOW) != 0 && errno != ENOENT) { |
321 | 0 | error_f("stat \"%s/%s\": %s", |
322 | 0 | dirpath, dp->d_name, strerror(errno)); |
323 | 0 | continue; |
324 | 0 | } |
325 | 0 | if (!S_ISSOCK(sb.st_mode)) |
326 | 0 | continue; |
327 | 0 | #ifdef HAVE_STRUCT_STAT_ST_MTIM |
328 | 0 | mtimp = &sb.st_mtim; |
329 | | #else |
330 | | sub.tv_sec = sb.st_mtime; |
331 | | sub.tv_nsec = 0; |
332 | | mtimp = ⊂ |
333 | | #endif |
334 | 0 | if (timespeccmp(mtimp, &now, >)) { |
335 | 0 | debug3_f("Ignoring recent socket \"%s/%s\"", |
336 | 0 | dirpath, dp->d_name); |
337 | 0 | continue; |
338 | 0 | } |
339 | 0 | if (!ignore_hosthash && |
340 | 0 | strncmp(dp->d_name, prefix, strlen(prefix)) != 0) { |
341 | 0 | debug3_f("Ignoring socket \"%s/%s\" " |
342 | 0 | "from different host", dirpath, dp->d_name); |
343 | 0 | continue; |
344 | 0 | } |
345 | 0 | if (socket_is_stale(path)) { |
346 | 0 | debug_f("cleanup stale socket %s", path); |
347 | 0 | unlinkat(dirfd(d), dp->d_name, 0); |
348 | 0 | } |
349 | 0 | } |
350 | 0 | out: |
351 | 0 | if (d != NULL) |
352 | 0 | closedir(d); |
353 | 0 | free(path); |
354 | 0 | free(dirpath); |
355 | 0 | free(prefix); |
356 | 0 | } |
357 | | |
358 | | #undef unlinkat |
359 | | #undef fstatat |