From: Greg Hudson Date: Tue, 14 May 2019 16:13:35 +0000 (-0400) Subject: Add file2 rcache type X-Git-Tag: krb5-1.18-beta1~116 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=12117dbc61639ff3fb510f2feb2de8c41dd2bd23;p=thirdparty%2Fkrb5.git Add file2 rcache type Add a new replay cache type using a hash-based file format. ticket: 8786 --- diff --git a/.gitignore b/.gitignore index 140f0f89c8..bc15f78dc0 100644 --- a/.gitignore +++ b/.gitignore @@ -388,6 +388,8 @@ local.properties /src/lib/krb5/os/t_trace /src/lib/krb5/rcache/t_memrcache +/src/lib/krb5/rcache/t_rcfile2 +/src/lib/krb5/rcache/testrcache /src/lib/krb5/unicode/.links /src/lib/krb5/unicode/ucdata.[ch] diff --git a/doc/basic/rcache_def.rst b/doc/basic/rcache_def.rst index 2de953354e..56d369d836 100644 --- a/doc/basic/rcache_def.rst +++ b/doc/basic/rcache_def.rst @@ -9,7 +9,7 @@ request is detected in the replay cache, an error message is sent to the application program. The replay cache interface, like the credential cache and -:ref:`keytab_definition` interfaces, uses `type:value` strings to +:ref:`keytab_definition` interfaces, uses `type:residual` strings to indicate the type of replay cache and any associated cache naming data to use. @@ -57,17 +57,27 @@ additional messages), or if the simple act of presenting the authenticator triggers some interesting action in the service being attacked. -Default rcache type -------------------- +Replay cache types +------------------ + +Unlike the credential cache and keytab interfaces, replay cache types +are in lowercase. The following types are defined: + +#. **none** disables the replay cache. The residual value is ignored. + +#. **file2** (new in release 1.18) uses a hash-based format to store + replay records. The file may grow to accomodate hash collisions. + The residual value is the filename. -There is currently only one implemented kind of replay cache, called -**dfl**. It stores replay data in one file, occasionally rewriting it -to purge old, expired entries. +#. **dfl** is the default type if no environment variable or + configuration specifies a different type. It stores replay data in + a file, occasionally rewriting it to purge old, expired entries. The default type can be overridden by the **KRB5RCACHETYPE** environment variable. -The placement of the replay cache file is determined by the following: +For the dfl type, the placement of the replay cache file is determined +by the following: #. The **KRB5RCACHEDIR** environment variable; diff --git a/doc/formats/index.rst b/doc/formats/index.rst index 4ad5344245..47dea12fcf 100644 --- a/doc/formats/index.rst +++ b/doc/formats/index.rst @@ -6,5 +6,6 @@ Protocols and file formats ccache_file_format keytab_file_format + rcache_file_format cookie freshness_token diff --git a/doc/formats/rcache_file_format.rst b/doc/formats/rcache_file_format.rst new file mode 100644 index 0000000000..42ee82817a --- /dev/null +++ b/doc/formats/rcache_file_format.rst @@ -0,0 +1,50 @@ +Replay cache file format +======================== + +This section documents the second version of the replay cache file +format, used by the "file2" replay cache type (new in release 1.18). +The first version of the file replay cache format is not documented. + +All accesses to the replay cache file take place under an exclusive +POSIX or Windows file lock, obtained when the file is opened and +released when it is closed. Replay cache files are automatically +created when first accessed. + +For each store operation, a tag is derived from the checksum part of +the :RFC:`3961` ciphertext of the authenticator. The checksum is +coerced to a fixed length of 12 bytes, either through truncation or +right-padding with zero bytes. A four-byte timestamp is appended to +the tag to produce a total record length of 16 bytes. + +Bytes 0 through 15 of the file contain a hash seed for the SipHash-2-4 +algorithm (siphash_); this field is populated with random bytes when +the file is first created. All remaining bytes are divided into a +series of expanding hash tables: + +* Bytes 16-16383: hash table 1 (1023 slots) +* Bytes 16384-49151: hash table 2 (2048 slots) +* Bytes 49152-114687: hash table 3 (4096 slots) +* ... + +Only some hash tables will be present in the file at any specific +time, and the final table may be only partially filled. Replay cache +files may be sparse if the filesystem supports it. + +For each table present in the file, the tag is hashed with SipHash-2-4 +using the seed recorded in the file. The first byte of the seed is +incremented by one (modulo 256) for each table after the first. The +resulting hash value is taken modulo one less than the table size +(1022 for the first hash table, 2047 for the second) to produce the +index. The record may be found at the slot given by the index or at +the next slot. + +All candidate locations for the record must be searched until a slot +is found with a timestamp of zero (indicating a slot which has never +been written to) or an offset is reached at or beyond the end of the +file. Any candidate location with a timestamp value of zero, with a +timestamp value less than the current time minus clockskew, or at or +beyond the end of the file is available for writing. When all +candidate locations have been searched without finding a match, the +new entry is written to the earliest candidate available for writing. + +.. _siphash: https://131002.net/siphash/siphash.pdf diff --git a/src/lib/krb5/rcache/Makefile.in b/src/lib/krb5/rcache/Makefile.in index e61b657280..0513937b23 100644 --- a/src/lib/krb5/rcache/Makefile.in +++ b/src/lib/krb5/rcache/Makefile.in @@ -9,6 +9,7 @@ STLIBOBJS = \ memrcache.o \ rc_base.o \ rc_dfl.o \ + rc_file2.o \ rc_io.o \ rcdef.o \ rc_none.o \ @@ -20,6 +21,7 @@ OBJS= \ $(OUTPRE)memrcache.$(OBJEXT) \ $(OUTPRE)rc_base.$(OBJEXT) \ $(OUTPRE)rc_dfl.$(OBJEXT) \ + $(OUTPRE)rc_file2.$(OBJEXT) \ $(OUTPRE)rc_io.$(OBJEXT) \ $(OUTPRE)rcdef.$(OBJEXT) \ $(OUTPRE)rc_none.$(OBJEXT) \ @@ -31,6 +33,7 @@ SRCS= \ $(srcdir)/memrcache.c \ $(srcdir)/rc_base.c \ $(srcdir)/rc_dfl.c \ + $(srcdir)/rc_file2.c \ $(srcdir)/rc_io.c \ $(srcdir)/rcdef.c \ $(srcdir)/rc_none.c \ @@ -48,16 +51,22 @@ clean-unix:: clean-libobjs t_memrcache: t_memrcache.o $(KRB5_BASE_DEPLIBS) $(CC_LINK) -o $@ t_memrcache.o $(KRB5_BASE_LIBS) +t_rcfile2: t_rcfile2.o $(KRB5_BASE_DEPLIBS) + $(CC_LINK) -o $@ t_rcfile2.o $(KRB5_BASE_LIBS) + T_REPLAY_OBJS= t_replay.o t_replay: $(T_REPLAY_OBJS) $(KRB5_BASE_DEPLIBS) $(CC_LINK) -o t_replay $(T_REPLAY_OBJS) $(KRB5_BASE_LIBS) -check-unix: t_memrcache +check-unix: t_memrcache t_rcfile2 $(RUN_TEST) ./t_memrcache + $(RUN_TEST) ./t_rcfile2 testrcache expiry 10000 + $(RUN_TEST) ./t_rcfile2 testrcache concurrent 10 1000 + $(RUN_TEST) ./t_rcfile2 testrcache race 10 100 clean-unix:: - $(RM) t_memrcache.o t_memrcache + $(RM) t_memrcache.o t_memrcache t_rcfile2.o t_rcfile2 testrcache @libobj_frag@ diff --git a/src/lib/krb5/rcache/rc-int.h b/src/lib/krb5/rcache/rc-int.h index 72a9483c02..599b736180 100644 --- a/src/lib/krb5/rcache/rc-int.h +++ b/src/lib/krb5/rcache/rc-int.h @@ -86,6 +86,12 @@ typedef struct _krb5_rc_ops krb5_rc_ops; krb5_error_code krb5_rc_register_type(krb5_context, const krb5_rc_ops *); extern const krb5_rc_ops krb5_rc_dfl_ops; +extern const krb5_rc_ops krb5_rc_file2_ops; extern const krb5_rc_ops krb5_rc_none_ops; +/* Check and store a replay record in an open (but not locked) file descriptor, + * using the file2 format. fd is assumed to be at offset 0. */ +krb5_error_code k5_rcfile2_store(krb5_context context, int fd, + krb5_donot_replay *rep); + #endif /* __KRB5_RCACHE_INT_H__ */ diff --git a/src/lib/krb5/rcache/rc_base.c b/src/lib/krb5/rcache/rc_base.c index 9fa46432d9..c5f1d2342c 100644 --- a/src/lib/krb5/rcache/rc_base.c +++ b/src/lib/krb5/rcache/rc_base.c @@ -19,7 +19,8 @@ struct krb5_rc_typelist { struct krb5_rc_typelist *next; }; static struct krb5_rc_typelist none = { &krb5_rc_none_ops, 0 }; -static struct krb5_rc_typelist krb5_rc_typelist_dfl = { &krb5_rc_dfl_ops, &none }; +static struct krb5_rc_typelist file2 = { &krb5_rc_file2_ops, &none }; +static struct krb5_rc_typelist krb5_rc_typelist_dfl = { &krb5_rc_dfl_ops, &file2 }; static struct krb5_rc_typelist *typehead = &krb5_rc_typelist_dfl; static k5_mutex_t rc_typelist_lock = K5_MUTEX_PARTIAL_INITIALIZER; diff --git a/src/lib/krb5/rcache/rc_file2.c b/src/lib/krb5/rcache/rc_file2.c new file mode 100644 index 0000000000..e34c43a3c1 --- /dev/null +++ b/src/lib/krb5/rcache/rc_file2.c @@ -0,0 +1,306 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* lib/krb5/rcache/rc_file2.c - file-based replay cache, version 2 */ +/* + * Copyright (C) 2019 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "k5-int.h" +#include "k5-hashtab.h" +#include "rc-int.h" +#ifndef _WIN32 +#include +#include +#endif + +#define MAX_SIZE INT32_MAX +#define TAG_LEN 12 +#define RECORD_LEN (TAG_LEN + 4) +#define FIRST_TABLE_RECORDS 1023 + +/* Return the offset and number of records in the next table. *offset should + * initially be -1. */ +static inline krb5_error_code +next_table(off_t *offset, off_t *nrecords) +{ + if (*offset == -1) { + *offset = K5_HASH_SEED_LEN; + *nrecords = FIRST_TABLE_RECORDS; + } else if (*offset == K5_HASH_SEED_LEN) { + *offset += *nrecords * RECORD_LEN; + *nrecords = (FIRST_TABLE_RECORDS + 1) * 2; + } else { + *offset += *nrecords * RECORD_LEN; + *nrecords *= 2; + } + + /* Make sure the next table fits within the maximum file size. */ + if (*nrecords > MAX_SIZE / RECORD_LEN) + return EOVERFLOW; + if (*offset > MAX_SIZE - (*nrecords * RECORD_LEN)) + return EOVERFLOW; + + return 0; +} + +/* Read up to two records from fd at offset, and parse them out into tags and + * timestamps. Place the number of records read in *nread. */ +static krb5_error_code +read_records(int fd, off_t offset, uint8_t tag1_out[TAG_LEN], + uint32_t *timestamp1_out, uint8_t tag2_out[TAG_LEN], + uint32_t *timestamp2_out, int *nread) +{ + uint8_t buf[RECORD_LEN * 2]; + ssize_t st; + + *nread = 0; + + st = lseek(fd, offset, SEEK_SET); + if (st == -1) + return errno; + st = read(fd, buf, RECORD_LEN * 2); + if (st == -1) + return errno; + + if (st >= RECORD_LEN) { + memcpy(tag1_out, buf, TAG_LEN); + *timestamp1_out = load_32_be(buf + TAG_LEN); + *nread = 1; + } + if (st == RECORD_LEN * 2) { + memcpy(tag2_out, buf + RECORD_LEN, TAG_LEN); + *timestamp2_out = load_32_be(buf + RECORD_LEN + TAG_LEN); + *nread = 2; + } + return 0; +} + +/* Write one record to fd at offset, marshalling the tag and timestamp. */ +static krb5_error_code +write_record(int fd, off_t offset, const uint8_t tag[TAG_LEN], + uint32_t timestamp) +{ + uint8_t record[RECORD_LEN]; + ssize_t st; + + memcpy(record, tag, TAG_LEN); + store_32_be(timestamp, record + TAG_LEN); + + st = lseek(fd, offset, SEEK_SET); + if (st == -1) + return errno; + st = write(fd, record, RECORD_LEN); + if (st == -1) + return errno; + if (st != RECORD_LEN) /* Unexpected for a regular file */ + return EIO; + + return 0; +} + +/* Check and store a record into an open and locked file. fd is assumed to be + * at offset 0. */ +static krb5_error_code +store(krb5_context context, int fd, const uint8_t tag[TAG_LEN], uint32_t now, + uint32_t skew) +{ + krb5_error_code ret; + krb5_data d; + off_t table_offset = -1, nrecords = 0, avail_offset = -1, record_offset; + ssize_t st; + int ind, nread; + uint8_t seed[K5_HASH_SEED_LEN], rec1_tag[TAG_LEN], rec2_tag[TAG_LEN]; + uint32_t rec1_stamp, rec2_stamp; + + /* Read or generate the hash seed. */ + st = read(fd, seed, sizeof(seed)); + if (st < 0) + return errno; + if ((size_t)st < sizeof(seed)) { + d = make_data(seed, sizeof(seed)); + ret = krb5_c_random_make_octets(context, &d); + if (ret) + return ret; + st = write(fd, seed, sizeof(seed)); + if (st < 0) + return errno; + if ((size_t)st != sizeof(seed)) + return EIO; + } + + for (;;) { + ret = next_table(&table_offset, &nrecords); + if (ret) + return ret; + + ind = k5_siphash24(tag, TAG_LEN, seed) % nrecords; + record_offset = table_offset + ind * RECORD_LEN; + + ret = read_records(fd, record_offset, rec1_tag, &rec1_stamp, rec2_tag, + &rec2_stamp, &nread); + if (ret) + return ret; + + if ((nread >= 1 && memcmp(rec1_tag, tag, TAG_LEN) == 0) || + (nread == 2 && memcmp(rec2_tag, tag, TAG_LEN) == 0)) + return KRB5KRB_AP_ERR_REPEAT; + + if (avail_offset == -1) { + if (nread == 0 || ts_after(now, ts_incr(rec1_stamp, skew))) + avail_offset = record_offset; + else if (nread == 1 || ts_after(now, ts_incr(rec2_stamp, skew))) + avail_offset = record_offset + RECORD_LEN; + } + + if (nread < 2 || rec1_stamp == 0 || rec2_stamp == 0) + return write_record(fd, avail_offset, tag, now); + + /* Use a different hash seed for the next table we search. */ + seed[0]++; + } +} + +krb5_error_code +k5_rcfile2_store(krb5_context context, int fd, krb5_donot_replay *rep) +{ + krb5_error_code ret; + krb5_timestamp now; + uint8_t tag[TAG_LEN]; + + if (rep->tag.length == 0) + return EINVAL; + + ret = krb5_timeofday(context, &now); + if (ret) + return ret; + + if (rep->tag.length >= TAG_LEN) { + memcpy(tag, rep->tag.data, TAG_LEN); + } else { + memcpy(tag, rep->tag.data, rep->tag.length); + memset(tag + rep->tag.length, 0, TAG_LEN - rep->tag.length); + } + + ret = krb5_lock_file(context, fd, KRB5_LOCKMODE_EXCLUSIVE); + if (ret) + return ret; + ret = store(context, fd, tag, now, context->clockskew); + (void)krb5_unlock_file(NULL, fd); + return ret; +} + +static char * KRB5_CALLCONV +file2_get_name(krb5_context context, krb5_rcache rc) +{ + return (char *)rc->data; +} + +static krb5_error_code KRB5_CALLCONV +file2_get_span(krb5_context context, krb5_rcache rc, krb5_deltat *lifespan) +{ + *lifespan = context->clockskew; + return 0; +} + +static krb5_error_code KRB5_CALLCONV +file2_init(krb5_context context, krb5_rcache rc, krb5_deltat lifespan) +{ + return 0; +} + +static krb5_error_code KRB5_CALLCONV +file2_close(krb5_context context, krb5_rcache rc) +{ + k5_mutex_destroy(&rc->lock); + free(rc->data); + free(rc); + return 0; +} + +#define file2_destroy file2_close + +static krb5_error_code KRB5_CALLCONV +file2_resolve(krb5_context context, krb5_rcache rc, char *name) +{ + rc->data = strdup(name); + return (rc->data == NULL) ? ENOMEM : 0; +} + +static krb5_error_code KRB5_CALLCONV +file2_recover(krb5_context context, krb5_rcache rc) +{ + return 0; +} + +static krb5_error_code KRB5_CALLCONV +file2_recover_or_init(krb5_context context, krb5_rcache rc, + krb5_deltat lifespan) +{ + return 0; +} + +static krb5_error_code KRB5_CALLCONV +file2_store(krb5_context context, krb5_rcache rc, krb5_donot_replay *rep) +{ + krb5_error_code ret; + const char *filename = rc->data; + int fd; + + fd = open(filename, O_CREAT | O_RDWR | O_BINARY, 0600); + if (fd < 0) { + ret = errno; + k5_setmsg(context, ret, "%s (filename: %s)", error_message(ret), + filename); + return ret; + } + ret = k5_rcfile2_store(context, fd, rep); + close(fd); + return ret; +} + +static krb5_error_code KRB5_CALLCONV +file2_expunge(krb5_context context, krb5_rcache rc) +{ + return 0; +} + +const krb5_rc_ops krb5_rc_file2_ops = +{ + 0, + "file2", + file2_init, + file2_recover, + file2_recover_or_init, + file2_destroy, + file2_close, + file2_store, + file2_expunge, + file2_get_span, + file2_get_name, + file2_resolve +}; diff --git a/src/lib/krb5/rcache/t_rcfile2.c b/src/lib/krb5/rcache/t_rcfile2.c new file mode 100644 index 0000000000..cc32719f17 --- /dev/null +++ b/src/lib/krb5/rcache/t_rcfile2.c @@ -0,0 +1,212 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* lib/krb5/rcache/t_rcfile2.c - rcache file version 2 tests */ +/* + * Copyright (C) 2019 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Usage: + * + * t_rcfile2 expiry + * store records spaced far enough apart that all records appear + * expired; verify that the file size doesn't increase beyond one table. + * + * t_rcfile2 concurrent + * spawn subprocesses, each of which stores unique + * tags. As each process completes, the master process tests that the + * records stored by the subprocess appears as replays. + * + * t_rcfile2 race + * spawn subprocesses, each of which tries to store the same + * tag and reports success or failure. The master process verifies that + * exactly one subprocess succeeds. Repeat times. + */ + +#include "rc_file2.c" +#include +#include + +krb5_context ctx; + +static krb5_error_code +test_store(krb5_rcache rc, uint8_t *tag, krb5_timestamp timestamp, + const uint32_t clockskew) +{ + krb5_donot_replay rep = { 0 }; + + ctx->clockskew = clockskew; + (void)krb5_set_debugging_time(ctx, timestamp, 0); + rep.tag = make_data(tag, TAG_LEN); + return file2_store(ctx, rc, &rep); +} + +/* Store a sequence of unique tags, with timestamps far enough apart that all + * previous records appear expired. Verify that we only use one table. */ +static void +expiry_test(krb5_rcache rc, int reps, const char *filename) +{ + krb5_error_code ret; + struct stat statbuf; + uint8_t tag[TAG_LEN] = { 0 }, seed[K5_HASH_SEED_LEN] = { 0 }, data[4]; + uint32_t timestamp; + const uint32_t clockskew = 5, start = 1000; + uint64_t hashval; + int i, st; + + assert((uint32_t)reps < (UINT32_MAX - start) / clockskew / 2); + for (i = 0, timestamp = start; i < reps; i++, timestamp += clockskew * 2) { + store_32_be(i, data); + hashval = k5_siphash24(data, 4, seed); + store_64_be(hashval, tag); + + ret = test_store(rc, tag, timestamp, clockskew); + assert(ret == 0); + + /* Since we increment timestamp enough to expire every record between + * each call, we should never create a second hash table. */ + st = stat(filename, &statbuf); + assert(st == 0); + assert(statbuf.st_size <= (FIRST_TABLE_RECORDS + 1) * RECORD_LEN); + } +} + +/* Store a sequence of unique tags with the same timestamp. Exit with failure + * if any store operation doesn't succeed or fail as given by expect_fail. */ +static void +store_records(krb5_rcache rc, int id, int reps, int expect_fail) +{ + krb5_error_code ret; + uint8_t tag[TAG_LEN] = { 0 }; + int i; + + store_32_be(id, tag); + for (i = 0; i < reps; i++) { + store_32_be(i, tag + 4); + ret = test_store(rc, tag, 1000, 100); + if (ret != (expect_fail ? KRB5KRB_AP_ERR_REPEAT : 0)) { + fprintf(stderr, "store %d %d %sfail\n", id, i, + expect_fail ? "didn't " : ""); + _exit(1); + } + } +} + +/* Spawn multiple child processes, each storing a sequence of unique tags. + * After each process completes, verify that its tags appear as replays. */ +static void +concurrency_test(krb5_rcache rc, int nchildren, int reps) +{ + pid_t *pids, pid; + int i, nprocs, status; + + pids = calloc(nchildren, sizeof(*pids)); + assert(pids != NULL); + for (i = 0; i < nchildren; i++) { + pids[i] = fork(); + assert(pids[i] != -1); + if (pids[i] == 0) { + store_records(rc, i, reps, 0); + _exit(0); + } + } + for (nprocs = nchildren; nprocs > 0; nprocs--) { + pid = wait(&status); + assert(pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0); + for (i = 0; i < nchildren; i++) { + if (pids[i] == pid) + store_records(rc, i, reps, 1); + } + } + free(pids); +} + +/* Spawn multiple child processes, all trying to store the same tag. Verify + * that only one of the processes succeeded. Repeat reps times. */ +static void +race_test(krb5_rcache rc, int nchildren, int reps) +{ + int i, j, status, nsuccess; + uint8_t tag[TAG_LEN] = { 0 }; + pid_t pid; + + for (i = 0; i < reps; i++) { + store_32_be(i, tag); + for (j = 0; j < nchildren; j++) { + pid = fork(); + assert(pid != -1); + if (pid == 0) + _exit(test_store(rc, tag, 1000, 100) != 0); + } + + nsuccess = 0; + for (j = 0; j < nchildren; j++) { + pid = wait(&status); + assert(pid != -1); + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) + nsuccess++; + } + assert(nsuccess == 1); + } +} + +int +main(int argc, char **argv) +{ + const char *filename, *cmd; + struct krb5_rc_st rc = { 0 }; + + argv++; + assert(*argv != NULL); + + if (krb5_init_context(&ctx) != 0) + abort(); + + assert(*argv != NULL); + filename = *argv++; + unlink(filename); + rc.data = (void *)filename; + + assert(*argv != NULL); + cmd = *argv++; + if (strcmp(cmd, "expiry") == 0) { + assert(argv[0] != NULL); + expiry_test(&rc, atoi(argv[0]), filename); + } else if (strcmp(cmd, "concurrent") == 0) { + assert(argv[0] != NULL && argv[1] != NULL); + concurrency_test(&rc, atoi(argv[0]), atoi(argv[1])); + } else if (strcmp(cmd, "race") == 0) { + assert(argv[0] != NULL && argv[1] != NULL); + race_test(&rc, atoi(argv[0]), atoi(argv[1])); + } else { + abort(); + } + + krb5_free_context(ctx); + return 0; +}