Coverage Report

Created: 2025-10-10 06:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openssh/misc-agent.c
Line
Count
Source
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
#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
0
#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
0
{
53
0
  char hostname[NI_MAXHOST], p[65];
54
0
  u_char hash[64];
55
0
  int r;
56
0
  size_t l, i;
57
58
0
  l = ssh_digest_bytes(SSH_DIGEST_SHA512);
59
0
  if (len > 64) {
60
0
    error_f("bad length %zu >= max %zd", len, l);
61
0
    return NULL;
62
0
  }
63
0
  if (gethostname(hostname, sizeof(hostname)) == -1) {
64
0
    error_f("gethostname: %s", strerror(errno));
65
0
    return NULL;
66
0
  }
67
0
  if ((r = ssh_digest_memory(SSH_DIGEST_SHA512,
68
0
      hostname, strlen(hostname), hash, sizeof(hash))) != 0) {
69
0
    error_fr(r, "ssh_digest_memory");
70
0
    return NULL;
71
0
  }
72
0
  memset(p, '\0', sizeof(p));
73
0
  for (i = 0; i < l; i++)
74
0
    p[i] = presentation_chars[
75
0
        hash[i] % (sizeof(presentation_chars) - 1)];
76
  /* debug3_f("hostname \"%s\" => hash \"%s\"", hostname, p); */
77
0
  p[len] = '\0';
78
0
  return xstrdup(p);
79
0
}
80
81
char *
82
agent_hostname_hash(void)
83
0
{
84
0
  return hostname_hash(SOCKET_HOSTNAME_HASHLEN);
85
0
}
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
0
{
94
0
  struct sockaddr_un sunaddr;
95
0
  int good, sock = -1;
96
0
  size_t i, xstart;
97
0
  mode_t prev_mask;
98
99
  /* Find first 'X' template character back from end of string */
100
0
  xstart = strlen(path);
101
0
  while (xstart > 0 && path[xstart - 1] == 'X')
102
0
    xstart--;
103
104
0
  memset(&sunaddr, 0, sizeof(sunaddr));
105
0
  sunaddr.sun_family = AF_UNIX;
106
0
  prev_mask = umask(0177);
107
0
  for (good = 0; !good;) {
108
0
    sock = -1;
109
    /* Randomise path suffix */
110
0
    for (i = xstart; path[i] != '\0'; i++) {
111
0
      path[i] = presentation_chars[
112
0
          arc4random_uniform(sizeof(presentation_chars)-1)];
113
0
    }
114
0
    debug_f("trying path \"%s\"", path);
115
116
0
    if (strlcpy(sunaddr.sun_path, path,
117
0
        sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
118
0
      error_f("path \"%s\" too long for Unix domain socket",
119
0
          path);
120
0
      break;
121
0
    }
122
123
0
    if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
124
0
      error_f("socket: %.100s", strerror(errno));
125
0
      break;
126
0
    }
127
0
    if (bind(sock, (struct sockaddr *)&sunaddr,
128
0
        sizeof(sunaddr)) == -1) {
129
0
      if (errno == EADDRINUSE) {
130
0
        error_f("bind \"%s\": %.100s",
131
0
            path, strerror(errno));
132
0
        close(sock);
133
0
        sock = -1;
134
0
        continue;
135
0
      }
136
0
      error_f("bind \"%s\": %.100s", path, strerror(errno));
137
0
      break;
138
0
    }
139
0
    if (listen(sock, backlog) == -1) {
140
0
      error_f("listen \"%s\": %s", path, strerror(errno));
141
0
      break;
142
0
    }
143
0
    good = 1;
144
0
  }
145
0
  umask(prev_mask);
146
0
  if (good) {
147
0
    debug3_f("listening on unix socket \"%s\" as fd=%d",
148
0
        path, sock);
149
0
  } else if (sock != -1) {
150
0
    close(sock);
151
0
    sock = -1;
152
0
  }
153
0
  return sock;
154
0
}
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
0
{
163
0
  char *path;
164
165
0
  xasprintf(&path, "%s/%s", homedir, subdir);
166
0
  if (mkdir(path, 0700) == 0)
167
0
    debug("created directory %s", path);
168
0
  else if (errno != EEXIST) {
169
0
    error_f("mkdir %s: %s", path, strerror(errno));
170
0
    free(path);
171
0
    return -1;
172
0
  }
173
0
  free(path);
174
0
  return 0;
175
0
}
176
177
static int
178
agent_prepare_sockdir(const char *homedir)
179
0
{
180
0
  if (homedir == NULL || *homedir == '\0' ||
181
0
      ensure_mkdir(homedir, _PATH_SSH_USER_DIR) != 0 ||
182
0
      ensure_mkdir(homedir, _PATH_SSH_AGENT_SOCKET_DIR) != 0)
183
0
    return -1;
184
0
  return 0;
185
0
}
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
0
{
192
0
  char *hostnamehash, *ret;
193
194
0
  if ((hostnamehash = hostname_hash(SOCKET_HOSTNAME_HASHLEN)) == NULL)
195
0
    return NULL;
196
0
  xasprintf(&ret, "%s/%s/s.%s.%s.XXXXXXXXXX",
197
0
      homedir, _PATH_SSH_AGENT_SOCKET_DIR, hostnamehash, tag);
198
0
  free(hostnamehash);
199
0
  return ret;
200
0
}
201
202
int
203
agent_listener(const char *homedir, const char *tag, int *sockp, char **pathp)
204
0
{
205
0
  int sock;
206
0
  char *path;
207
208
0
  *sockp = -1;
209
0
  *pathp = NULL;
210
211
0
  if (agent_prepare_sockdir(homedir) != 0)
212
0
    return -1; /* error already logged */
213
0
  if ((path = agent_socket_template(homedir, tag)) == NULL)
214
0
    return -1; /* error already logged */
215
0
  if ((sock = unix_listener_tmp(path, SSH_LISTEN_BACKLOG)) == -1) {
216
0
    free(path);
217
0
    return -1; /* error already logged */
218
0
  }
219
  /* success */
220
0
  *sockp = sock;
221
0
  *pathp = path;
222
0
  return 0;
223
0
}
224
225
static int
226
socket_is_stale(const char *path)
227
0
{
228
0
  int fd, r;
229
0
  struct sockaddr_un sunaddr;
230
0
  socklen_t l = sizeof(r);
231
232
  /* attempt non-blocking connect on socket */
233
0
  memset(&sunaddr, '\0', sizeof(sunaddr));
234
0
  sunaddr.sun_family = AF_UNIX;
235
0
  if (strlcpy(sunaddr.sun_path, path,
236
0
      sizeof(sunaddr.sun_path)) >= sizeof(sunaddr.sun_path)) {
237
0
    debug_f("path for \"%s\" too long for sockaddr_un", path);
238
0
    return 0;
239
0
  }
240
0
  if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) {
241
0
    error_f("socket: %s", strerror(errno));
242
0
    return 0;
243
0
  }
