update-game-score.c 12.2 KB
Newer Older
Colin Walters's avatar
Colin Walters committed
1
/* update-game-score.c --- Update a score file
2

3
Copyright (C) 2002-2014 Free Software Foundation, Inc.
4 5

Author: Colin Walters <walters@debian.org>
Colin Walters's avatar
Colin Walters committed
6 7 8

This file is part of GNU Emacs.

9
GNU Emacs is free software: you can redistribute it and/or modify
Colin Walters's avatar
Colin Walters committed
10
it under the terms of the GNU General Public License as published by
11 12
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Colin Walters's avatar
Colin Walters committed
13 14 15 16 17 18 19

GNU Emacs is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
20 21
along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.  */

Colin Walters's avatar
Colin Walters committed
22

Juanma Barranquero's avatar
Juanma Barranquero committed
23
/* This program allows a game to securely and atomically update a
Colin Walters's avatar
Colin Walters committed
24 25 26 27 28 29
   score file.  It should be installed setuid, owned by an appropriate
   user like `games'.

   Alternatively, it can be compiled without HAVE_SHARED_GAME_DIR
   defined, and in that case it will store scores in the user's home
   directory (it should NOT be setuid).
Colin Walters's avatar
Colin Walters committed
30

31
   Created 2002/03/22.
Colin Walters's avatar
Colin Walters committed
32 33
*/

34 35
#include <config.h>

Colin Walters's avatar
Colin Walters committed
36 37
#include <unistd.h>
#include <errno.h>
38
#include <inttypes.h>
39
#include <limits.h>
40
#include <stdbool.h>
Colin Walters's avatar
Colin Walters committed
41 42 43 44
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
Colin Walters's avatar
Colin Walters committed
45
#include <pwd.h>
Colin Walters's avatar
Colin Walters committed
46 47 48
#include <ctype.h>
#include <fcntl.h>
#include <sys/stat.h>
49
#include <getopt.h>
50

51 52 53 54
#ifdef WINDOWSNT
#include "ntlib.h"
#endif

55 56 57 58
#ifndef min
# define min(a,b) ((a) < (b) ? (a) : (b))
#endif

Colin Walters's avatar
Colin Walters committed
59
#define MAX_ATTEMPTS 5
Colin Walters's avatar
Colin Walters committed
60
#define MAX_DATA_LEN 1024
61

62
static _Noreturn void
63
usage (int err)
Colin Walters's avatar
Colin Walters committed
64
{
65
  fprintf (stdout, "Usage: update-game-score [-m MAX] [-r] [-d DIR] game/scorefile SCORE DATA\n");
Richard M. Stallman's avatar
Richard M. Stallman committed
66 67 68 69 70 71
  fprintf (stdout, "       update-game-score -h\n");
  fprintf (stdout, " -h\t\tDisplay this help.\n");
  fprintf (stdout, " -m MAX\t\tLimit the maximum number of scores to MAX.\n");
  fprintf (stdout, " -r\t\tSort the scores in increasing order.\n");
  fprintf (stdout, " -d DIR\t\tStore scores in DIR (only if not setuid).\n");
  exit (err);
Colin Walters's avatar
Colin Walters committed
72 73
}

74 75
static int lock_file (const char *filename, void **state);
static int unlock_file (const char *filename, void *state);
Colin Walters's avatar
Colin Walters committed
76 77 78

struct score_entry
{
79
  intmax_t score;
Colin Walters's avatar
Colin Walters committed
80
  char *username;
Colin Walters's avatar
Colin Walters committed
81 82 83
  char *data;
};

84 85
#define MAX_SCORES min (PTRDIFF_MAX, SIZE_MAX / sizeof (struct score_entry))

86
static int read_scores (const char *filename, struct score_entry **scores,
87 88 89 90 91
			ptrdiff_t *count, ptrdiff_t *alloc);
static int push_score (struct score_entry **scores, ptrdiff_t *count,
		       ptrdiff_t *size, struct score_entry const *newscore);
static void sort_scores (struct score_entry *scores, ptrdiff_t count,
			 bool reverse);
