gpgme/tests/run-threaded.c
Andre Heinecke d0402f886b
tests: Add run-threaded for multithread tests
* tests/Makefile.am (run-threaded): Add.
* tests/run-threaded.c: New.

--
This test is intended to help detect race conditions
or other multithread problems. It can also be used
to put the whole GnuPG system under extreme load.
2018-11-15 11:57:27 +01:00

679 lines
14 KiB
C

/* 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 <https://www.gnu.org/licenses/>.
*/
/* 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 <config.h>
#endif
#include <gpgme.h>
#include <gpg-error.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#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 <windows.h>
# 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 <pthread.h>
# 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 <gpg-error.h>
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;
}