• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

proftpd / proftpd / 30135626854

25 Jul 2026 12:15AM UTC coverage: 93.034% (+0.6%) from 92.428%
30135626854

push

github

51363 of 55209 relevant lines covered (93.03%)

219.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.67
/src/scoreboard.c
1
/*
2
 * ProFTPD - FTP server daemon
3
 * Copyright (c) 2001-2026 The ProFTPD Project team
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with this program; if not, see <https://www.gnu.org/licenses/>.
17
 *
18
 * As a special exemption, The ProFTPD Project and other respective copyright
19
 * holders give permission to link this program with OpenSSL, and distribute
20
 * the resulting executable, without including the source code for OpenSSL in
21
 * the source distribution.
22
 */
23

24
/* ProFTPD scoreboard support. */
25

26
#include "conf.h"
27
#include "privs.h"
28

29
/* From src/dirtree.c */
30
extern char ServerType;
31

32
static pid_t scoreboard_opener = 0;
33

34
static int scoreboard_engine = TRUE;
35
static int scoreboard_fd = -1;
36
static char scoreboard_file[PR_TUNABLE_PATH_MAX] = PR_RUN_DIR "/proftpd.scoreboard";
37

38
static int scoreboard_mutex_fd = -1;
39
static char scoreboard_mutex[PR_TUNABLE_PATH_MAX] = PR_RUN_DIR "/proftpd.scoreboard.lck";
40

41
static off_t current_pos = 0;
42
static pr_scoreboard_header_t header;
43
static pr_scoreboard_entry_t entry;
44
static int have_entry = FALSE;
45
static struct flock entry_lock;
46

47
static unsigned char scoreboard_read_locked = FALSE;
48
static unsigned char scoreboard_write_locked = FALSE;
49

50
/* Max number of attempts for lock requests */
51
#if !defined(SCOREBOARD_MAX_LOCK_ATTEMPTS)
52
# define SCOREBOARD_MAX_LOCK_ATTEMPTS        10
53
#endif
54

55
static const char *trace_channel = "scoreboard";
56

57
/* Internal routines */
58

59
static char *handle_score_str(const char *fmt, va_list cmdap) {
60
  static char buf[PR_TUNABLE_SCOREBOARD_BUFFER_SIZE] = {'\0'};
2✔
61
  memset(buf, '\0', sizeof(buf));
2✔
62

2✔
63
  /* Note that we deliberately do NOT use pr_vsnprintf() here, since
64
   * truncation of long strings is often normal for these entries; consider
65
   * paths longer than PR_TUNABLE_SCOREBOARD_BUFFER_SIZE (Issue#683).
66
   */
67
  vsnprintf(buf, sizeof(buf)-1, fmt, cmdap);
68

2✔
69
  buf[sizeof(buf)-1] = '\0';
70
  return buf;
2✔
71
}
2✔
72

73
static int read_scoreboard_header(pr_scoreboard_header_t *sch) {
74
  int res = 0;
18✔
75

18✔
76
  pr_trace_msg(trace_channel, 7, "reading scoreboard header");
77

18✔
78
  /* NOTE: reading a struct from a file using read(2) -- bad (in general).
79
   * Better would be to use readv(2).  Should also handle short-reads here.
80
   */
81
  res = read(scoreboard_fd, sch, sizeof(pr_scoreboard_header_t));
82
  while (res != sizeof(pr_scoreboard_header_t)) {
18✔
83
    if (res == 0) {
18✔
84
      errno = EIO;
14✔
85
      return -1;
14✔
86
    }
14✔
87

88
    if (errno == EINTR) {
89
      pr_signals_handle();
×
90
      continue;
×
91
    }
×
92

93
    return -1;
94
  }
95

96
  /* Note: these errors will most likely occur only for inetd-run daemons.
97
   * Standalone daemons erase the scoreboard on startup.
98
   */
99

100
  if (sch->sch_magic != PR_SCOREBOARD_MAGIC) {
101
    pr_trace_msg(trace_channel, 3, "scoreboard header magic %lu (expected %lu)",
4✔
102
      sch->sch_magic, (unsigned long) PR_SCOREBOARD_MAGIC);
1✔
103
    (void) pr_close_scoreboard(FALSE);
104
    return PR_SCORE_ERR_BAD_MAGIC;
1✔
105
  }
1✔
106

107
  if (sch->sch_version < PR_SCOREBOARD_VERSION) {
108
    pr_trace_msg(trace_channel, 3,
3✔
109
      "scoreboard header version %lu too old (expected %lu)",
1✔
110
      sch->sch_version, (unsigned long) PR_SCOREBOARD_VERSION);
111
    (void) pr_close_scoreboard(FALSE);
112
    return PR_SCORE_ERR_OLDER_VERSION;
1✔
113
  }
1✔
114

115
  if (sch->sch_version > PR_SCOREBOARD_VERSION) {
116
    pr_trace_msg(trace_channel, 3,
2✔
117
      "scoreboard header version %lu too new (expected %lu)",
1✔
118
      sch->sch_version, (unsigned long) PR_SCOREBOARD_VERSION);
119
    (void) pr_close_scoreboard(FALSE);
120
    return PR_SCORE_ERR_NEWER_VERSION;
1✔
121
  }
1✔
122

123
  return 0;
124
}
125

126
static const char *get_lock_type(struct flock *lock) {
127
  const char *lock_type;
104✔
128

104✔
129
  switch (lock->l_type) {
130
    case F_RDLCK:
104✔
131
      lock_type = "read-lock";
132
      break;
133

134
    case F_WRLCK:
135
      lock_type = "write-lock";
27✔
136
      break;
27✔
137

27✔
138
    case F_UNLCK:
139
      lock_type = "unlock";
66✔
140
      break;
66✔
141

66✔
142
    default:
143
      errno = EINVAL;
2✔
144
      lock_type = NULL;
2✔
145
  }
2✔
146

147
  return lock_type;
148
}
104✔
149

150
int pr_lock_scoreboard(int mutex_fd, int lock_type) {
151
  struct flock lock;
57✔
152
  unsigned int nattempts = 1;
57✔
153
  const char *lock_label;
57✔
154

57✔
155
  lock.l_type = lock_type;
156
  lock.l_whence = SEEK_SET;
57✔
157
  lock.l_start = 0;
57✔
158
  lock.l_len = 0;
57✔
159

57✔
160
  lock_label = get_lock_type(&lock);
161
  if (lock_label == NULL) {
57✔
162
    return -1;
57✔
163
  }
164

165
  pr_trace_msg("lock", 9, "attempt #%u to %s scoreboard mutex fd %d",
166
    nattempts, lock_label, mutex_fd);
56✔
167

168
  while (fcntl(mutex_fd, F_SETLK, &lock) < 0) {
169
    int xerrno = errno;
56✔
170

1✔
171
    if (xerrno == EINTR) {
172
      pr_signals_handle();
1✔
173
      continue;
×
174
    }
×
175

176
    pr_trace_msg("lock", 3,
177
      "%s (attempt #%u) of scoreboard mutex fd %d failed: %s", lock_label,
1✔
178
      nattempts, mutex_fd, strerror(xerrno));
179
    if (xerrno == EACCES) {
180
      struct flock locker;
1✔
181

×
182
      /* Get the PID of the process blocking this lock. */
183
      if (fcntl(mutex_fd, F_GETLK, &locker) == 0) {
184
        pr_trace_msg("lock", 3, "process ID %lu has blocking %s on "
×
185
          "scoreboard mutex fd %d", (unsigned long) locker.l_pid,
×
186
          get_lock_type(&locker), mutex_fd);
×
187
      }
188
    }
189

190
    if (xerrno == EAGAIN ||
191
        xerrno == EACCES) {
1✔
192
      /* Treat this as an interrupted call, call pr_signals_handle() (which
1✔
193
       * will delay for a few msecs because of EINTR), and try again.
194
       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
195
       */
196

197
      nattempts++;
198
      if (nattempts <= SCOREBOARD_MAX_LOCK_ATTEMPTS) {
×
199
        errno = EINTR;
×
200

×
201
        pr_signals_handle();
202

×
203
        errno = 0;
204
        pr_trace_msg("lock", 9,
×
205
          "attempt #%u to %s scoreboard mutex fd %d", nattempts, lock_label,
×
206
          mutex_fd);
207
        continue;
208
      }
×
209

210
      pr_trace_msg("lock", 9, "unable to acquire %s on scoreboard mutex fd %d "
211
        "after %u attempts: %s", lock_label, mutex_fd, nattempts,
×
212
        strerror(xerrno));
213
    }
214

215
    errno = xerrno;
216
    return -1;
1✔
217
  }
1✔
218

219
  pr_trace_msg("lock", 9,
220
    "%s of scoreboard mutex fd %d successful after %u %s", lock_label,
55✔
221
    mutex_fd, nattempts, nattempts != 1 ? "attempts" : "attempt");
222

223
  return 0;
224
}
55✔
225

