Coverage Report

Created: 2025-07-01 07:01

/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 = &sub;
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