92
static int write_scores (const char *filename,
93
			 const struct score_entry *scores, ptrdiff_t count);
94

95
static _Noreturn void
96
lose (const char *msg)
97
{
Richard M. Stallman's avatar
Richard M. Stallman committed
98
  fprintf (stderr, "%s\n", msg);
99
  exit (EXIT_FAILURE);
100
}
Colin Walters's avatar
Colin Walters committed
101

102
static _Noreturn void
103
lose_syserr (const char *msg)
Colin Walters's avatar
Colin Walters committed
104
{
Richard M. Stallman's avatar
Richard M. Stallman committed
105
  fprintf (stderr, "%s: %s\n", msg, strerror (errno));
106
  exit (EXIT_FAILURE);
Colin Walters's avatar
Colin Walters committed
107 108
}

109
static char *
110
get_user_id (void)
Colin Walters's avatar
Colin Walters committed
111
{
Richard M. Stallman's avatar
Richard M. Stallman committed
112
  struct passwd *buf = getpwuid (getuid ());
113
  if (!buf || strchr (buf->pw_name, ' ') || strchr (buf->pw_name, '\n'))
Colin Walters's avatar
Colin Walters committed
114
    {
115 116
      intmax_t uid = getuid ();
      char *name = malloc (sizeof uid * CHAR_BIT / 3 + 4);
117
      if (name)
118
	sprintf (name, "%"PRIdMAX, uid);
Colin Walters's avatar
Colin Walters committed
119 120 121 122 123
      return name;
    }
  return buf->pw_name;
}

124
static const char *
125
get_prefix (bool running_suid, const char *user_prefix)
Colin Walters's avatar
Colin Walters committed
126
{
Colin Walters's avatar
Colin Walters committed
127
  if (!running_suid && user_prefix == NULL)
Richard M. Stallman's avatar
Richard M. Stallman committed
128
    lose ("Not using a shared game directory, and no prefix given.");
Colin Walters's avatar
Colin Walters committed
129 130 131 132 133
  if (running_suid)
    {
#ifdef HAVE_SHARED_GAME_DIR
      return HAVE_SHARED_GAME_DIR;
#else
Richard M. Stallman's avatar
Richard M. Stallman committed
134
      lose ("This program was compiled without HAVE_SHARED_GAME_DIR,\n and should not be suid.");
Colin Walters's avatar
Colin Walters committed
135 136 137
#endif
    }
  return user_prefix;
Colin Walters's avatar
Colin Walters committed
138 139
}