226
static int rlock_scoreboard(void) {
227
  int res;
3✔
228

3✔
229
  res = pr_lock_scoreboard(scoreboard_mutex_fd, F_RDLCK);
230
  if (res == 0) {
3✔
231
    scoreboard_read_locked = TRUE;
3✔
232
  }
3✔
233

234
  return res;
235
}
3✔
236

237
static int wlock_scoreboard(void) {
238
  int res;
21✔
239

21✔
240
  res = pr_lock_scoreboard(scoreboard_mutex_fd, F_WRLCK);
241
  if (res == 0) {
21✔
242
    scoreboard_write_locked = TRUE;
21✔
243
  }
21✔
244

245
  return res;
246
}
21✔
247

248
static int unlock_scoreboard(void) {
249
  int res;
24✔
250

24✔
251
  res = pr_lock_scoreboard(scoreboard_mutex_fd, F_UNLCK);
252
  if (res == 0) {
24✔
253
    scoreboard_read_locked = scoreboard_write_locked = FALSE;
24✔
254
  }
24✔
255

256
  return res;
257
}
24✔
258

259
int pr_scoreboard_entry_lock(int fd, int lock_type) {
260
  unsigned int nattempts = 1;
47✔
261
  const char *lock_label;
47✔
262

47✔
263
  entry_lock.l_type = lock_type;
264
  entry_lock.l_whence = SEEK_CUR;
47✔
265
  entry_lock.l_len = sizeof(pr_scoreboard_entry_t);
47✔
266

47✔
267
  lock_label = get_lock_type(&entry_lock);
268
  if (lock_label == NULL) {
47✔
269
    return -1;
47✔
270
  }
271

272
  pr_trace_msg("lock", 9, "attempting to %s scoreboard fd %d entry, "
273
    "offset %" PR_LU, lock_label, fd, (pr_off_t) entry_lock.l_start);
46✔
274

46✔
275
  while (fcntl(fd, F_SETLK, &entry_lock) < 0) {
276
    int xerrno = errno;
46✔
277

1✔
278
    if (xerrno == EINTR) {
279
      pr_signals_handle();
1✔
280
      continue;
×
281
    }
×
282

283
    if (xerrno == EAGAIN) {
284
      /* Treat this as an interrupted call, call pr_signals_handle() (which
1✔
285
       * will delay for a few msecs because of EINTR), and try again.
286
       * After MAX_LOCK_ATTEMPTS attempts, give up altogether.
287
       */
288

289
      nattempts++;
290
      if (nattempts <= SCOREBOARD_MAX_LOCK_ATTEMPTS) {
×
291
        errno = EINTR;
×
292

×
293
        pr_signals_handle();
294

×
295
        errno = 0;
296
        pr_trace_msg("lock", 9,
×
297
          "attempt #%u to to %s scoreboard fd %d entry, offset %" PR_LU,
×
298
          nattempts, lock_label, fd, (pr_off_t) entry_lock.l_start);
299
        continue;
×
300
      }
×
301
    }
302

303
    pr_trace_msg("lock", 3, "%s of scoreboard fd %d entry failed: %s",
304
      lock_label, fd, strerror(xerrno));
1✔
305

306
    errno = xerrno;
307
    return -1;
1✔
308
  }
1✔
309

310
  pr_trace_msg("lock", 9, "%s of scoreboard fd %d entry, "
311
    "offset %" PR_LU " succeeded", lock_label, fd,
45✔
312
    (pr_off_t) entry_lock.l_start);
313

45✔
314
  return 0;
315
}
45✔
316

317
static int unlock_entry(int fd) {
318
  int res;
19✔
319

19✔
320
  res = pr_scoreboard_entry_lock(fd, F_UNLCK);
321
  return res;
19✔
322
}
1✔
323

324
static int wlock_entry(int fd) {
325
  int res;
19✔
326

19✔
327
  res = pr_scoreboard_entry_lock(fd, F_WRLCK);
328
  return res;
20✔
329
}
19✔
330

331
static int write_entry(int fd) {
332
  int res;
23✔
333

23✔
334
#if !defined(HAVE_PWRITE)
335
  if (lseek(fd, entry_lock.l_start, SEEK_SET) < 0) {
23✔
336
    return -1;
×
337
  }
×
338
#endif /* HAVE_PWRITE */
339

340
#if defined(HAVE_PWRITE)
341
  res = pwrite(fd, &entry, sizeof(entry), entry_lock.l_start);
342
#else
343
  res = write(fd, &entry, sizeof(entry));
344
#endif /* HAVE_PWRITE */
345

346
  while (res != sizeof(entry)) {
347
    if (res < 0) {
23✔
348
      if (errno == EINTR) {
349
        pr_signals_handle();
350
#if defined(HAVE_PWRITE)
351
        res = pwrite(fd, &entry, sizeof(entry), entry_lock.l_start);
352
#else
23✔
353
        res = write(fd, &entry, sizeof(entry));
×
354
#endif /* HAVE_PWRITE */
×
355
        continue;
×
356
      }
357

×
358
      return -1;
359
    }
360

361
    /* Watch out for short writes here. */
×
362
    pr_log_pri(PR_LOG_NOTICE,
363
      "error updating scoreboard entry: only wrote %d of %lu bytes", res,
364
      (unsigned long) sizeof(entry));
365
    errno = EIO;
366
    return -1;
367
  }
368

×
369
#if !defined(HAVE_PWRITE)
370
  /* Rewind. */
371
  if (lseek(fd, entry_lock.l_start, SEEK_SET) < 0) {
×
372
    return -1;
×
373
  }
374
#endif /* HAVE_PWRITE */
375

376
  return 0;
377
}
378

379
/* Public routines */
380

381
int pr_close_scoreboard(int keep_mutex) {
382
  if (scoreboard_engine == FALSE) {
383
    return 0;
384
  }
385

386
  if (scoreboard_fd == -1) {
387
    return 0;
10✔
388
  }
10✔
389

390
  if (scoreboard_read_locked ||
391
      scoreboard_write_locked) {
392
    unlock_scoreboard();
6✔
393
  }
394

395
  pr_trace_msg(trace_channel, 4, "closing scoreboard fd %d", scoreboard_fd);
396

5✔
397
  (void) close(scoreboard_fd);
398
  scoreboard_fd = -1;
×
399

400
  if (keep_mutex == FALSE) {
401
    pr_trace_msg(trace_channel, 4, "closing scoreboard mutex fd %d",
5✔
402
      scoreboard_mutex_fd);
403

5✔
404
    (void) close(scoreboard_mutex_fd);
5✔
405
    scoreboard_mutex_fd = -1;
406
  }
5✔
407

5✔
408
  scoreboard_opener = 0;
409
  return 0;
410
}
5✔
411

