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