Colin Walters's avatar
Colin Walters committed
140
int
141
main (int argc, char **argv)
Colin Walters's avatar
Colin Walters committed
142
{
143 144
  int c;
  bool running_suid;
Colin Walters's avatar
Colin Walters committed
145
  void *lockstate;
146 147
  char *scorefile;
  char *nl;
148
  const char *prefix, *user_prefix = NULL;
Colin Walters's avatar
Colin Walters committed
149 150
  struct stat buf;
  struct score_entry *scores;
151 152 153 154
  struct score_entry newscore;
  bool reverse = false;
  ptrdiff_t scorecount, scorealloc;
  ptrdiff_t max_scores = MAX_SCORES;
Colin Walters's avatar
Colin Walters committed
155

Richard M. Stallman's avatar
Richard M. Stallman committed
156
  srand (time (0));
Colin Walters's avatar
Colin Walters committed
157

Richard M. Stallman's avatar
Richard M. Stallman committed
158
  while ((c = getopt (argc, argv, "hrm:d:")) != -1)
Colin Walters's avatar
Colin Walters committed
159 160 161
    switch (c)
      {
      case 'h':
162
	usage (EXIT_SUCCESS);
Colin Walters's avatar
Colin Walters committed
163
	break;
Colin Walters's avatar
Colin Walters committed
164 165 166
      case 'd':
	user_prefix = optarg;
	break;
Colin Walters's avatar
Colin Walters committed
167 168 169 170
      case 'r':
	reverse = 1;
	break;
      case 'm':
171 172 173 174 175 176
	{
	  intmax_t m = strtoimax (optarg, 0, 10);
	  if (m < 0)
	    usage (EXIT_FAILURE);
	  max_scores = min (m, MAX_SCORES);
	}
Colin Walters's avatar
Colin Walters committed
177 178
	break;
      default:
179
	usage (EXIT_FAILURE);
Colin Walters's avatar
Colin Walters committed
180 181
      }

182
  if (argc - optind != 3)
183
    usage (EXIT_FAILURE);
184

Richard M. Stallman's avatar
Richard M. Stallman committed
185
  running_suid = (getuid () != geteuid ());
186

Richard M. Stallman's avatar
Richard M. Stallman committed
187
  prefix = get_prefix (running_suid, user_prefix);
Colin Walters's avatar
Colin Walters committed
188

Richard M. Stallman's avatar
Richard M. Stallman committed
189
  scorefile = malloc (strlen (prefix) + strlen (argv[optind]) + 2);
Colin Walters's avatar
Colin Walters committed
190
  if (!scorefile)
Richard M. Stallman's avatar
Richard M. Stallman committed
191
    lose_syserr ("Couldn't allocate score file");
Colin Walters's avatar
Colin Walters committed
192

Richard M. Stallman's avatar
Richard M. Stallman committed
193 194 195
  strcpy (scorefile, prefix);
  strcat (scorefile, "/");
  strcat (scorefile, argv[optind]);
Colin Walters's avatar
Colin Walters committed
196

197 198 199 200 201 202 203 204 205 206 207
  newscore.score = strtoimax (argv[optind + 1], 0, 10);

  newscore.data = argv[optind + 2];
  if (strlen (newscore.data) > MAX_DATA_LEN)
    newscore.data[MAX_DATA_LEN] = '\0';
  nl = strchr (newscore.data, '\n');
  if (nl)
    *nl = '\0';

  newscore.username = get_user_id ();
  if (! newscore.username)
Richard M. Stallman's avatar
Richard M. Stallman committed
208
    lose_syserr ("Couldn't determine user id");
209

Richard M. Stallman's avatar
Richard M. Stallman committed
210 211
  if (stat (scorefile, &buf) < 0)
    lose_syserr ("Failed to access scores file");
212

Richard M. Stallman's avatar
Richard M. Stallman committed
213 214
  if (lock_file (scorefile, &lockstate) < 0)
    lose_syserr ("Failed to lock scores file");
215

216
  if (read_scores (scorefile, &scores, &scorecount, &scorealloc) < 0)
Colin Walters's avatar
Colin Walters committed
217
    {
Richard M. Stallman's avatar
Richard M. Stallman committed
218 219
      unlock_file (scorefile, lockstate);
      lose_syserr ("Failed to read scores file");
Colin Walters's avatar
Colin Walters committed
220
    }
221 222 223 224 225
  if (push_score (&scores, &scorecount, &scorealloc, &newscore) < 0)
    {
      unlock_file (scorefile, lockstate);
      lose_syserr ("Failed to add score");
    }
226
  sort_scores (scores, scorecount, reverse);
Colin Walters's avatar
Colin Walters committed
227
  /* Limit the number of scores.  If we're using reverse sorting, then
228 229 230
     also increment the beginning of the array, to skip over the
     *smallest* scores.  Otherwise, just decrementing the number of
     scores suffices, since the smallest is at the end. */
231
  if (scorecount > max_scores)
232 233
    {
      if (reverse)
234 235
	scores += scorecount - max_scores;
      scorecount = max_scores;
236
    }
Richard M. Stallman's avatar
Richard M. Stallman committed
237
  if (write_scores (scorefile, scores, scorecount) < 0)
Colin Walters's avatar
Colin Walters committed
238
    {
Richard M. Stallman's avatar
Richard M. Stallman committed
239 240
      unlock_file (scorefile, lockstate);
      lose_syserr ("Failed to write scores file");
Colin Walters's avatar
Colin Walters committed
241
    }
242 243
  if (unlock_file (scorefile, lockstate) < 0)
    lose_syserr ("Failed to unlock scores file");
244
  exit (EXIT_SUCCESS);
Colin Walters's avatar
Colin Walters committed
245 246
}