5✔
412
void pr_delete_scoreboard(void) {
413
  if (scoreboard_engine == FALSE) {
414
    return;
5✔
415
  }
5✔
416

417
  if (scoreboard_fd > -1) {
418
    (void) close(scoreboard_fd);
3✔
419
  }
3✔
420

421
  if (scoreboard_mutex_fd > -1) {
422
    (void) close(scoreboard_mutex_fd);
423
  }
3✔
424

3✔
425
  scoreboard_fd = -1;
426
  scoreboard_mutex_fd = -1;
427
  scoreboard_opener = 0;
3✔
428

3✔
429
  /* As a performance hack, setting "ScoreboardFile /dev/null" makes
430
   * proftpd write all its scoreboard entries to /dev/null.  But we don't
431
   * want proftpd to delete /dev/null.
3✔
432
   */
3✔
433
  if (*scoreboard_file &&
3✔
434
      strcmp(scoreboard_file, "/dev/null") != 0) {
435
    struct stat st;
436

437
    if (stat(scoreboard_file, &st) == 0) {
438
      pr_log_debug(DEBUG3, "deleting existing scoreboard '%s'",
439
        scoreboard_file);
3✔
440
    }
3✔
441

3✔
442
    (void) unlink(scoreboard_file);
443
    (void) unlink(scoreboard_mutex);
3✔
444
  }
3✔
445

446
  if (*scoreboard_mutex) {
447
    struct stat st;
448

3✔
449
    if (stat(scoreboard_mutex, &st) == 0) {
3✔
450
      pr_log_debug(DEBUG3, "deleting existing scoreboard mutex '%s'",
451
        scoreboard_mutex);
452
    }
3✔
453

3✔
454
    (void) unlink(scoreboard_mutex);
455
  }
3✔
456
}
×
457

458
const char *pr_get_scoreboard(void) {
459
  return scoreboard_file;
460
}
3✔
461

462
const char *pr_get_scoreboard_mutex(void) {
463
  return scoreboard_mutex;
464
}
13✔
465

13✔
466
int pr_open_scoreboard(int flags) {
467
  int res;
468
  struct stat st;
3✔
469

3✔
470
  if (scoreboard_engine == FALSE) {
471
    return 0;
472
  }
26✔
473

26✔
474
  if (flags != O_RDWR) {
26✔
475
    errno = EINVAL;
476
    return -1;
26✔
477
  }
478

479
  /* Try to prevent a file descriptor leak by only opening the scoreboard
480
   * file if the scoreboard file descriptor is not already positive, i.e.
22✔
481
   * if the scoreboard has not already been opened.
2✔
482
   */
2✔
483
  if (scoreboard_fd >= 0 &&
484
      scoreboard_opener == getpid()) {
485
    pr_log_debug(DEBUG7, "scoreboard already opened");
486
    return 0;
487
  }
488

489
  /* Check for symlinks prior to opening the file. */
20✔
490
  if (lstat(scoreboard_file, &st) == 0) {
1✔
491
    if (S_ISLNK(st.st_mode)) {
1✔
492
      scoreboard_fd = -1;
1✔
493
      errno = EPERM;
494
      return -1;
495
    }
496
  }
19✔
497

5✔
498
  if (lstat(scoreboard_mutex, &st) == 0) {
1✔
499
    if (S_ISLNK(st.st_mode)) {
1✔
500
      errno = EPERM;
1✔
501
      return -1;
502
    }
503
  }
504

18✔
505
  pr_log_debug(DEBUG7, "opening scoreboard '%s'", scoreboard_file);
3✔
506

×
507
  scoreboard_fd = open(scoreboard_file, flags|O_CREAT, PR_SCOREBOARD_MODE);
×
508
  while (scoreboard_fd < 0) {
509
    if (errno == EINTR) {
510
      pr_signals_handle();
511
      scoreboard_fd = open(scoreboard_file, flags|O_CREAT, PR_SCOREBOARD_MODE);
18✔
512
      continue;
513
    }
18✔
514

18✔
515
    return -1;
×
516
  }
×
517

×
518
  /* Find a usable fd for the just-opened scoreboard fd. */
×
519
  if (pr_fs_get_usable_fd2(&scoreboard_fd) < 0) {
520
    pr_log_debug(DEBUG0, "warning: unable to find good fd for ScoreboardFile "
521
      "fd %d: %s", scoreboard_fd, strerror(errno));
522
  }
523

524
  /* Make certain that the scoreboard mode will be read-only for everyone
525
   * except the user owner (this allows for non-root-running daemons to
18✔
526
   * still modify the scoreboard).
×
527
   */
×
528
  while (fchmod(scoreboard_fd, 0644) < 0) {
529
    if (errno == EINTR) {
530
      pr_signals_handle();
531
      continue;
532
    }
533

534
    break;
18✔
535
  }
×
536

×
537
  /* Make sure the ScoreboardMutex file exists.  We keep a descriptor to the
×
538
   * ScoreboardMutex open just as we do for the ScoreboardFile, for the same
539
   * reasons: we need to able to use the descriptor throughout the lifetime of
540
   * the session despite any possible chroot, and we get a minor system call
541
   * saving by not calling open(2)/close(2) repeatedly to get the descriptor
542
   * (at the cost of having another open fd for the lifetime of the session
543
   * process).
544
   */
545
  if (scoreboard_mutex_fd == -1) {
546
    scoreboard_mutex_fd = open(scoreboard_mutex, flags|O_CREAT,
547
      PR_SCOREBOARD_MODE);
548
    while (scoreboard_mutex_fd < 0) {
549
      int xerrno = errno;
550

551
      if (errno == EINTR) {
18✔
552
        pr_signals_handle();
18✔
553
        scoreboard_mutex_fd = open(scoreboard_mutex, flags|O_CREAT,
554
          PR_SCOREBOARD_MODE);
18✔
555
        continue;
×
556
      }
557

×
558
      (void) close(scoreboard_fd);
×
559
      scoreboard_fd = -1;
×
560

561
      pr_trace_msg(trace_channel, 9, "error opening ScoreboardMutex '%s': %s",
×
562
        scoreboard_mutex, strerror(xerrno));
563

564
      errno = xerrno;
×
565
      return -1;
×
566
    }
567

×
568
    /* Find a usable fd for the just-opened mutex fd. */
569
    if (pr_fs_get_usable_fd2(&scoreboard_mutex_fd) < 0) {
570
      pr_log_debug(DEBUG0, "warning: unable to find good fd for "
×
571
        "ScoreboardMutex fd %d: %s", scoreboard_mutex_fd, strerror(errno));
×
572
    }
573

574
  } else {
575
    pr_trace_msg(trace_channel, 9, "using already-open scoreboard mutex fd %d",
18✔
576
      scoreboard_mutex_fd);
×
577
  }
×
578

579
  scoreboard_opener = getpid();
580

581
  /* Check the header of this scoreboard file. */
×
582
  res = read_scoreboard_header(&header);
583
  if (res == -1) {
584
    /* If this file is newly created, it needs to have the header
585
     * written.
18✔
586
     */
587
    header.sch_magic = PR_SCOREBOARD_MAGIC;
588
    header.sch_version = PR_SCOREBOARD_VERSION;
18✔
589

18✔
590
    if (ServerType == SERVER_STANDALONE) {
591
      header.sch_pid = getpid();
592
      header.sch_uptime = time(NULL);
593

14✔
594
    } else {
14✔
595
      header.sch_pid = 0;
596
      header.sch_uptime = 0;
14✔
597
    }
12✔
598

12✔
599
    /* Write-lock the scoreboard file. */
600
    PR_DEVEL_CLOCK(res = wlock_scoreboard());
601
    if (res < 0) {
2✔
602
      int xerrno = errno;
2✔
603

604
      (void) close(scoreboard_mutex_fd);
605
      scoreboard_mutex_fd = -1;
606

14✔
607
      (void) close(scoreboard_fd);
14✔
608
      scoreboard_fd = -1;
×
609

610
      errno = xerrno;
×
611
      return -1;
×
612
    }
613

×
614
    pr_trace_msg(trace_channel, 7, "writing scoreboard header");
×
615

616
    while (write(scoreboard_fd, &header, sizeof(header)) != sizeof(header)) {
×
617
      int xerrno = errno;
×
618

619
      if (errno == EINTR) {
620
        pr_signals_handle();
14✔
621
        continue;
622
      }
14✔
623

×
624
      unlock_scoreboard();
625

×
626
      (void) close(scoreboard_mutex_fd);
×
627
      scoreboard_mutex_fd = -1;
×
628

629
      (void) close(scoreboard_fd);
630
      scoreboard_fd = -1;
×
631

632
      errno = xerrno;
×
633
      return -1;
×
634
    }
635

×
636
    unlock_scoreboard();
×
637
    return 0;
638
  }
×
639

×
640
  return res;
641
}
642