244
0
  set_nonblock(fd);
245
  /* a socket without a listener should yield an error immediately */
246
0
  if (connect(fd, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) {
247
0
    debug_f("connect \"%s\": %s", path, strerror(errno));
248
0
    close(fd);
249
0
    return 1;
250
0
  }
251
0
  if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &r, &l) == -1) {
252
0
    debug_f("getsockopt: %s", strerror(errno));
253
0
    close(fd);
254
0
    return 0;
255
0
  }
256
0
  if (r != 0) {
257
0
    debug_f("socket error on %s: %s", path, strerror(errno));
258
0
    close(fd);
259
0
    return 1;
260
0
  }
261
0
  close(fd);
262
0
  debug_f("socket %s seems still active", path);
263
0
  return 0;
264
0
}
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
0
{
276
0
  DIR *d = NULL;
277
0
  struct dirent *dp;
278
0
  struct stat sb;
279
0
  char *prefix = NULL, *dirpath = NULL, *path = NULL;
280
0
  struct timespec now, sub, *mtimp = NULL;
281
282
  /* Only consider sockets last modified > 1 hour ago */
283
0
  if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
284
0
    error_f("clock_gettime: %s", strerror(errno));
285
0
    return;
286
0
  }
287
0
  sub.tv_sec = 60 * 60;
288
0
  sub.tv_nsec = 0;
289
0
  timespecsub(&now, &sub, &now);
290
291
  /* Only consider sockets from the same hostname */
292
0
  if (!ignore_hosthash) {
293
0
    if ((path = agent_hostname_hash()) == NULL) {
294
0
      error_f("couldn't get hostname hash");
295
0
      return;
296
0
    }
297
0
    xasprintf(&prefix, "s.%s.", path);
298
0
    free(path);
299
0
    path = NULL;
300
0
  }
301
302
0
  xasprintf(&dirpath, "%s/%s", homedir, _PATH_SSH_AGENT_SOCKET_DIR);
303
0
  if ((d = opendir(dirpath)) == NULL) {
304
0
    if (errno != ENOENT)
305
0
      error_f("opendir \"%s\": %s", dirpath, strerror(errno));
306
0
    goto out;
307
0
  }
308
309
0
  path = NULL;
310
0
  while ((dp = readdir(d)) != NULL) {
311
0
    free(path);
312
0
    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
0
    if (fstatat(dirfd(d), dp->d_name,
318
0
        &sb, AT_SYMLINK_NOFOLLOW) != 0 && errno != ENOENT) {
319
0
      error_f("stat \"%s/%s\": %s",
320
0
          dirpath, dp->d_name, strerror(errno));
321
0
      continue;
322
0
    }
323
0
    if (!S_ISSOCK(sb.st_mode))
324
0
      continue;
325
0
#ifdef HAVE_STRUCT_STAT_ST_MTIM
326
0
    mtimp = &sb.st_mtim;
327
#else
328
    sub.tv_sec = sb.st_mtime;
329
    sub.tv_nsec = 0;
330
    mtimp = &sub;
331
#endif
332
0
    if (timespeccmp(mtimp, &now, >)) {
333
0
      debug3_f("Ignoring recent socket \"%s/%s\"",
334
0
          dirpath, dp->d_name);
335
0
      continue;
336
0
    }
337
0
    if (!ignore_hosthash &&
338
0
        strncmp(dp->d_name, prefix, strlen(prefix)) != 0) {
339
0
      debug3_f("Ignoring socket \"%s/%s\" "
340
0
          "from different host", dirpath, dp->d_name);
341
0
      continue;
342
0
    }
343
0
    if (socket_is_stale(path)) {
344
0
      debug_f("cleanup stale socket %s", path);
345
0
      unlinkat(dirfd(d), dp->d_name, 0);
346
0
    }
347
0
  }
348
0
 out:
349
0
  if (d != NULL)
350
0
    closedir(d);
351
0
  free(path);
352
0
  free(dirpath);
353
0
  free(prefix);
354
0
}
355
356
#undef unlinkat
357
#undef fstatat