247
static int
248
read_score (FILE *f, struct score_entry *score)
Colin Walters's avatar
Colin Walters committed
249 250
{
  int c;
251 252
  if ((c = getc (f)) != EOF)
    ungetc (c, f);
Richard M. Stallman's avatar
Richard M. Stallman committed
253
  if (feof (f))
Colin Walters's avatar
Colin Walters committed
254
    return 1;
255
  for (score->score = 0; (c = getc (f)) != EOF && isdigit (c); )
Colin Walters's avatar
Colin Walters committed
256
    {
257 258
      if (INTMAX_MAX / 10 < score->score)
	return -1;
Colin Walters's avatar
Colin Walters committed
259
      score->score *= 10;
260 261 262
      if (INTMAX_MAX - (c - '0') < score->score)
	return -1;
      score->score += c - '0';
Colin Walters's avatar
Colin Walters committed
263
    }
Richard M. Stallman's avatar
Richard M. Stallman committed
264 265
  while ((c = getc (f)) != EOF
	 && isspace (c))
Colin Walters's avatar
Colin Walters committed
266 267 268
    ;
  if (c == EOF)
    return -1;
Richard M. Stallman's avatar
Richard M. Stallman committed
269
  ungetc (c, f);
Colin Walters's avatar
Colin Walters committed
270 271
#ifdef HAVE_GETDELIM
  {
272
    size_t count = 0;
273
    score->username = 0;
Richard M. Stallman's avatar
Richard M. Stallman committed
274
    if (getdelim (&score->username, &count, ' ', f) < 1
Colin Walters's avatar
Colin Walters committed
275 276
	|| score->username == NULL)
      return -1;
277
    /* Trim the space */
Richard M. Stallman's avatar
Richard M. Stallman committed
278
    score->username[strlen (score->username)-1] = '\0';
Colin Walters's avatar
Colin Walters committed
279
  }
Colin Walters's avatar
Colin Walters committed
280 281
#else
  {
282 283
    ptrdiff_t unameread = 0;
    ptrdiff_t unamelen = 30;
Richard M. Stallman's avatar
Richard M. Stallman committed
284
    char *username = malloc (unamelen);
285 286
    if (!username)
      return -1;
287

288
    while ((c = getc (f)) != EOF && c != ' ')
Colin Walters's avatar
Colin Walters committed
289
      {
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
	if (unameread >= unamelen - 1)
	  {
	    ptrdiff_t unamelen_max = min (PTRDIFF_MAX, SIZE_MAX);
	    if (unamelen <= unamelen_max / 2)
	      unamelen *= 2;
	    else if (unamelen < unamelen_max)
	      unamelen = unamelen_max;
	    else
	      {
		errno = ENOMEM;
		return -1;
	      }
	    username = realloc (username, unamelen);
	    if (!username)
	      return -1;
	  }
Colin Walters's avatar
Colin Walters committed
306 307 308
	username[unameread] = c;
	unameread++;
      }
309
    if (c == EOF)
310 311
      return -1;
    username[unameread] = '\0';
Colin Walters's avatar
Colin Walters committed
312 313 314
    score->username = username;
  }
#endif
Colin Walters's avatar
Colin Walters committed
315 316
#ifdef HAVE_GETLINE
  score->data = NULL;
317
  errno = 0;
Colin Walters's avatar
Colin Walters committed
318
  {
319
    size_t len;
Richard M. Stallman's avatar
Richard M. Stallman committed
320
    if (getline (&score->data, &len, f) < 0)
Colin Walters's avatar
Colin Walters committed
321
      return -1;
Richard M. Stallman's avatar
Richard M. Stallman committed
322
    score->data[strlen (score->data)-1] = '\0';
Colin Walters's avatar
Colin Walters committed
323 324
  }
#else
Colin Walters's avatar
Colin Walters committed
325
  {
326 327
    ptrdiff_t cur = 0;
    ptrdiff_t len = 16;
Richard M. Stallman's avatar
Richard M. Stallman committed
328
    char *buf = malloc (len);
Colin Walters's avatar
Colin Walters committed
329 330
    if (!buf)
      return -1;
Richard M. Stallman's avatar
Richard M. Stallman committed
331
    while ((c = getc (f)) != EOF
332
	   && c != '\n')
Colin Walters's avatar
Colin Walters committed
333 334 335
      {
	if (cur >= len-1)
	  {
336 337 338 339 340
	    if (min (PTRDIFF_MAX, SIZE_MAX) / 2 < len)
	      {
		errno = ENOMEM;
		return -1;
	      }
Richard M. Stallman's avatar
Richard M. Stallman committed
341
	    if (!(buf = realloc (buf, len *= 2)))
Colin Walters's avatar
Colin Walters committed
342 343 344 345 346 347
	      return -1;
	  }
	buf[cur] = c;
	cur++;
      }
    score->data = buf;
348
    score->data[cur] = '\0';
Colin Walters's avatar
Colin Walters committed
349
  }
Colin Walters's avatar
Colin Walters committed
350 351 352 353
#endif
  return 0;
}