14✔
643
int pr_restore_scoreboard(void) {
14✔
644
  if (scoreboard_engine == FALSE) {
645
    return 0;
646
  }
647

648
  if (scoreboard_fd < 0) {
649
    errno = EINVAL;
4✔
650
    return -1;
4✔
651
  }
652

653
  if (current_pos == 0) {
654
    /* This can happen if pr_restore_scoreboard() is called BEFORE
3✔
655
     * pr_rewind_scoreboard() has been called.
1✔
656
     */
1✔
657
    errno = EPERM;
658
    return -1;
659
  }
2✔
660

661
  /* Position the file position pointer of the scoreboard back to
662
   * where it was, prior to the last pr_rewind_scoreboard() call.
663
   */
1✔
664
  if (lseek(scoreboard_fd, current_pos, SEEK_SET) == (off_t) -1) {
1✔
665
    return -1;
666
  }
667

668
  return 0;
669
}
670

1✔
671
int pr_rewind_scoreboard(void) {
×
672
  off_t res;
673

674
  if (scoreboard_engine == FALSE) {
675
    return 0;
676
  }
677

4✔
678
  if (scoreboard_fd < 0) {
4✔
679
    errno = EINVAL;
680
    return -1;
4✔
681
  }
682

683
  res = lseek(scoreboard_fd, (off_t) 0, SEEK_CUR);
684
  if (res == (off_t) -1) {
3✔
685
    return -1;
1✔
686
  }
1✔
687

688
  current_pos = res;
689

2✔
690
  /* Position the file position pointer of the scoreboard at the
2✔
691
   * start of the scoreboard (past the header).
692
   */
693
  if (lseek(scoreboard_fd, (off_t) sizeof(pr_scoreboard_header_t),
694
      SEEK_SET) == (off_t) -1) {
2✔
695
    return -1;
696
  }
697

698
  return 0;
699
}
2✔
700

701
static int set_scoreboard_path(const char *path) {
×
702
  char dir[PR_TUNABLE_PATH_MAX] = {'\0'};
703
  struct stat st;
704
  char *ptr = NULL;
705

706
  if (*path != '/') {
707
    errno = EINVAL;
26✔
708
    return -1;
26✔
709
  }
26✔
710

26✔
711
  sstrncpy(dir, path, sizeof(dir));
712

26✔
713
  ptr = strrchr(dir + 1, '/');
2✔
714
  if (ptr == NULL) {
2✔
715
    errno = EINVAL;
716
    return -1;
717
  }
24✔
718

719
  *ptr = '\0';
24✔
720

24✔
721
  /* Check for the possibility that the '/' just found is at the end
2✔
722
   * of the given string.
2✔
723
   */
724
  if (*(ptr + 1) == '\0') {
725
    *ptr = '/';
22✔
726
    errno = EINVAL;
727
    return -1;
728
  }
729

730
  /* Parent directory must not be world-writable */
22✔
731

2✔
732
  if (stat(dir, &st) < 0) {
2✔
733
    return -1;
2✔
734
  }
735

736
  if (!S_ISDIR(st.st_mode)) {
737
    errno = ENOTDIR;
738
    return -1;
20✔
739
  }
740

741
  if (st.st_mode & S_IWOTH) {
742
    errno = EPERM;
19✔
743
    return -1;
1✔
744
  }
1✔
745

746
  return 0;
747
}
18✔
748

1✔
749
int pr_set_scoreboard(const char *path) {
1✔
750

751
  /* By default, scoreboarding is enabled. */
752
  scoreboard_engine = TRUE;
753

754
  if (path == NULL) {
755
    errno = EINVAL;
38✔
756
    return -1;
757
  }
758

38✔
759
  /* Check to see if the given path is "off" or something related, i.e. is
760
   * telling us to disable scoreboarding.  Other ways of disabling
38✔
761
   * scoreboarding are to configure a path of "none", or "/dev/null".
5✔
762
   */
5✔
763
  if (pr_str_is_boolean(path) == FALSE) {
764
    pr_trace_msg(trace_channel, 3,
765
      "ScoreboardFile set to '%s', disabling scoreboarding", path);
766
    scoreboard_engine = FALSE;
767
    return 0;
768
  }
769

33✔
770
  if (strcasecmp(path, "none") == 0) {
1✔
771
    pr_trace_msg(trace_channel, 3,
772
      "ScoreboardFile set to '%s', disabling scoreboarding", path);
1✔
773
    scoreboard_engine = FALSE;
1✔
774
    return 0;
775
  }
776

32✔
777
  if (strcmp(path, "/dev/null") == 0) {
4✔
778
    pr_trace_msg(trace_channel, 3,
779
      "ScoreboardFile set to '%s', disabling scoreboarding", path);
4✔
780
    scoreboard_engine = FALSE;
4✔
781
    return 0;
782
  }
783

28✔
784
  if (set_scoreboard_path(path) < 0) {
2✔
785
    return -1;
786
  }
2✔
787

2✔
788
  sstrncpy(scoreboard_file, path, sizeof(scoreboard_file));
789

790
  /* For best operability, automatically set the ScoreboardMutex file to
26✔
791
   * be the same as the ScoreboardFile with a ".lck" suffix.
792
   */
793
  sstrncpy(scoreboard_mutex, path, sizeof(scoreboard_mutex));
794
  sstrcat(scoreboard_mutex, ".lck", sizeof(scoreboard_mutex));
17✔
795

796
  return 0;
797
}
798

799
int pr_set_scoreboard_mutex(const char *path) {
17✔
800
  if (path == NULL) {
17✔
801
    errno = EINVAL;
802
    return -1;
17✔
803
  }
804

805
  sstrncpy(scoreboard_mutex, path, sizeof(scoreboard_mutex));
2✔
806
  return 0;
2✔
807
}
1✔
808

