1 /* update-game-score.c --- Update a score file
3 Copyright (C) 2002-2018 Free Software Foundation, Inc.
5 Author: Colin Walters <walters@debian.org>
7 This file is part of GNU Emacs.
9 GNU Emacs is free software: you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation, either version 3 of the License, or (at
12 your option) any later version.
14 GNU Emacs is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
23 /* This program allows a game to securely and atomically update a
24 score file. It should be installed either setuid or setgid, owned
25 by an appropriate user or group like `games'.
27 Alternatively, it can be compiled without HAVE_SHARED_GAME_DIR
28 defined, and in that case it will store scores in the user's home
29 directory (it should NOT be setuid).
49 #include <unlocked-io.h>
56 # define min(a,b) ((a) < (b) ? (a) : (b))
59 #define MAX_ATTEMPTS 5
60 #define MAX_DATA_LEN 1024
65 fprintf (stdout
, "Usage: update-game-score [-m MAX] [-r] [-d DIR] game/scorefile SCORE DATA\n");
66 fprintf (stdout
, " update-game-score -h\n");
67 fprintf (stdout
, " -h\t\tDisplay this help.\n");
68 fprintf (stdout
, " -m MAX\t\tLimit the maximum number of scores to MAX.\n");
69 fprintf (stdout
, " -r\t\tSort the scores in increasing order.\n");
70 fprintf (stdout
, " -d DIR\t\tStore scores in DIR (only if not setuid).\n");
74 static int lock_file (const char *filename
, void **state
);
75 static int unlock_file (const char *filename
, void *state
);
83 #define MAX_SCORES min (PTRDIFF_MAX, SIZE_MAX / sizeof (struct score_entry))
85 static int read_scores (const char *filename
, struct score_entry
**scores
,
86 ptrdiff_t *count
, ptrdiff_t *alloc
);
87 static int push_score (struct score_entry
**scores
, ptrdiff_t *count
,
88 ptrdiff_t *size
, struct score_entry
const *newscore
);
89 static void sort_scores (struct score_entry
*scores
, ptrdiff_t count
,
91 static int write_scores (const char *filename
, mode_t mode
,
92 const struct score_entry
*scores
, ptrdiff_t count
);
95 lose (const char *msg
)
97 fprintf (stderr
, "%s\n", msg
);
101 static _Noreturn
void
102 lose_syserr (const char *msg
)
104 fprintf (stderr
, "%s: %s\n", msg
,
105 errno
? strerror (errno
) : "Invalid data in score file");
112 struct passwd
*buf
= getpwuid (getuid ());
113 if (!buf
|| strchr (buf
->pw_name
, ' ') || strchr (buf
->pw_name
, '\n'))
115 intmax_t uid
= getuid ();
116 char *name
= malloc (sizeof uid
* CHAR_BIT
/ 3 + 4);
118 sprintf (name
, "%"PRIdMAX
, uid
);
125 get_prefix (bool privileged
, const char *user_prefix
)
129 #ifdef HAVE_SHARED_GAME_DIR
130 return HAVE_SHARED_GAME_DIR
;
132 lose ("This program was compiled without HAVE_SHARED_GAME_DIR,\n"
133 "and should not run with elevated privileges.");
136 if (user_prefix
== NULL
)
137 lose ("Not using a shared game directory, and no prefix given.");
142 normalize_integer (char *num
)
146 while (*num
!= '\n' && isspace (*num
))
149 num
+= neg
|| *num
== '-';
153 while (*++num
== '0')
159 for (p
= num
; '0' <= *p
&& *p
<= '9'; p
++)
174 main (int argc
, char **argv
)
177 bool running_suid
, running_sgid
;
180 char *end
, *nl
, *user
, *data
;
181 const char *prefix
, *user_prefix
= NULL
;
182 struct score_entry
*scores
;
183 struct score_entry newscore
;
184 bool reverse
= false;
185 ptrdiff_t scorecount
, scorealloc
;
186 ptrdiff_t max_scores
= MAX_SCORES
;
190 while ((c
= getopt (argc
, argv
, "hrm:d:")) != -1)
194 usage (EXIT_SUCCESS
);
197 user_prefix
= optarg
;
204 intmax_t m
= strtoimax (optarg
, &end
, 10);
205 if (optarg
== end
|| *end
|| m
< 0)
206 usage (EXIT_FAILURE
);
207 max_scores
= min (m
, MAX_SCORES
);
211 usage (EXIT_FAILURE
);
214 if (argc
- optind
!= 3)
215 usage (EXIT_FAILURE
);
217 running_suid
= (getuid () != geteuid ());
218 running_sgid
= (getgid () != getegid ());
219 if (running_suid
&& running_sgid
)
220 lose ("This program can run either suid or sgid, but not both.");
222 prefix
= get_prefix (running_suid
|| running_sgid
, user_prefix
);
224 scorefile
= malloc (strlen (prefix
) + strlen (argv
[optind
]) + 2);
226 lose_syserr ("Couldn't allocate score file");
228 char *z
= stpcpy (scorefile
, prefix
);
230 strcpy (z
, argv
[optind
]);
232 newscore
.score
= normalize_integer (argv
[optind
+ 1]);
233 if (! newscore
.score
)
235 fprintf (stderr
, "%s: Invalid score\n", argv
[optind
+ 1]);
239 user
= get_user_id ();
241 lose_syserr ("Couldn't determine user id");
242 data
= argv
[optind
+ 2];
243 if (strlen (data
) > MAX_DATA_LEN
)
244 data
[MAX_DATA_LEN
] = '\0';
245 nl
= strchr (data
, '\n');
248 newscore
.user_data
= malloc (strlen (user
) + 1 + strlen (data
) + 1);
249 if (! newscore
.user_data
250 || sprintf (newscore
.user_data
, "%s %s", user
, data
) < 0)
251 lose_syserr ("Memory exhausted");
253 if (lock_file (scorefile
, &lockstate
) < 0)
254 lose_syserr ("Failed to lock scores file");
256 if (read_scores (scorefile
, &scores
, &scorecount
, &scorealloc
) < 0)
258 unlock_file (scorefile
, lockstate
);
259 lose_syserr ("Failed to read scores file");
261 if (push_score (&scores
, &scorecount
, &scorealloc
, &newscore
) < 0)
263 unlock_file (scorefile
, lockstate
);
264 lose_syserr ("Failed to add score");
266 sort_scores (scores
, scorecount
, reverse
);
267 /* Limit the number of scores. If we're using reverse sorting, then
268 also increment the beginning of the array, to skip over the
269 *smallest* scores. Otherwise, just decrementing the number of
270 scores suffices, since the smallest is at the end. */
271 if (scorecount
> max_scores
)
274 scores
+= scorecount
- max_scores
;
275 scorecount
= max_scores
;
277 if (write_scores (scorefile
, running_sgid
? 0664 : 0644,
278 scores
, scorecount
) < 0)
280 unlock_file (scorefile
, lockstate
);
281 lose_syserr ("Failed to write scores file");
283 if (unlock_file (scorefile
, lockstate
) < 0)
284 lose_syserr ("Failed to unlock scores file");
289 read_score (char *p
, struct score_entry
*score
)
296 score
->user_data
= p
;
297 p
= strchr (p
, '\n');
305 read_scores (const char *filename
, struct score_entry
**scores
,
306 ptrdiff_t *count
, ptrdiff_t *alloc
)
309 ptrdiff_t filesize
, nread
;
311 FILE *f
= fopen (filename
, "r");
314 if (fstat (fileno (f
), &st
) != 0)
316 if (! (0 <= st
.st_size
&& st
.st_size
< min (PTRDIFF_MAX
, SIZE_MAX
)))
321 filesize
= st
.st_size
;
322 filedata
= malloc (filesize
+ 1);
325 nread
= fread (filedata
, 1, filesize
+ 1, f
);
326 if (filesize
< nread
)
331 if (nread
< filesize
)
333 if (ferror (f
) || fclose (f
) != 0)
335 filedata
[filesize
] = 0;
336 if (strlen (filedata
) != filesize
)
344 for (p
= filedata
; p
< filedata
+ filesize
; )
346 struct score_entry entry
;
347 p
= read_score (p
, &entry
);
353 if (push_score (scores
, count
, alloc
, &entry
) < 0)
360 score_compare (const void *a
, const void *b
)
362 const struct score_entry
*sa
= (const struct score_entry
*) a
;
363 const struct score_entry
*sb
= (const struct score_entry
*) b
;
364 char *sca
= sa
->score
;
365 char *scb
= sb
->score
;
367 bool nega
= *sca
== '-';
368 bool negb
= *scb
== '-';
369 int diff
= nega
- negb
;
381 return lenb
< lena
? -1 : 1;
382 return strcmp (scb
, sca
);
386 score_compare_reverse (const void *a
, const void *b
)
388 return score_compare (b
, a
);
392 push_score (struct score_entry
**scores
, ptrdiff_t *count
, ptrdiff_t *size
,
393 struct score_entry
const *newscore
)
395 struct score_entry
*newscores
= *scores
;
398 ptrdiff_t newsize
= *size
;
401 else if (newsize
<= MAX_SCORES
/ 2)
403 else if (newsize
< MAX_SCORES
)
404 newsize
= MAX_SCORES
;
410 newscores
= realloc (newscores
, sizeof *newscores
* newsize
);
416 newscores
[*count
] = *newscore
;
422 sort_scores (struct score_entry
*scores
, ptrdiff_t count
, bool reverse
)
424 qsort (scores
, count
, sizeof *scores
,
425 reverse
? score_compare_reverse
: score_compare
);
429 write_scores (const char *filename
, mode_t mode
,
430 const struct score_entry
*scores
, ptrdiff_t count
)
435 char *tempfile
= malloc (strlen (filename
) + strlen (".tempXXXXXX") + 1);
438 strcpy (stpcpy (tempfile
, filename
), ".tempXXXXXX");
439 fd
= mkostemp (tempfile
, 0);
443 if (fchmod (fd
, mode
) != 0)
446 f
= fdopen (fd
, "w");
449 for (i
= 0; i
< count
; i
++)
450 if (fprintf (f
, "%s %s\n", scores
[i
].score
, scores
[i
].user_data
) < 0)
454 if (rename (tempfile
, filename
) != 0)
460 lock_file (const char *filename
, void **state
)
465 const char *lockext
= ".lockfile";
466 char *lockpath
= malloc (strlen (filename
) + strlen (lockext
) + 60);
469 strcpy (stpcpy (lockpath
, filename
), lockext
);
472 while ((fd
= open (lockpath
, O_CREAT
| O_EXCL
, 0600)) < 0)
478 /* Break the lock if it is over an hour old, or if we've tried
479 more than MAX_ATTEMPTS times. We won't corrupt the file, but
480 we might lose some scores. */
481 if (MAX_ATTEMPTS
< attempts
482 || (stat (lockpath
, &buf
) == 0 && 60 * 60 < time (0) - buf
.st_ctime
))
484 if (unlink (lockpath
) != 0 && errno
!= ENOENT
)
489 sleep ((rand () & 1) + 1);
497 unlock_file (const char *filename
, void *state
)
499 char *lockpath
= (char *) state
;
500 int saved_errno
= errno
;
501 int ret
= unlink (lockpath
);
502 int unlink_errno
= errno
;
504 errno
= ret
< 0 ? unlink_errno
: saved_errno
;
508 /* update-game-score.c ends here */