From 688e8d8ffe331a1dd75a78002bf212277f2d7664 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 21 Mar 2017 13:25:11 +0100 Subject: [PATCH 35/36] KCM: Queue requests by the same UID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to avoid race conditions, we queue requests towards the KCM responder coming from the same client UID. Reviewed-by: Michal Židek Reviewed-by: Simo Sorce Reviewed-by: Lukáš Slebodník --- Makefile.am | 21 ++- src/responder/kcm/kcm.c | 7 + src/responder/kcm/kcmsrv_cmd.c | 10 +- src/responder/kcm/kcmsrv_op_queue.c | 264 ++++++++++++++++++++++++++ src/responder/kcm/kcmsrv_ops.c | 44 ++++- src/responder/kcm/kcmsrv_ops.h | 1 + src/responder/kcm/kcmsrv_pvt.h | 20 ++ src/tests/cmocka/test_kcm_queue.c | 365 ++++++++++++++++++++++++++++++++++++ 8 files changed, 721 insertions(+), 11 deletions(-) create mode 100644 src/responder/kcm/kcmsrv_op_queue.c create mode 100644 src/tests/cmocka/test_kcm_queue.c diff --git a/Makefile.am b/Makefile.am index e9eaa312c91e3aee40bcf13c90a0ad8c683045d5..91afdd669aa11a3cc316588d3b51d7e8e9c91cb8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -304,7 +304,10 @@ non_interactive_cmocka_based_tests += test_inotify endif # HAVE_INOTIFY if BUILD_KCM -non_interactive_cmocka_based_tests += test_kcm_json +non_interactive_cmocka_based_tests += \ + test_kcm_json \ + test_kcm_queue \ + $(NULL) endif # BUILD_KCM if BUILD_SAMBA @@ -1501,6 +1504,7 @@ sssd_kcm_SOURCES = \ src/responder/kcm/kcmsrv_ccache_json.c \ src/responder/kcm/kcmsrv_ccache_secrets.c \ src/responder/kcm/kcmsrv_ops.c \ + src/responder/kcm/kcmsrv_op_queue.c \ src/util/sss_sockets.c \ src/util/sss_krb5.c \ src/util/sss_iobuf.c \ @@ -3402,6 +3406,21 @@ test_kcm_json_LDADD = \ $(SSSD_INTERNAL_LTLIBS) \ libsss_test_common.la \ $(NULL) + +test_kcm_queue_SOURCES = \ + src/tests/cmocka/test_kcm_queue.c \ + src/responder/kcm/kcmsrv_op_queue.c \ + $(NULL) +test_kcm_queue_CFLAGS = \ + $(AM_CFLAGS) \ + $(NULL) +test_kcm_queue_LDADD = \ + $(CMOCKA_LIBS) \ + $(SSSD_LIBS) \ + $(SSSD_INTERNAL_LTLIBS) \ + libsss_test_common.la \ + $(NULL) + endif # BUILD_KCM endif # HAVE_CMOCKA diff --git a/src/responder/kcm/kcm.c b/src/responder/kcm/kcm.c index 063c27b915b4b92f6259496feee891aa94a498b6..3ee978066c589a5cc38b0ae358f741d389d00e7a 100644 --- a/src/responder/kcm/kcm.c +++ b/src/responder/kcm/kcm.c @@ -133,6 +133,13 @@ static int kcm_get_config(struct kcm_ctx *kctx) goto done; } + kctx->qctx = kcm_ops_queue_create(kctx); + if (ret != EOK) { + DEBUG(SSSDBG_OP_FAILURE, + "Cannot create KCM request queue [%d]: %s\n", + ret, strerror(ret)); + goto done; + } ret = EOK; done: return ret; diff --git a/src/responder/kcm/kcmsrv_cmd.c b/src/responder/kcm/kcmsrv_cmd.c index 537e88953fd1a190a9a73bcdd430d8e0db8f9291..81015de4a91617de3dca444cde95b636c8d5c0d1 100644 --- a/src/responder/kcm/kcmsrv_cmd.c +++ b/src/responder/kcm/kcmsrv_cmd.c @@ -353,14 +353,18 @@ struct kcm_req_ctx { static void kcm_cmd_request_done(struct tevent_req *req); -static errno_t kcm_cmd_dispatch(struct kcm_req_ctx *req_ctx) +static errno_t kcm_cmd_dispatch(struct kcm_ctx *kctx, + struct kcm_req_ctx *req_ctx) { struct tevent_req *req; struct cli_ctx *cctx; cctx = req_ctx->cctx; - req = kcm_cmd_send(req_ctx, cctx->ev, req_ctx->kctx->kcm_data, + req = kcm_cmd_send(req_ctx, + cctx->ev, + kctx->qctx, + req_ctx->kctx->kcm_data, req_ctx->cctx->creds, &req_ctx->op_io.request, req_ctx->op_io.op); @@ -505,7 +509,7 @@ static void kcm_recv(struct cli_ctx *cctx) /* do not read anymore, client is done sending */ TEVENT_FD_NOT_READABLE(cctx->cfde); - ret = kcm_cmd_dispatch(req); + ret = kcm_cmd_dispatch(kctx, req); if (ret != EOK) { DEBUG(SSSDBG_FATAL_FAILURE, "Failed to dispatch KCM operation [%d]: %s\n", diff --git a/src/responder/kcm/kcmsrv_op_queue.c b/src/responder/kcm/kcmsrv_op_queue.c new file mode 100644 index 0000000000000000000000000000000000000000..f6c425dd5b64877c8b7401e488dd6565157fc9b5 --- /dev/null +++ b/src/responder/kcm/kcmsrv_op_queue.c @@ -0,0 +1,264 @@ +/* + SSSD + + KCM Server - the KCM operations wait queue + + Copyright (C) Red Hat, 2017 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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 + along with this program. If not, see . +*/ + +#include "util/util.h" +#include "util/util_creds.h" +#include "responder/kcm/kcmsrv_pvt.h" + +#define QUEUE_HASH_SIZE 32 + +struct kcm_ops_queue_entry { + struct tevent_req *req; + uid_t uid; + + hash_table_t *wait_queue_hash; + + struct kcm_ops_queue_entry *head; + struct kcm_ops_queue_entry *next; + struct kcm_ops_queue_entry *prev; +}; + +struct kcm_ops_queue_ctx { + /* UID: dlist of kcm_ops_queue_entry */ + hash_table_t *wait_queue_hash; +}; + +/* + * Per-UID wait queue + * + * They key in the hash table is the UID of the peer. The value of each + * hash table entry is a linked list of kcm_ops_queue_entry structures + * which primarily hold the tevent request being queued. + */ +struct kcm_ops_queue_ctx *kcm_ops_queue_create(TALLOC_CTX *mem_ctx) +{ + errno_t ret; + struct kcm_ops_queue_ctx *queue_ctx; + + queue_ctx = talloc_zero(mem_ctx, struct kcm_ops_queue_ctx); + if (queue_ctx == NULL) { + return NULL; + } + + ret = sss_hash_create_ex(mem_ctx, QUEUE_HASH_SIZE, + &queue_ctx->wait_queue_hash, 0, 0, 0, 0, + NULL, NULL); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, + "sss_hash_create failed [%d]: %s\n", ret, sss_strerror(ret)); + talloc_free(queue_ctx); + return NULL; + } + + return queue_ctx; +} + +static int kcm_op_queue_entry_destructor(struct kcm_ops_queue_entry *entry) +{ + int ret; + struct kcm_ops_queue_entry *next_entry; + hash_key_t key; + + if (entry == NULL) { + return 1; + } + + /* Take the next entry from the queue */ + next_entry = entry->next; + + /* Remove the current entry from the queue */ + DLIST_REMOVE(entry->head, entry); + + if (next_entry == NULL) { + key.type = HASH_KEY_ULONG; + key.ul = entry->uid; + + /* If this was the last entry, remove the key (the UID) from the + * hash table to signal the queue is empty + */ + ret = hash_delete(entry->wait_queue_hash, &key); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Failed to remove wait queue for user %"SPRIuid"\n", + entry->uid); + return 1; + } + return 0; + } + + /* Otherwise, mark the current head as done to run the next request */ + tevent_req_done(next_entry->req); + return 0; +} + +static errno_t kcm_op_queue_add(hash_table_t *wait_queue_hash, + struct kcm_ops_queue_entry *entry, + uid_t uid) +{ + errno_t ret; + hash_key_t key; + hash_value_t value; + struct kcm_ops_queue_entry *head = NULL; + + key.type = HASH_KEY_ULONG; + key.ul = uid; + + ret = hash_lookup(wait_queue_hash, &key, &value); + switch (ret) { + case HASH_SUCCESS: + /* The key with this UID already exists. Its value is request queue + * for the UID, so let's just add the current request to the end + * of the queue and wait for the previous requests to finish + */ + if (value.type != HASH_VALUE_PTR) { + DEBUG(SSSDBG_CRIT_FAILURE, "Unexpected hash value type.\n"); + return EINVAL; + } + + head = talloc_get_type(value.ptr, struct kcm_ops_queue_entry); + if (head == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Invalid queue pointer\n"); + return EINVAL; + } + + entry->head = head; + DLIST_ADD_END(head, entry, struct kcm_ops_queue_entry *); + + DEBUG(SSSDBG_TRACE_LIBS, "Waiting in queue\n"); + ret = EAGAIN; + break; + + case HASH_ERROR_KEY_NOT_FOUND: + /* No request for this UID yet. Enqueue this request in case + * another one comes in and return EOK to run the current request + * immediatelly + */ + entry->head = entry; + + value.type = HASH_VALUE_PTR; + value.ptr = entry; + + ret = hash_enter(wait_queue_hash, &key, &value); + if (ret != HASH_SUCCESS) { + DEBUG(SSSDBG_CRIT_FAILURE, "hash_enter failed.\n"); + return EIO; + } + + DEBUG(SSSDBG_TRACE_LIBS, + "Added a first request to the queue, running immediately\n"); + ret = EOK; + break; + + default: + DEBUG(SSSDBG_CRIT_FAILURE, "hash_lookup failed.\n"); + return EIO; + } + + talloc_steal(wait_queue_hash, entry); + talloc_set_destructor(entry, kcm_op_queue_entry_destructor); + return ret; +} + +struct kcm_op_queue_state { + struct kcm_ops_queue_entry *entry; +}; + +/* + * Enqueue a request. + * + * If the request queue /for the given ID/ is empty, that is, if this + * request is the first one in the queue, run the request immediatelly. + * + * Otherwise just add it to the queue and wait until the previous request + * finishes and only at that point mark the current request as done, which + * will trigger calling the recv function and allow the request to continue. + */ +struct tevent_req *kcm_op_queue_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct kcm_ops_queue_ctx *qctx, + struct cli_creds *client) +{ + errno_t ret; + struct tevent_req *req; + struct kcm_op_queue_state *state; + uid_t uid; + + uid = cli_creds_get_uid(client); + + req = tevent_req_create(mem_ctx, &state, struct kcm_op_queue_state); + if (req == NULL) { + return NULL; + } + + state->entry = talloc_zero(state, struct kcm_ops_queue_entry); + if (state->entry == NULL) { + ret = ENOMEM; + goto immediate; + } + state->entry->req = req; + state->entry->uid = uid; + state->entry->wait_queue_hash = qctx->wait_queue_hash; + + DEBUG(SSSDBG_FUNC_DATA, + "Adding request by %"SPRIuid" to the wait queue\n", uid); + + ret = kcm_op_queue_add(qctx->wait_queue_hash, state->entry, uid); + if (ret == EOK) { + DEBUG(SSSDBG_TRACE_LIBS, + "Wait queue was empty, running immediately\n"); + goto immediate; + } else if (ret != EAGAIN) { + DEBUG(SSSDBG_OP_FAILURE, + "Cannot enqueue request [%d]: %s\n", ret, sss_strerror(ret)); + goto immediate; + } + + DEBUG(SSSDBG_TRACE_LIBS, "Waiting our turn in the queue\n"); + return req; + +immediate: + if (ret == EOK) { + tevent_req_done(req); + } else { + tevent_req_error(req, ret); + } + tevent_req_post(req, ev); + return req; +} + +/* + * The queue recv function is called when this request is 'activated'. The queue + * entry should be allocated on the same memory context as the enqueued request + * to trigger freeing the kcm_ops_queue_entry structure destructor when the + * parent request is done and its tevent_req freed. This would in turn unblock + * the next request in the queue + */ +errno_t kcm_op_queue_recv(struct tevent_req *req, + TALLOC_CTX *mem_ctx, + struct kcm_ops_queue_entry **_entry) +{ + struct kcm_op_queue_state *state = tevent_req_data(req, + struct kcm_op_queue_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + *_entry = talloc_steal(mem_ctx, state->entry); + return EOK; +} diff --git a/src/responder/kcm/kcmsrv_ops.c b/src/responder/kcm/kcmsrv_ops.c index 50e8cc635424e15d53e3c8d122c5525044f59c8a..2feaf51f227ce9d90f706229ce7ac201b282dc6f 100644 --- a/src/responder/kcm/kcmsrv_ops.c +++ b/src/responder/kcm/kcmsrv_ops.c @@ -67,17 +67,21 @@ struct kcm_op { struct kcm_cmd_state { struct kcm_op *op; + struct tevent_context *ev; + struct kcm_ops_queue_entry *queue_entry; struct kcm_op_ctx *op_ctx; struct sss_iobuf *reply; uint32_t op_ret; }; +static void kcm_cmd_queue_done(struct tevent_req *subreq); static void kcm_cmd_done(struct tevent_req *subreq); struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev, + struct kcm_ops_queue_ctx *qctx, struct kcm_resp_ctx *kcm_data, struct cli_creds *client, struct kcm_data *input, @@ -93,6 +97,7 @@ struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx, return NULL; } state->op = op; + state->ev = ev; if (op == NULL) { ret = EINVAL; @@ -154,18 +159,43 @@ struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx, goto immediate; } - subreq = op->fn_send(state, ev, state->op_ctx); + subreq = kcm_op_queue_send(state, ev, qctx, client); if (subreq == NULL) { ret = ENOMEM; goto immediate; } + tevent_req_set_callback(subreq, kcm_cmd_queue_done, req); + return req; + +immediate: + tevent_req_error(req, ret); + tevent_req_post(req, ev); + return req; +} + +static void kcm_cmd_queue_done(struct tevent_req *subreq) +{ + struct tevent_req *req = tevent_req_callback_data(subreq, struct tevent_req); + struct kcm_cmd_state *state = tevent_req_data(req, struct kcm_cmd_state); + errno_t ret; + + /* When this request finishes, it frees the queue_entry which unblocks + * other requests by the same UID + */ + ret = kcm_op_queue_recv(subreq, state, &state->queue_entry); + talloc_zfree(subreq); + if (ret != EOK) { + DEBUG(SSSDBG_CRIT_FAILURE, "Cannot acquire queue slot\n"); + tevent_req_error(req, ret); + return; + } + + subreq = state->op->fn_send(state, state->ev, state->op_ctx); + if (subreq == NULL) { + tevent_req_error(req, ENOMEM); + return; + } tevent_req_set_callback(subreq, kcm_cmd_done, req); - return req; - -immediate: - tevent_req_error(req, ret); - tevent_req_post(req, ev); - return req; } static void kcm_cmd_done(struct tevent_req *subreq) diff --git a/src/responder/kcm/kcmsrv_ops.h b/src/responder/kcm/kcmsrv_ops.h index 8e6feaf56a10b73c8b6375aea9ef26c392b5b492..67d9f86026bf949548471f2280c130ebefd2f865 100644 --- a/src/responder/kcm/kcmsrv_ops.h +++ b/src/responder/kcm/kcmsrv_ops.h @@ -34,6 +34,7 @@ const char *kcm_opt_name(struct kcm_op *op); struct tevent_req *kcm_cmd_send(TALLOC_CTX *mem_ctx, struct tevent_context *ev, + struct kcm_ops_queue_ctx *qctx, struct kcm_resp_ctx *kcm_data, struct cli_creds *client, struct kcm_data *input, diff --git a/src/responder/kcm/kcmsrv_pvt.h b/src/responder/kcm/kcmsrv_pvt.h index 74f30c00014105ed533744779b02c5d42523722d..f081a6bf0c6e40d2f8a83b07f9bbc2abacff359d 100644 --- a/src/responder/kcm/kcmsrv_pvt.h +++ b/src/responder/kcm/kcmsrv_pvt.h @@ -25,6 +25,7 @@ #include "config.h" #include +#include #include "responder/common/responder.h" /* @@ -65,6 +66,7 @@ struct kcm_ctx { int fd_limit; char *socket_path; enum kcm_ccdb_be cc_be; + struct kcm_ops_queue_ctx *qctx; struct kcm_resp_ctx *kcm_data; }; @@ -78,4 +80,22 @@ int kcm_connection_setup(struct cli_ctx *cctx); */ krb5_error_code sss2krb5_error(errno_t err); +/* We enqueue all requests by the same UID to avoid concurrency issues + * especially when performing multiple round-trips to sssd-secrets. In + * future, we should relax the queue to allow multiple read-only operations + * if no write operations are in progress. + */ +struct kcm_ops_queue_entry; + +struct kcm_ops_queue_ctx *kcm_ops_queue_create(TALLOC_CTX *mem_ctx); + +struct tevent_req *kcm_op_queue_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct kcm_ops_queue_ctx *qctx, + struct cli_creds *client); + +errno_t kcm_op_queue_recv(struct tevent_req *req, + TALLOC_CTX *mem_ctx, + struct kcm_ops_queue_entry **_entry); + #endif /* __KCMSRV_PVT_H__ */ diff --git a/src/tests/cmocka/test_kcm_queue.c b/src/tests/cmocka/test_kcm_queue.c new file mode 100644 index 0000000000000000000000000000000000000000..ba0d2405629960df5c623848f3207b7c80fa948d --- /dev/null +++ b/src/tests/cmocka/test_kcm_queue.c @@ -0,0 +1,365 @@ +/* + Copyright (C) 2017 Red Hat + + SSSD tests: Test KCM wait queue + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program 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 + along with this program. If not, see . +*/ + +#include "config.h" + +#include +#include + +#include "util/util.h" +#include "util/util_creds.h" +#include "tests/cmocka/common_mock.h" +#include "responder/kcm/kcmsrv_pvt.h" + +#define INVALID_ID -1 +#define FAST_REQ_ID 0 +#define SLOW_REQ_ID 1 + +#define FAST_REQ_DELAY 1 +#define SLOW_REQ_DELAY 2 + +struct timed_request_state { + struct tevent_context *ev; + struct kcm_ops_queue_ctx *qctx; + struct cli_creds *client; + int delay; + int req_id; + + struct kcm_ops_queue_entry *queue_entry; +}; + +static void timed_request_start(struct tevent_req *subreq); +static void timed_request_done(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval current_time, + void *pvt); + +static struct tevent_req *timed_request_send(TALLOC_CTX *mem_ctx, + struct tevent_context *ev, + struct kcm_ops_queue_ctx *qctx, + struct cli_creds *client, + int delay, + int req_id) +{ + struct tevent_req *req; + struct tevent_req *subreq; + struct timed_request_state *state; + + req = tevent_req_create(mem_ctx, &state, struct timed_request_state); + if (req == NULL) { + return NULL; + } + state->ev = ev; + state->qctx = qctx; + state->client = client; + state->delay = delay; + state->req_id = req_id; + + DEBUG(SSSDBG_TRACE_ALL, "Request %p with delay %d\n", req, delay); + + subreq = kcm_op_queue_send(state, ev, qctx, client); + if (subreq == NULL) { + return NULL; + } + tevent_req_set_callback(subreq, timed_request_start, req); + + return req; +} + +static void timed_request_start(struct tevent_req *subreq) +{ + struct timeval tv; + struct tevent_timer *timeout = NULL; + struct tevent_req *req = tevent_req_callback_data(subreq, + struct tevent_req); + struct timed_request_state *state = tevent_req_data(req, + struct timed_request_state); + errno_t ret; + + ret = kcm_op_queue_recv(subreq, state, &state->queue_entry); + talloc_zfree(subreq); + if (ret != EOK) { + tevent_req_error(req, ret); + return; + } + + tv = tevent_timeval_current_ofs(state->delay, 0); + timeout = tevent_add_timer(state->ev, state, tv, timed_request_done, req); + if (timeout == NULL) { + tevent_req_error(req, ENOMEM); + return; + } + + return; +} + +static void timed_request_done(struct tevent_context *ev, + struct tevent_timer *te, + struct timeval current_time, + void *pvt) +{ + struct tevent_req *req = talloc_get_type(pvt, struct tevent_req); + DEBUG(SSSDBG_TRACE_ALL, "Request %p done\n", req); + tevent_req_done(req); +} + +static errno_t timed_request_recv(struct tevent_req *req, + int *req_id) +{ + struct timed_request_state *state = tevent_req_data(req, + struct timed_request_state); + + TEVENT_REQ_RETURN_ON_ERROR(req); + *req_id = state->req_id; + return EOK; +} + +struct test_ctx { + struct kcm_ops_queue_ctx *qctx; + struct tevent_context *ev; + + int *req_ids; + + int num_requests; + int finished_requests; + bool done; + errno_t error; +}; + +static int setup_kcm_queue(void **state) +{ + struct test_ctx *tctx; + + tctx = talloc_zero(NULL, struct test_ctx); + assert_non_null(tctx); + + tctx->ev = tevent_context_init(tctx); + assert_non_null(tctx->ev); + + tctx->qctx = kcm_ops_queue_create(tctx); + assert_non_null(tctx->qctx); + + *state = tctx; + return 0; +} + +static int teardown_kcm_queue(void **state) +{ + struct test_ctx *tctx = talloc_get_type(*state, struct test_ctx); + talloc_free(tctx); + return 0; +} + +static void test_kcm_queue_done(struct tevent_req *req) +{ + struct test_ctx *test_ctx = tevent_req_callback_data(req, + struct test_ctx); + int req_id = INVALID_ID; + + test_ctx->error = timed_request_recv(req, &req_id); + talloc_zfree(req); + if (test_ctx->error != EOK) { + test_ctx->done = true; + return; + } + + if (test_ctx->req_ids[test_ctx->finished_requests] != req_id) { + DEBUG(SSSDBG_CRIT_FAILURE, + "Request %d finished, expected %d\n", + req_id, test_ctx->req_ids[test_ctx->finished_requests]); + test_ctx->error = EIO; + test_ctx->done = true; + return; + } + + test_ctx->finished_requests++; + if (test_ctx->finished_requests == test_ctx->num_requests) { + test_ctx->done = true; + return; + } +} + +/* + * Just make sure that a single pass through the queue works + */ +static void test_kcm_queue_single(void **state) +{ + struct test_ctx *test_ctx = talloc_get_type(*state, struct test_ctx); + struct tevent_req *req; + struct cli_creds client; + static int req_ids[] = { 0 }; + + client.ucred.uid = getuid(); + client.ucred.gid = getgid(); + + req = timed_request_send(test_ctx, + test_ctx->ev, + test_ctx->qctx, + &client, 1, 0); + assert_non_null(req); + tevent_req_set_callback(req, test_kcm_queue_done, test_ctx); + + test_ctx->num_requests = 1; + test_ctx->req_ids = req_ids; + + while (test_ctx->done == false) { + tevent_loop_once(test_ctx->ev); + } + assert_int_equal(test_ctx->error, EOK); +} + +/* + * Test that multiple requests from the same ID wait for one another + */ +static void test_kcm_queue_multi_same_id(void **state) +{ + struct test_ctx *test_ctx = talloc_get_type(*state, struct test_ctx); + struct tevent_req *req; + struct cli_creds client; + /* The slow request will finish first because request from + * the same ID are serialized + */ + static int req_ids[] = { SLOW_REQ_ID, FAST_REQ_ID }; + + client.ucred.uid = getuid(); + client.ucred.gid = getgid(); + + req = timed_request_send(test_ctx, + test_ctx->ev, + test_ctx->qctx, + &client, + SLOW_REQ_DELAY, + SLOW_REQ_ID); + assert_non_null(req); + tevent_req_set_callback(req, test_kcm_queue_done, test_ctx); + + req = timed_request_send(test_ctx, + test_ctx->ev, + test_ctx->qctx, + &client, + FAST_REQ_DELAY, + FAST_REQ_ID); + assert_non_null(req); + tevent_req_set_callback(req, test_kcm_queue_done, test_ctx); + + test_ctx->num_requests = 2; + test_ctx->req_ids = req_ids; + + while (test_ctx->done == false) { + tevent_loop_once(test_ctx->ev); + } + assert_int_equal(test_ctx->error, EOK); +} + +/* + * Test that multiple requests from different IDs don't wait for one + * another and can run concurrently + */ +static void test_kcm_queue_multi_different_id(void **state) +{ + struct test_ctx *test_ctx = talloc_get_type(*state, struct test_ctx); + struct tevent_req *req; + struct cli_creds client; + /* In this test, the fast request will finish sooner because + * both requests are from different IDs, allowing them to run + * concurrently + */ + static int req_ids[] = { FAST_REQ_ID, SLOW_REQ_ID }; + + client.ucred.uid = getuid(); + client.ucred.gid = getgid(); + + req = timed_request_send(test_ctx, + test_ctx->ev, + test_ctx->qctx, + &client, + SLOW_REQ_DELAY, + SLOW_REQ_ID); + assert_non_null(req); + tevent_req_set_callback(req, test_kcm_queue_done, test_ctx); + + client.ucred.uid = getuid() + 1; + client.ucred.gid = getgid() + 1; + + req = timed_request_send(test_ctx, + test_ctx->ev, + test_ctx->qctx, + &client, + FAST_REQ_DELAY, + FAST_REQ_ID); + assert_non_null(req); + tevent_req_set_callback(req, test_kcm_queue_done, test_ctx); + + test_ctx->num_requests = 2; + test_ctx->req_ids = req_ids; + + while (test_ctx->done == false) { + tevent_loop_once(test_ctx->ev); + } + assert_int_equal(test_ctx->error, EOK); +} + +int main(int argc, const char *argv[]) +{ + poptContext pc; + int opt; + int rv; + struct poptOption long_options[] = { + POPT_AUTOHELP + SSSD_DEBUG_OPTS + POPT_TABLEEND + }; + + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(test_kcm_queue_single, + setup_kcm_queue, + teardown_kcm_queue), + cmocka_unit_test_setup_teardown(test_kcm_queue_multi_same_id, + setup_kcm_queue, + teardown_kcm_queue), + cmocka_unit_test_setup_teardown(test_kcm_queue_multi_different_id, + setup_kcm_queue, + teardown_kcm_queue), + }; + + /* Set debug level to invalid value so we can deside if -d 0 was used. */ + debug_level = SSSDBG_INVALID; + + pc = poptGetContext(argv[0], argc, argv, long_options, 0); + while((opt = poptGetNextOpt(pc)) != -1) { + switch(opt) { + default: + fprintf(stderr, "\nInvalid option %s: %s\n\n", + poptBadOption(pc, 0), poptStrerror(opt)); + poptPrintUsage(pc, stderr, 0); + return 1; + } + } + poptFreeContext(pc); + + DEBUG_CLI_INIT(debug_level); + + /* Even though normally the tests should clean up after themselves + * they might not after a failed run. Remove the old db to be sure */ + tests_set_cwd(); + + rv = cmocka_run_group_tests(tests, NULL, NULL); + + return rv; +} -- 2.9.3