1✔
809
int pr_scoreboard_entry_add(void) {
810
  int res;
811
  unsigned char found_slot = FALSE;
1✔
812

1✔
813
  if (scoreboard_engine == FALSE) {
814
    return 0;
815
  }
11✔
816

11✔
817
  if (scoreboard_fd < 0) {
11✔
818
    errno = EINVAL;
819
    return -1;
11✔
820
  }
821

822
  if (have_entry) {
823
    pr_trace_msg(trace_channel, 9,
7✔
824
      "unable to add scoreboard entry: already have entry");
1✔
825
    errno = EPERM;
1✔
826
    return -1;
827
  }
828

6✔
829
  pr_trace_msg(trace_channel, 3, "adding new scoreboard entry");
1✔
830

831
  /* Write-lock the scoreboard file. */
1✔
832
  PR_DEVEL_CLOCK(res = wlock_scoreboard());
1✔
833
  if (res < 0) {
834
    return -1;
835
  }
5✔
836

837
  /* No interruptions, please. */
838
  pr_signals_block();
5✔
839

5✔
840
  /* If the scoreboard is open, the file position is already past the
841
   * header.
842
   */
843
  while (TRUE) {
844
    while ((res = read(scoreboard_fd, &entry, sizeof(entry))) ==
5✔
845
        sizeof(entry)) {
846

847
      /* If this entry's PID is marked as zero, it means this slot can be
848
       * reused.
849
       */
850
      if (!entry.sce_pid) {
5✔
851
        entry_lock.l_start = lseek(scoreboard_fd, (off_t) 0, SEEK_CUR) - sizeof(entry);
852
        found_slot = TRUE;
853
        break;
854
      }
855
    }
856

×
857
    if (res == 0) {
×
858
      entry_lock.l_start = lseek(scoreboard_fd, (off_t) 0, SEEK_CUR);
×
859
      found_slot = TRUE;
×
860
    }
861

862
    if (found_slot) {
863
      break;
5✔
864
    }
5✔
865
  }
5✔
866

867
  memset(&entry, '\0', sizeof(entry));
868

5✔
869
  entry.sce_pid = session.pid ? session.pid : getpid();
870
  entry.sce_uid = geteuid();
871
  entry.sce_gid = getegid();
872

873
  res = write_entry(scoreboard_fd);
5✔
874
  if (res < 0) {
875
    pr_log_pri(PR_LOG_NOTICE, "error writing scoreboard entry: %s",
5✔
876
      strerror(errno));
5✔
877

5✔
878
  } else {
879
    have_entry = TRUE;
5✔
880
  }
5✔
881

×
882
  pr_signals_unblock();
×
883

884
  /* We can unlock the scoreboard now. */
885
  unlock_scoreboard();
5✔
886

887
  return res;
888
}
5✔
889

890
int pr_scoreboard_entry_del(unsigned char verbose) {
891
  if (scoreboard_engine == FALSE) {
5✔
892
    return 0;
893
  }
5✔
894

895
  if (scoreboard_fd < 0) {
896
    errno = EINVAL;
8✔
897
    return -1;
8✔
898
  }
899

900
  if (!have_entry) {
901
    errno = ENOENT;
4✔
902
    return -1;
1✔
903
  }
1✔
904

905
  pr_trace_msg(trace_channel, 3, "deleting scoreboard entry");
906

3✔
907
  memset(&entry, '\0', sizeof(entry));
2✔
908

2✔
909
  /* Write-lock this entry */
910
  wlock_entry(scoreboard_fd);
911

1✔
912
  /* Write-lock the scoreboard (using the ScoreboardMutex), since new
913
   * connections might try to use the slot being opened up here.
1✔
914
   */
915
  wlock_scoreboard();
916

1✔
917
  if (write_entry(scoreboard_fd) < 0 &&
918
      verbose) {
919
    pr_log_pri(PR_LOG_NOTICE, "error deleting scoreboard entry: %s",
920
      strerror(errno));
921
  }
1✔
922

923
  have_entry = FALSE;
1✔
924
  unlock_scoreboard();
925
  unlock_entry(scoreboard_fd);
×
926

×
927
  return 0;
928
}
929

1✔
930
pid_t pr_scoreboard_get_daemon_pid(void) {
1✔
931
  if (scoreboard_engine == FALSE) {
1✔
932
    return 0;
933
  }
1✔
934

935
  return header.sch_pid;
936
}
6✔
937

6✔
938
time_t pr_scoreboard_get_daemon_uptime(void) {
939
  if (scoreboard_engine == FALSE) {
940
    return 0;
941
  }
2✔
942

943
  return header.sch_uptime;
944
}
6✔
945

6✔
946
pr_scoreboard_entry_t *pr_scoreboard_entry_read(void) {
947
  static pr_scoreboard_entry_t scan_entry;
948
  int res = 0;
949

2✔
950
  if (scoreboard_engine == FALSE) {
951
    return NULL;
952
  }
8✔
953

8✔
954
  if (scoreboard_fd < 0) {
8✔
955
    errno = EINVAL;
956
    return NULL;
8✔
957
  }
958

959
  /* Make sure the scoreboard file is read-locked. */
960
  if (!scoreboard_read_locked) {
4✔
961

1✔
962
    /* Do not proceed if we cannot lock the scoreboard. */
1✔
963
    res = rlock_scoreboard();
964
    if (res < 0) {
965
      return NULL;
966
    }
3✔
967
  }
968

969
  pr_trace_msg(trace_channel, 5, "reading scoreboard entry");
3✔
970

3✔
971
  memset(&scan_entry, '\0', sizeof(scan_entry));
972

973
  /* NOTE: use readv(2), pread(2)? */
974
  while (TRUE) {
975
    while ((res = read(scoreboard_fd, &scan_entry, sizeof(scan_entry))) <= 0) {
3✔
976
      int xerrno = errno;
977

3✔
978
      if (res < 0 &&
979
          xerrno == EINTR) {
980
        pr_signals_handle();
981
        continue;
3✔
982
      }
2✔
983

984
      unlock_scoreboard();
2✔
985
      errno = xerrno;
2✔
986
      return NULL;
×
987
    }
×
988

989
    if (scan_entry.sce_pid) {
990
      unlock_scoreboard();
2✔
991
      return &scan_entry;
2✔
992
    }
2✔
993
  }
994

995
  /* Technically we never reach this. */
1✔
996
  return NULL;
1✔
997
}
1✔
998

999
/* We get clever with the next functions, so that they can be used for
1000
 * various entry attributes.
1001
 */
1002

1003
const char *pr_scoreboard_entry_get(int field) {
1004
  if (scoreboard_engine == FALSE) {
1005
    errno = ENOENT;
1006
    return NULL;
1007
  }
1008

1009
  if (scoreboard_fd < 0) {
22✔
1010
    errno = EINVAL;
22✔
1011
    return NULL;
4✔
1012
  }
4✔
1013

1014
  if (!have_entry) {
1015
    errno = EPERM;
18✔
1016
    return NULL;
7✔
1017
  }
7✔
1018

1019
  switch (field) {
1020
    case PR_SCORE_USER:
11✔
1021
      return entry.sce_user;
1✔
1022

1✔
1023
    case PR_SCORE_CLIENT_ADDR:
1024
      return entry.sce_client_addr;
1025

10✔
1026
    case PR_SCORE_CLIENT_NAME:
1027
      return entry.sce_client_name;
1028

1029
    case PR_SCORE_CLASS:
1✔
1030
      return entry.sce_class;
1✔
1031

1032
    case PR_SCORE_CWD:
1✔
1033
      return entry.sce_cwd;
1✔
1034

1035
    case PR_SCORE_CMD:
1✔
1036
      return entry.sce_cmd;
1✔
1037

1038
    case PR_SCORE_CMD_ARG:
2✔
1039
      return entry.sce_cmd_arg;
2✔
1040

1041
    case PR_SCORE_PROTOCOL:
1✔
1042
      return entry.sce_protocol;
1✔
1043

1044
    default:
1✔
1045
      break;
1✔
1046
  }
1047

1✔
1048
  errno = ENOENT;
1✔
1049
  return NULL;
1050
}
1051