354
static int
355 356
read_scores (const char *filename, struct score_entry **scores,
	     ptrdiff_t *count, ptrdiff_t *alloc)
Colin Walters's avatar
Colin Walters committed
357
{
358 359 360 361 362
  int readval = -1;
  ptrdiff_t scorecount = 0;
  ptrdiff_t cursize = 0;
  struct score_entry *ret = 0;
  struct score_entry entry;
Richard M. Stallman's avatar
Richard M. Stallman committed
363
  FILE *f = fopen (filename, "r");
364
  int retval = -1;
365
  if (!f)
Colin Walters's avatar
Colin Walters committed
366
    return -1;
367 368 369
  while ((readval = read_score (f, &entry)) == 0)
    if (push_score (&ret, &scorecount, &cursize, &entry) < 0)
      return -1;
370
  if (readval > 0 && fclose (f) == 0)
371 372
    {
      *count = scorecount;
373
      *alloc = cursize;
374 375 376 377
      *scores = ret;
      retval = 0;
    }
  return retval;
Colin Walters's avatar
Colin Walters committed
378 379
}

380
static int
381
score_compare (const void *a, const void *b)
Colin Walters's avatar
Colin Walters committed
382 383 384 385 386 387
{
  const struct score_entry *sa = (const struct score_entry *) a;
  const struct score_entry *sb = (const struct score_entry *) b;
  return (sb->score > sa->score) - (sb->score < sa->score);
}

388
static int
389
score_compare_reverse (const void *a, const void *b)
Colin Walters's avatar
Colin Walters committed
390
{
391
  return score_compare (b, a);
Colin Walters's avatar
Colin Walters committed
392 393 394
}

int
395 396
push_score (struct score_entry **scores, ptrdiff_t *count, ptrdiff_t *size,
	    struct score_entry const *newscore)
Colin Walters's avatar
Colin Walters committed
397
{
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
  struct score_entry *newscores = *scores;
  if (*count == *size)
    {
      ptrdiff_t newsize = *size;
      if (newsize <= 0)
	newsize = 1;
      else if (newsize <= MAX_SCORES / 2)
	newsize *= 2;
      else if (newsize < MAX_SCORES)
	newsize = MAX_SCORES;
      else
	{
	  errno = ENOMEM;
	  return -1;
	}
      newscores = realloc (newscores, sizeof *newscores * newsize);
      if (!newscores)
	return -1;
      *scores = newscores;
      *size = newsize;
    }
  newscores[*count] = *newscore;
Colin Walters's avatar
Colin Walters committed
420 421 422
  (*count) += 1;
  return 0;
}
423

424
static void
425
sort_scores (struct score_entry *scores, ptrdiff_t count, bool reverse)
Colin Walters's avatar
Colin Walters committed
426
{
427 428
  qsort (scores, count, sizeof *scores,
	 reverse ? score_compare_reverse : score_compare);
Colin Walters's avatar
Colin Walters committed
429 430
}

431
static int
432 433
write_scores (const char *filename, const struct score_entry *scores,
	      ptrdiff_t count)
