diff --git a/tests/Makefile.am b/tests/Makefile.am index 29e0c424..1b990e6f 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -34,8 +34,9 @@ noinst_HEADERS = run-support.h noinst_PROGRAMS = $(TESTS) run-keylist run-export run-import run-sign \ run-verify run-encrypt run-identify run-decrypt run-genkey \ - run-keysign run-tofu run-swdb + run-keysign run-tofu run-swdb run-threaded +run_threaded_LDADD = ../src/libgpgme.la -lpthread @GPG_ERROR_LIBS@ if RUN_GPG_TESTS gpgtests = gpg json diff --git a/tests/run-threaded.c b/tests/run-threaded.c new file mode 100644 index 00000000..14a9cbec --- /dev/null +++ b/tests/run-threaded.c @@ -0,0 +1,678 @@ +/* run-threaded.c - Helper to put GPGME under multithread load. + Copyright (C) 2018 by Bundesamt für Sicherheit in der Informationstechnik + Software engineering by Intevation GmbH + + This file is part of GPGME. + + GPGME is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of + the License, or (at your option) any later version. + + GPGME 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this program; if not, see . +*/ + +/* The idea of this test is not to be run as unit test but as part + * of development to find threading issues and resource leaks. */ + +#ifdef HAVE_CONFIG_H +# include +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define PGM "run-threaded" + +#include "run-support.h" + +static volatile int stop; +static volatile int thread_cnt; +static volatile int running_threads; +static int verbose; +static int data_type; +static int mem_only; + +#ifdef HAVE_W32_SYSTEM +# include +# define THREAD_RET DWORD CALLBACK + +static void +create_thread (THREAD_RET (*func) (void *), void *arg) +{ + running_threads++; + if (CloseHandle (CreateThread (NULL, 0, func, arg, 0, NULL))) + { + fprintf (stderr, "Failed to create thread!\n"); + exit (1); + } +} + +#else +# include +# define THREAD_RET void * + +static void +create_thread (THREAD_RET (func) (void *), void *arg) +{ + pthread_t handle; + running_threads++; + if (pthread_create (&handle, NULL, func, arg)) + { + fprintf (stderr, "Failed to create thread!\n"); + exit (1); + } +} +#endif + +#include + +GPGRT_LOCK_DEFINE (out_lock); + +#define out(format, ...) \ + { \ + gpgrt_lock_lock (&out_lock); \ + printf (format "\n", ##__VA_ARGS__); \ + gpgrt_lock_unlock (&out_lock); \ + } + +#define errpoint \ +{ \ + out ("Error on %i", __LINE__); \ + exit (1); \ +} + +#define log(format, ...) \ +if (verbose) \ + { \ + gpgrt_lock_lock (&out_lock); \ + printf (format "\n", ##__VA_ARGS__); \ + gpgrt_lock_unlock (&out_lock); \ + } + +/* Lazy mans signal */ +GPGRT_LOCK_DEFINE (threads_lock); + +void del_thread (void) +{ + if (--running_threads == 0) + { + gpgrt_lock_unlock (&threads_lock); + } +} + +static int +show_usage (int ex) +{ + fputs ("usage: " PGM " [options] [messages]\n\n" + "Options:\n" + " --verbose run in verbose mode\n" + " --no-list do not do keylistings\n" + " --data-type mem function to use one of:\n" + " 1: fstream\n" + " 2: posix fd\n" + " 3: memory\n" + " 4: gpgrt_stream\n" + " default: random\n" +/* " --mem-cache read data only once and then work on memory\n" + " exlusive with data-type option\n" */ + " --threads N use 4+N threads (4 are used for keylisting" +" default 1)\n" + " --repeat N do N repeats on the messages (default 1)\n\n" + "Note: The test does keylistings of both S/MIME and OpenPGP\n" + " in the background while running operations on the\n" + " messages, depending on their type.\n" + " (Currently decrypt / verify).\n\n" + " Without messages only keylistings will be done.\n" + , stderr); + exit (ex); +} + + +struct msg_list_s +{ + const char *file_name; + struct msg_list_s *next; +}; +typedef struct msg_list_s *msg_list_t; + +struct data_s +{ + int fd; + FILE *file; + gpgrt_stream_t stream; + unsigned char *mem; + gpgme_data_t dh; +}; +typedef struct data_s *data_t; + +struct keylist_args_s +{ + gpgme_protocol_t proto; + int secret; +}; +typedef struct keylist_args_s *keylist_args_t; + +static volatile int keylists; + +static THREAD_RET +do_keylist (void *keylist_args) +{ + gpgme_error_t err; + gpgme_ctx_t ctx; + gpgme_key_t key; + + keylist_args_t args = (keylist_args_t) keylist_args; + + log ("Keylist %i, Protocol: %s, Secret %i", + keylists++, + args->proto == GPGME_PROTOCOL_CMS ? "CMS" : "OpenPGP", + args->secret); + + err = gpgme_new (&ctx); + fail_if_err (err); + + err = gpgme_set_protocol (ctx, args->proto); + fail_if_err (err); + + err = gpgme_op_keylist_start (ctx, NULL, args->secret); + fail_if_err (err); + + while (!(err = gpgme_op_keylist_next (ctx, &key))) + { + gpgme_key_unref (key); + } + + if (gpgme_err_code (err) != GPG_ERR_EOF) + { + fail_if_err (err); + } + + gpgme_release (ctx); + + if (!stop) + { + create_thread (do_keylist, keylist_args); + } + del_thread (); + return 0; +} + + +static unsigned char * +get_file (const char *fname, size_t *r_size) +{ + gpg_error_t err; + gpgrt_stream_t fp; + struct stat st; + unsigned char *buf; + size_t buflen; + + fp = gpgrt_fopen (fname, "r"); + if (!fp) + { + err = gpg_error_from_syserror (); + fprintf (stderr, "Error: can't open '%s': %s\n", fname, + gpg_strerror (err)); + return NULL; + } + + if (fstat (gpgrt_fileno(fp), &st)) + { + err = gpg_error_from_syserror (); + fprintf (stderr, "Error: can't stat '%s': %s\n", fname, + gpg_strerror (err)); + gpgrt_fclose (fp); + return NULL; + } + + buflen = st.st_size; + buf = malloc (buflen+1); + if (!buf) + { + fprintf (stderr, "Error: no mem\n"); + gpgrt_fclose (fp); + return NULL; + } + + if (gpgrt_fread (buf, buflen, 1, fp) != 1) + { + err = gpg_error_from_syserror (); + fprintf (stderr, "error reading '%s': %s\n", fname, + gpg_strerror (err)); + gpgrt_fclose (fp); + free (buf); + return NULL; + } + buf[buflen] = 0; + gpgrt_fclose (fp); + + if (r_size) + { + *r_size = buflen; + } + + return buf; +} + +/** Lets use random data. This should also introduce a bit + of randomness into the system by changing the runtimes + of various data functions. Esp. Mem against the file ops. */ +data_t +random_data_new (const char *fname) +{ + data_t ret = calloc (1, sizeof (struct data_s)); + int data_rand; + if (data_type) + { + data_rand = data_type; + } + else + { + data_rand = rand () % 3; + } + + if (data_rand == 0) /* stream */ + { + FILE *f_stream = fopen (fname, "rb"); + if (!f_stream) + { + errpoint; + } + fail_if_err (gpgme_data_new_from_stream (&(ret->dh), f_stream)); + ret->file = f_stream; + return ret; + } + if (data_rand == 1) /* fd */ + { + int fd = open (fname, O_RDONLY); + gpgme_data_t dh; + if (fd == -1) + { + errpoint; + } + fail_if_err (gpgme_data_new_from_fd (&dh, fd)); + ret->fd = fd; + ret->dh = dh; + return ret; + } + if (data_rand == 2) /* mem */ + { + unsigned char *mem; + size_t size; + + mem = get_file (fname, &size); + if (!mem) + { + errpoint; + } + fail_if_err (gpgme_data_new_from_mem (&(ret->dh), + (const char *)mem, + size, 0)); + ret->mem = mem; + return ret; + } + if (data_rand == 3) /* estream */ + { + gpgrt_stream_t stream = gpgrt_fopen (fname, "rb"); + + if (!stream) + { + errpoint; + } + + fail_if_err (gpgme_data_new_from_estream (&(ret->dh), stream)); + ret->stream = stream; + return ret; + } + /* notreached */ + return ret; +} + +void +random_data_close (data_t data) +{ + if (data->dh) + { + gpgme_data_release (data->dh); + } + if (data->fd) + { + close (data->fd); + } + else if (data->file) + { + fclose (data->file); + } + else if (data->stream) + { + gpgrt_fclose (data->stream); + } + else if (data->mem) + { + free (data->mem); + } + free (data); +} + +void +verify (const char *fname, gpgme_protocol_t proto) +{ + gpgme_ctx_t ctx; + gpgme_error_t err; + gpgme_data_t out; + char *msg; + size_t msg_len; + data_t data = random_data_new (fname); + + log ("Starting verify on: %s with protocol %s", fname, + proto == GPGME_PROTOCOL_CMS ? "CMS" : "OpenPGP"); + + gpgme_data_new (&out); + + err = gpgme_new (&ctx); + fail_if_err (err); + + err = gpgme_set_protocol (ctx, proto); + fail_if_err (err); + + err = gpgme_op_verify (ctx, data->dh, NULL, out); + out ("Data: %p, %i %p %p %p", data->dh, + data->fd, data->file, data->stream, + data->mem); + fail_if_err (err); + + msg = gpgme_data_release_and_get_mem (out, &msg_len); + + if (msg_len) + { + log ("Verify result \n'%.*s'", (int)msg_len, msg); + } + + gpgme_release (ctx); + + random_data_close (data); +} + + +/* We randomize data access to put in a bit additional + entropy in this test and also to check if maybe + some data functions might not be properly thread + safe. */ +void +decrypt (const char *fname, gpgme_protocol_t proto) +{ + gpgme_ctx_t ctx; + gpgme_error_t err; + gpgme_data_t out; + char *msg; + size_t msg_len; + data_t data = random_data_new (fname); + + log ("Starting decrypt on: %s", fname); + + gpgme_data_new (&out); + + err = gpgme_new (&ctx); + fail_if_err (err); + + err = gpgme_set_protocol (ctx, proto); + fail_if_err (err); + + err = gpgme_op_decrypt (ctx, data->dh, out); + fail_if_err (err); + + gpgme_release (ctx); + + msg = gpgme_data_release_and_get_mem (out, &msg_len); + + if (msg_len) + { + log ("Decrypt result \n'%.*s'", (int)msg_len, msg); + } + + random_data_close (data); +} + + +static THREAD_RET +do_data_op (void *file_name) +{ + gpgme_data_t data; + const char *fname = (const char *) file_name; + FILE *f = fopen (fname, "rb"); + gpgme_data_type_t type; + + if (!f) + { + fprintf (stderr, "Failed to open '%s'\n", fname); + exit (1); + } + + fail_if_err (gpgme_data_new_from_stream (&data, f)); + + type = gpgme_data_identify (data, 0); + gpgme_data_release (data); + fclose (f); + + switch (type) + { + case GPGME_DATA_TYPE_INVALID: + case GPGME_DATA_TYPE_UNKNOWN: + { + fprintf (stderr, "Failed to identify '%s'", fname); + exit(1); + } + case GPGME_DATA_TYPE_PGP_SIGNED: + { + verify (fname, GPGME_PROTOCOL_OpenPGP); + break; + } + case GPGME_DATA_TYPE_CMS_SIGNED: + { + verify (fname, GPGME_PROTOCOL_CMS); + break; + } + case GPGME_DATA_TYPE_PGP_ENCRYPTED: + { + decrypt (fname, GPGME_PROTOCOL_OpenPGP); + break; + } + case GPGME_DATA_TYPE_CMS_ENCRYPTED: + { + decrypt (fname, GPGME_PROTOCOL_CMS); + break; + } + default: + { + fprintf (stderr, "Unhandled data type 0x%x for '%s'", type, fname); + exit(1); + } + } + + del_thread (); + return 0; +} + + +void +start_keylistings (void) +{ + static struct keylist_args_s args[4]; + + args[0].proto = GPGME_PROTOCOL_OpenPGP; + args[0].secret = 0; + + args[1].proto = GPGME_PROTOCOL_OpenPGP; + args[1].secret = 1; + + args[2].proto = GPGME_PROTOCOL_CMS; + args[2].secret = 0; + + args[3].proto = GPGME_PROTOCOL_CMS; + args[3].secret = 1; + + for (int i = 0; i < 4; i++) + { + thread_cnt--; + create_thread (do_keylist, &args[i]); + } +} + +int +main (int argc, char **argv) +{ + int last_argc = -1; + int repeats = 1; + int threads = 0; + int no_list = 0; + msg_list_t msgs = NULL; + msg_list_t msg_it = NULL; + stop = 0; + + srand (1 /* Somewhat deterministic results */); + + if (argc) + { argc--; argv++; } + + while (argc && last_argc != argc ) + { + last_argc = argc; + if (!strcmp (*argv, "--help")) + { + show_usage (0); + } + else if (!strcmp (*argv, "--verbose")) + { + verbose = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--no-list")) + { + no_list = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--mem-only")) + { + if (data_type) + { + show_usage (1); + } + mem_only = 1; + argc--; argv++; + } + else if (!strcmp (*argv, "--threads")) + { + argc--; argv++; + if (!argc) + { + show_usage (1); + } + threads = atoi (*argv); + if (!threads) + { + show_usage (1); + } + argc--; argv++; + } + else if (!strcmp (*argv, "--data-type")) + { + argc--; argv++; + if (!argc || mem_only) + { + show_usage (1); + } + data_type = atoi (*argv); + if (data_type > 4) + { + show_usage (1); + } + argc--; argv++; + } + else if (!strcmp (*argv, "--repeat")) + { + argc--; argv++; + if (!argc) + { + show_usage (1); + } + repeats = atoi (*argv); + if (!repeats) + { + show_usage (1); + } + argc--; argv++; + } + } + + init_gpgme_basic (); + + if (threads < argc) + { + /* Make sure we run once on each arg */ + threads += argc; + } + while (argc) + { + if (!msgs) + { + msgs = calloc (1, sizeof *msgs); + msg_it = msgs; + } + else + { + msg_it->next = calloc (1, sizeof *msgs); + msg_it = msg_it->next; + } + msg_it->file_name = *argv; + argc--; argv++; + } + + gpgrt_lock_lock (&threads_lock); + do + { + stop = 0; + thread_cnt = threads + 4; + out ("Repeats left: %i", repeats); + + if (!no_list) + { + start_keylistings (); + } + else + { + thread_cnt -= 4; + } + + while (thread_cnt) + { + log ("Thread %i", thread_cnt); + for (msg_it = msgs; msg_it && thread_cnt; msg_it = msg_it->next) + { + thread_cnt--; + create_thread (do_data_op, (void *)msg_it->file_name); + } + } + + stop = 1; + gpgrt_lock_lock (&threads_lock); + } + while (--repeats != 0); + + return 0; +}