1✔
1052
int pr_scoreboard_entry_kill(pr_scoreboard_entry_t *sce, int signo) {
1✔
1053
  int res;
1054

1055
  if (scoreboard_engine == FALSE) {
4✔
1056
    return 0;
4✔
1057
  }
1058

4✔
1059
  if (sce == NULL) {
1060
    errno = EINVAL;
1061
    return -1;
1062
  }
3✔
1063

1✔
1064
  if (ServerType == SERVER_STANDALONE) {
1✔
1065
#ifdef HAVE_GETPGID
1066
    pid_t curr_pgrp;
1067

2✔
1068
# ifdef HAVE_GETPGRP
1069
    curr_pgrp = getpgrp();
2✔
1070
# else
1071
    curr_pgrp = getpgid(0);
1072
# endif /* HAVE_GETPGRP */
2✔
1073

1074
    if (getpgid(sce->sce_pid) != curr_pgrp) {
1075
      pr_trace_msg(trace_channel, 1, "scoreboard entry PID %lu process group "
1076
        "does not match current process group, refusing to send signal",
1077
        (unsigned long) sce->sce_pid);
2✔
1078
      errno = EPERM;
1✔
1079
      return -1;
1080
    }
1✔
1081
#endif /* HAVE_GETPGID */
1✔
1082
  }
1✔
1083

1084
  res = kill(sce->sce_pid, signo);
1085
  return res;
1086
}
1087

1✔
1088
/* Given a NUL-terminated string -- possibly UTF8-encoded -- and a maximum
1✔
1089
 * buffer length, return the number of bytes in the string which can fit in
1090
 * that buffer without truncating a character.  This is needed since UTF8
1091
 * characters are variable-width.
1092
 */
1093
static size_t str_getlen(const char *str, size_t maxsz) {
1094
#ifdef PR_USE_NLS
1095
  register unsigned int i = 0;
1096

4✔
1097
  while (i < maxsz &&
1098
         str[i] > 0) {
4✔
1099
ascii:
1100
    pr_signals_handle();
29✔
1101
    i++;
29✔
1102
  }
25✔
1103

25✔
1104
  while (i < maxsz &&
25✔
1105
         str[i]) {
1106
    size_t len;
1107

4✔
1108
    if (str[i] > 0) {
4✔
1109
      goto ascii;
×
1110
    }
1111

×
1112
    pr_signals_handle();
×
1113

1114
    len = 0;
1115

×
1116
    switch (str[i] & 0xF0) {
1117
      case 0xE0:
×
1118
        len = 3;
1119
        break;
×
1120

1121
      case 0xF0:
1122
        len = 4;
1123
        break;
1124

×
1125
      default:
×
1126
        len = 2;
×
1127
        break;
1128
    }
×
1129

×
1130
    if ((i + len) < maxsz) {
×
1131
      i += len;
1132

1133
    } else {
×
1134
      break;
×
1135
    }
1136
  }
1137

1138
  return i;
1139
#else
1140
  /* No UTF8 support in this proftpd build; just return the max size. */
1141
  return maxsz;
4✔
1142
#endif /* !PR_USE_NLS */
1143
}
1144

1145
int pr_scoreboard_entry_update(pid_t pid, ...) {
1146
  va_list ap;
1147
  char *tmp = NULL;
1148
  int entry_tag = 0;
30✔
1149

30✔
1150
  if (scoreboard_engine == FALSE) {
30✔
1151
    return 0;
30✔
1152
  }
1153

30✔
1154
  if (scoreboard_fd < 0) {
1155
    errno = EINVAL;
1156
    return -1;
1157
  }
26✔
1158

7✔
1159
  if (!have_entry) {
7✔
1160
    errno = EPERM;
1161
    return -1;
1162
  }
19✔
1163

1✔
1164
  pr_trace_msg(trace_channel, 3, "updating scoreboard entry");
1✔
1165

1166
  va_start(ap, pid);
1167

18✔
1168
  while ((entry_tag = va_arg(ap, int)) != 0) {
1169
    pr_signals_handle();
18✔
1170

1171
    switch (entry_tag) {
35✔
1172
      case PR_SCORE_USER:
18✔
1173
        tmp = va_arg(ap, char *);
1174
        memset(entry.sce_user, '\0', sizeof(entry.sce_user));
18✔
1175
        sstrncpy(entry.sce_user, tmp,
1✔
1176
          str_getlen(tmp, sizeof(entry.sce_user)-1) + 1);
1✔
1177

1✔
1178
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry user to '%s'",
1✔
1179
          entry.sce_user);
1✔
1180
        break;
1181

1✔
1182
      case PR_SCORE_CLIENT_ADDR: {
1183
          pr_netaddr_t *remote_addr = va_arg(ap, pr_netaddr_t *);
1✔
1184

1185
          pr_snprintf(entry.sce_client_addr, sizeof(entry.sce_client_addr),
1✔
1186
            "%s", remote_addr ? pr_netaddr_get_ipstr(remote_addr) :
1✔
1187
            "(unknown)");
1188
          entry.sce_client_addr[sizeof(entry.sce_client_addr) - 1] = '\0';
2✔
1189

1✔
1190
          pr_trace_msg(trace_channel, 15, "updated scoreboard entry client "
1191
            "address to '%s'", entry.sce_client_addr);
1✔
1192
        }
1193
        break;
1✔
1194

1195
      case PR_SCORE_CLIENT_NAME: {
1196
          char *remote_name = va_arg(ap, char *);
1✔
1197

1198
          if (remote_name == NULL) {
1✔
1199
            remote_name = "(unknown)";
1✔
1200
          }
1201

1✔
1202
          memset(entry.sce_client_name, '\0', sizeof(entry.sce_client_name));
×
1203

1204
          snprintf(entry.sce_client_name,
1205
            str_getlen(remote_name, sizeof(entry.sce_client_name)-1) + 1,
1✔
1206
            "%s", remote_name);
1207
          entry.sce_client_name[sizeof(entry.sce_client_name)-1] = '\0';
1✔
1208

1✔
1209
          pr_trace_msg(trace_channel, 15, "updated scoreboard entry client "
1210
            "name to '%s'", entry.sce_client_name);
1✔
1211
        }
1212
        break;
1✔
1213

1214
      case PR_SCORE_CLASS:
1215
        tmp = va_arg(ap, char *);
1✔
1216
        memset(entry.sce_class, '\0', sizeof(entry.sce_class));
1217
        sstrncpy(entry.sce_class, tmp, sizeof(entry.sce_class));
1✔
1218

1✔
1219
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry class to "
1✔
1220
          "'%s'", entry.sce_class);
1✔
1221
        break;
1222

1✔
1223
      case PR_SCORE_CWD:
1224
        tmp = va_arg(ap, char *);
1✔
1225
        memset(entry.sce_cwd, '\0', sizeof(entry.sce_cwd));
1226
        sstrncpy(entry.sce_cwd, tmp,
1✔
1227
          str_getlen(tmp, sizeof(entry.sce_cwd)-1) + 1);
1✔
1228

1✔
1229
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry cwd to '%s'",
1✔
1230
          entry.sce_cwd);