Colin Walters's avatar
Colin Walters committed
434
{
435
  int fd;
436
  FILE *f;
437
  ptrdiff_t i;
Richard M. Stallman's avatar
Richard M. Stallman committed
438
  char *tempfile = malloc (strlen (filename) + strlen (".tempXXXXXX") + 1);
Colin Walters's avatar
Colin Walters committed
439 440
  if (!tempfile)
    return -1;
Richard M. Stallman's avatar
Richard M. Stallman committed
441 442
  strcpy (tempfile, filename);
  strcat (tempfile, ".tempXXXXXX");
443 444 445
  fd = mkostemp (tempfile, 0);
  if (fd < 0)
    return -1;
446 447
  if (fchmod (fd, 0644) != 0)
    return -1;
448 449
  f = fdopen (fd, "w");
  if (! f)
Colin Walters's avatar
Colin Walters committed
450 451
    return -1;
  for (i = 0; i < count; i++)
452 453 454
    if (fprintf (f, "%"PRIdMAX" %s %s\n",
		 scores[i].score, scores[i].username, scores[i].data)
	< 0)
Colin Walters's avatar
Colin Walters committed
455
      return -1;
456
  if (fclose (f) != 0)
Colin Walters's avatar
Colin Walters committed
457
    return -1;
458
  if (rename (tempfile, filename) != 0)
Colin Walters's avatar
Colin Walters committed
459
    return -1;
Colin Walters's avatar
Colin Walters committed
460
  return 0;
Colin Walters's avatar
Colin Walters committed
461
}
462

463
static int
464
lock_file (const char *filename, void **state)
Colin Walters's avatar
Colin Walters committed
465 466
{
  int fd;
467
  struct stat buf;
Colin Walters's avatar
Colin Walters committed
468
  int attempts = 0;
469
  const char *lockext = ".lockfile";
Richard M. Stallman's avatar
Richard M. Stallman committed
470
  char *lockpath = malloc (strlen (filename) + strlen (lockext) + 60);
Colin Walters's avatar
Colin Walters committed
471 472
  if (!lockpath)
    return -1;
Richard M. Stallman's avatar
Richard M. Stallman committed
473 474
  strcpy (lockpath, filename);
  strcat (lockpath, lockext);
Colin Walters's avatar
Colin Walters committed
475 476 477
  *state = lockpath;
 trylock:
  attempts++;
478
  /* If the lock is over an hour old, delete it.  */
Richard M. Stallman's avatar
Richard M. Stallman committed
479
  if (stat (lockpath, &buf) == 0
480
      && 60 * 60 < time (0) - buf.st_ctime)
Richard M. Stallman's avatar
Richard M. Stallman committed
481
    unlink (lockpath);
482 483
  fd = open (lockpath, O_CREAT | O_EXCL, 0600);
  if (fd < 0)
Colin Walters's avatar
Colin Walters committed
484 485 486 487 488 489
    {
      if (errno == EEXIST)
	{
	  /* Break the lock; we won't corrupt the file, but we might
	     lose some scores. */
	  if (attempts > MAX_ATTEMPTS)
490
	    {
Richard M. Stallman's avatar
Richard M. Stallman committed
491
	      unlink (lockpath);
492 493
	      attempts = 0;
	    }
Richard M. Stallman's avatar
Richard M. Stallman committed
494
	  sleep ((rand () % 2)+1);
Colin Walters's avatar
Colin Walters committed
495 496 497 498 499
	  goto trylock;
	}
      else
	return -1;
    }
Richard M. Stallman's avatar
Richard M. Stallman committed
500
  close (fd);
Colin Walters's avatar
Colin Walters committed
501 502
  return 0;
}
503

504
static int
505
unlock_file (const char *filename, void *state)
Colin Walters's avatar
Colin Walters committed
506 507 508
{
  char *lockpath = (char *) state;
  int saved_errno = errno;
509 510
  int ret = unlink (lockpath);
  int unlink_errno = errno;
Richard M. Stallman's avatar
Richard M. Stallman committed
511
  free (lockpath);
512
  errno = ret < 0 ? unlink_errno : saved_errno;
Colin Walters's avatar
Colin Walters committed
513 514
  return ret;
}
Miles Bader's avatar
Miles Bader committed
515

516
/* update-game-score.c ends here */