jcs's openbsd hax
openbsd
at jcs 332 lines 8.6 kB view raw
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}