1✔
1231
        break;
1232

1✔
1233
      case PR_SCORE_CMD: {
1234
        char *cmdstr = NULL;
1✔
1235
        tmp = va_arg(ap, char *);
1236
        cmdstr = handle_score_str(tmp, ap);
1✔
1237

1✔
1238
        memset(entry.sce_cmd, '\0', sizeof(entry.sce_cmd));
1✔
1239
        sstrncpy(entry.sce_cmd, cmdstr, sizeof(entry.sce_cmd));
1✔
1240
        (void) va_arg(ap, void *);
1241

1✔
1242
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry "
1✔
1243
          "command to '%s'", entry.sce_cmd);
1✔
1244
        break;
1245
      }
1✔
1246

1247
      case PR_SCORE_CMD_ARG: {
1✔
1248
        char *argstr = NULL;
1249
        tmp = va_arg(ap, char *);
1250
        argstr = handle_score_str(tmp, ap);
1✔
1251

1✔
1252
        memset(entry.sce_cmd_arg, '\0', sizeof(entry.sce_cmd_arg));
1✔
1253
        sstrncpy(entry.sce_cmd_arg, argstr,
1✔
1254
          str_getlen(argstr, sizeof(entry.sce_cmd_arg)-1) + 1);
1255
        (void) va_arg(ap, void *);
1✔
1256

1✔
1257
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry "
1✔
1258
          "command args to '%s'", entry.sce_cmd_arg);
1✔
1259
        break;
1260
      }
1✔
1261

1262
      case PR_SCORE_SERVER_PORT:
1✔
1263
        entry.sce_server_port = va_arg(ap, int);
1264
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry "
1265
          "server port to %d", entry.sce_server_port);
1✔
1266
        break;
1✔
1267

1✔
1268
      case PR_SCORE_SERVER_ADDR: {
1269
        pr_netaddr_t *server_addr = va_arg(ap, pr_netaddr_t *);
1✔
1270
        int server_port = va_arg(ap, int);
1271

1✔
1272
        pr_snprintf(entry.sce_server_addr, sizeof(entry.sce_server_addr),
1✔
1273
          "%s:%d", server_addr ? pr_netaddr_get_ipstr(server_addr) :
1✔
1274
          "(unknown)", server_port);
1275
        entry.sce_server_addr[sizeof(entry.sce_server_addr)-1] = '\0';
2✔
1276

1✔
1277
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry server "
1278
          "address to '%s'", entry.sce_server_addr);
1✔
1279
        break;
1280
      }
1✔
1281

1282
      case PR_SCORE_SERVER_LABEL:
1✔
1283
        tmp = va_arg(ap, char *);
1284
        memset(entry.sce_server_label, '\0', sizeof(entry.sce_server_label));
1285
        sstrncpy(entry.sce_server_label, tmp, sizeof(entry.sce_server_label));
1✔
1286

1✔
1287
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry server "
1✔
1288
          "label to '%s'", entry.sce_server_label);
1✔
1289
        break;
1290

1✔
1291
      case PR_SCORE_BEGIN_IDLE:
1292
        /* Ignore this */
1✔
1293
        (void) va_arg(ap, time_t);
1294

1✔
1295
        time(&entry.sce_begin_idle);
1296
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry idle "
1✔
1297
          "start time to %lu", (unsigned long) entry.sce_begin_idle);
1298
        break;
1✔
1299

1✔
1300
      case PR_SCORE_BEGIN_SESSION:
1✔
1301
        /* Ignore this */
1✔
1302
        (void) va_arg(ap, time_t);
1303

1✔
1304
        time(&entry.sce_begin_session);
1305
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry session "
1✔
1306
          "start time to %lu", (unsigned long) entry.sce_begin_session);
1307
        break;
1✔
1308

1✔
1309
      case PR_SCORE_XFER_DONE:
1✔
1310
        entry.sce_xfer_done = va_arg(ap, off_t);
1✔
1311
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry transfer "
1312
          "bytes done to %" PR_LU " bytes", (pr_off_t) entry.sce_xfer_done);
1✔
1313
        break;
1✔
1314

1✔
1315
      case PR_SCORE_XFER_SIZE:
1316
        entry.sce_xfer_size = va_arg(ap, off_t);
1✔
1317
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry transfer "
1318
          "size to %" PR_LU " bytes", (pr_off_t) entry.sce_xfer_size);
1✔
1319
        break;
1✔
1320

1✔
1321
      case PR_SCORE_XFER_LEN:
1322
        entry.sce_xfer_len = va_arg(ap, off_t);
1✔
1323
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry transfer "
1324
          "length to %" PR_LU " bytes", (pr_off_t) entry.sce_xfer_len);
1✔
1325
        break;
1✔
1326

1✔
1327
      case PR_SCORE_XFER_ELAPSED:
1328
        entry.sce_xfer_elapsed = va_arg(ap, unsigned long);
1✔
1329
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry transfer "
1330
          "elapsed to %lu ms", (unsigned long) entry.sce_xfer_elapsed);
1✔
1331
        break;
1✔
1332

1✔
1333
      case PR_SCORE_PROTOCOL:
1334
        tmp = va_arg(ap, char *);
1✔
1335
        memset(entry.sce_protocol, '\0', sizeof(entry.sce_protocol));
1336
        sstrncpy(entry.sce_protocol, tmp, sizeof(entry.sce_protocol));
1✔
1337
        pr_trace_msg(trace_channel, 15, "updated scoreboard entry protocol to "
1✔
1338
          "'%s'", entry.sce_protocol);
1✔
1339
        break;
1✔
1340

1✔
1341
      default:
1342
        va_end(ap);
1✔
1343
        errno = ENOENT;
1344
        return -1;
1✔
1345
    }
1✔
1346
  }
1✔
1347

1✔
1348
  va_end(ap);
1349

1350
  /* Write-lock this entry */
1351
  wlock_entry(scoreboard_fd);
17✔
1352
  if (write_entry(scoreboard_fd) < 0) {
1353
    pr_log_pri(PR_LOG_NOTICE, "error writing scoreboard entry: %s",
1354
      strerror(errno));
17✔
1355
  }
17✔
1356
  unlock_entry(scoreboard_fd);
×
1357

×
1358
  pr_trace_msg(trace_channel, 3, "finished updating scoreboard entry");
1359
  return 0;
17✔
1360
}
1361

17✔
1362
/* Validate the PID in a scoreboard entry.  A PID can be invalid in a couple
17✔
1363
 * of ways:
1364
 *
1365
 *  1.  The PID refers to a process no longer present on the system.
1366
 *  2.  The PID refers to a process not in the daemon process group
1367
 *      (for "ServerType standalone" servers only).
1368
 */
1369
static int scoreboard_valid_pid(pid_t pid, pid_t curr_pgrp) {
1370
  int res;
1371

1372
  res = kill(pid, 0);
×
1373
  if (res < 0 &&
×
1374
      errno == ESRCH) {
1375
    return -1;
×
1376
  }
×
1377

×
1378
  if (ServerType == SERVER_STANDALONE &&
1379
      curr_pgrp > 0) {
1380
#if defined(HAVE_GETPGID)
1381
    if (getpgid(pid) != curr_pgrp) {
×
1382
      pr_trace_msg(trace_channel, 1, "scoreboard entry PID %lu process group "
1383
        "does not match current process group, removing entry",
1384
        (unsigned long) pid);
×
1385
      errno = EPERM;
×
1386
      return -1;
1387
    }
1388
#endif /* HAVE_GETPGID */
×
1389
  }
×
1390

1391
  return 0;
1392
}
1393

1394
int pr_scoreboard_scrub(void) {
1395
  int fd = -1, res, xerrno;
1396
  off_t curr_offset = 0;
1397
  pid_t curr_pgrp = 0;
6✔
1398
  pr_scoreboard_entry_t sce;
6✔
1399

6✔
1400
  if (scoreboard_engine == FALSE) {
6✔
1401
    return 0;
6✔
1402
  }
1403

6✔
1404
  pr_log_debug(DEBUG9, "scrubbing scoreboard");
1405
  pr_trace_msg(trace_channel, 9, "%s", "scrubbing scoreboard");
1406

1407
  /* Manually open the scoreboard.  It won't hurt if the process already
2✔
1408
   * has a descriptor opened on the scoreboard file.
2✔
1409
   */
1410
  PRIVS_ROOT
1411
  fd = open(pr_get_scoreboard(), O_RDWR);
1412
  xerrno = errno;
1413
  PRIVS_RELINQUISH
2✔
1414

2✔
1415
  if (fd < 0) {
2✔
1416
    pr_log_debug(DEBUG1, "unable to scrub ScoreboardFile '%s': %s",
2✔
1417
      pr_get_scoreboard(), strerror(xerrno));
1418

2✔
1419
    errno = xerrno;
1✔
1420
    return -1;
1421
  }
1422

1✔
1423
  /* Write-lock the scoreboard file. */
1✔
1424
  PR_DEVEL_CLOCK(res = wlock_scoreboard());
1425
  if (res < 0) {
1426
    xerrno = errno;
1427

1✔
1428
    (void) close(fd);
1✔
1429

×
1430
    errno = xerrno;
1431
    return -1;
×
1432
  }
1433

×
1434
#if defined(HAVE_GETPGRP)
×
1435
  curr_pgrp = getpgrp();
1436
#elif HAVE_GETPGID
1437
  curr_pgrp = getpgid(0);
1438
#endif /* !HAVE_GETPGRP and !HAVE_GETPGID */
1✔
1439

1440
  /* Skip past the scoreboard header. */
1441
  curr_offset = lseek(fd, (off_t) sizeof(pr_scoreboard_header_t), SEEK_SET);
1442
  if (curr_offset < 0) {
1443
    xerrno = errno;
1444

1✔
1445
    unlock_scoreboard();
1✔
1446
    (void) close(fd);
×
1447

1448
    errno = xerrno;
×
1449
    return -1;
×
1450
  }
1451

×
1452
  entry_lock.l_start = curr_offset;
×
1453

1454
  PRIVS_ROOT
1455

1✔
1456
  while (TRUE) {
1457
    pr_signals_handle();
1✔
1458

1459
    /* First, lock the scoreboard entry/slot about to be checked.  If we can't
1✔
1460
     * (e.g. because the session process has it locked), then just move on.
1✔
1461
     * If another process has it locked, then it is presumed to be valid.
1462
     */
1463
    if (wlock_entry(fd) < 0) {
1464
      /* Seek to the next entry/slot.  If it fails for any reason, just
1465
       * be done with the scrubbing.
1466
       */
1✔
1467
      curr_offset = lseek(fd, sizeof(sce), SEEK_CUR);
1468
      entry_lock.l_start = curr_offset;
1469

1470
      if (curr_offset < 0) {
×
1471
        pr_trace_msg(trace_channel, 3,
×
1472
          "error seeking to next scoreboard entry (fd %d): %s", fd,
1473
          strerror(xerrno));
×
1474
        break;
×
1475
      }
1476

1477
      continue;
×
1478
    }
1479

1480
    memset(&sce, 0, sizeof(sce));
×
1481
    res = read(fd, &sce, sizeof(sce));
1482
    if (res == 0) {
1483
      /* EOF */
1✔
1484
      unlock_entry(fd);
1✔
1485
      break;
1✔
1486
    }
1487

1✔
1488
    if (res == sizeof(sce)) {
1489

1490
      /* Check to see if the PID in this entry is valid.  If not, erase
1491
       * the slot.
×
1492
       */
1493
      if (sce.sce_pid &&
1494
          scoreboard_valid_pid(sce.sce_pid, curr_pgrp) < 0) {
1495
        pid_t slot_pid;
1496

×
1497
        slot_pid = sce.sce_pid;
×
1498

×
1499
        /* OK, the recorded PID is no longer valid. */
1500
        pr_log_debug(DEBUG9, "scrubbing scoreboard entry for PID %lu",
×
1501
          (unsigned long) slot_pid);
1502

1503
        /* Rewind to the start of this slot. */
×
1504
        if (lseek(fd, curr_offset, SEEK_SET) < 0) {
1505
          xerrno = errno;
1506

1507
          pr_log_debug(DEBUG0, "error seeking to scoreboard entry to scrub: %s",
×
1508
            strerror(xerrno));
×
1509

1510
          pr_trace_msg(trace_channel, 3,
×
1511
            "error seeking to scoreboard entry for PID %lu (offset %" PR_LU ") "
1512
            "to scrub: %s", (unsigned long) slot_pid, (pr_off_t) curr_offset,
1513
            strerror(xerrno));
×
1514
        }
1515

1516
        memset(&sce, 0, sizeof(sce));
1517

1518
        /* Note: It does not matter that we only have a read-lock on this
1519
         * slot; we can safely write over the byte range here, since we know
×
1520
         * that the process for this slot is not around anymore, and there
1521
         * are no incoming processes to use take it.
1522
         */
1523

1524
        res = write(fd, &sce, sizeof(sce));
1525
        while (res != sizeof(sce)) {
1526
          if (res < 0) {
1527
            xerrno = errno;
×
1528

×
1529
            if (xerrno == EINTR) {
×
1530
              pr_signals_handle();
×
1531
              res = write(fd, &sce, sizeof(sce));
1532
              continue;
×
1533
            }
×
1534

×
1535
            pr_log_debug(DEBUG0, "error scrubbing scoreboard: %s",
×
1536
              strerror(xerrno));
1537
            pr_trace_msg(trace_channel, 3,
1538
              "error writing out scrubbed scoreboard entry for PID %lu: %s",
×
1539
              (unsigned long) slot_pid, strerror(xerrno));
1540

×
1541
          } else {
1542
            /* Watch out for short writes here. */
1543
            pr_log_pri(PR_LOG_NOTICE,
1544
              "error scrubbing scoreboard entry: only wrote %d of %lu bytes",
1545
              res, (unsigned long) sizeof(sce));
1546
          }
×
1547
        }
1548
      }
1549

1550
      /* Unlock the slot, and move to the next one. */
1551
      unlock_entry(fd);
1552

1553
      /* Mark the current offset. */
1554
      curr_offset = lseek(fd, (off_t) 0, SEEK_CUR);
×
1555
      if (curr_offset < 0) {
1556
        break;
1557
      }
×
1558

×
1559
      entry_lock.l_start = curr_offset;
1560
    }
1561
  }
1562

×
1563
  PRIVS_RELINQUISH
1564

1565
  /* Release the scoreboard. */
1566
  unlock_scoreboard();
1✔
1567

1568
  /* Don't need the descriptor anymore. */
1569
  (void) close(fd);
1✔
1570

1571
  pr_log_debug(DEBUG9, "finished scrubbing scoreboard");
1572
  pr_trace_msg(trace_channel, 9, "%s", "finished scrubbing scoreboard");
1✔
1573

1574
  return 0;
1✔
1575
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc