0

I'm trying to run a piece of code with multiple threads. My understanding is, even though threads are sharing process memory, each function calling from the thread has its own stack frame, registers. Therefore, calling the same function from multiple threads should not be a problem (please correct me if I'm wrong).

I have the following code. This code works normally with a single thread. My goal is to run some part of this code concurrently.

#include "rsa/config.h"
#include "rsa/aes.h"
#include "rsa/bignum.h"
#include "rsa/rsa.h"
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <pthread.h>
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>


#define NUM_OF_THREAD 8  

void thread_function(void * input){
    printf("Test thread\n");
    unsigned char private_encrypt[KEY_BUFFER_SIZE];
    int total_dec=1000;
    unsigned char * buffer = 0;
    long length;
    unsigned char msg_decrypted[KEY_LEN];


    FILE * fp2 = fopen ("msg.enc", "rb");
    int size1=KEY_BUFFER_SIZE;
    if(fp2){
        while(size1>0){
            fread(private_encrypt,1,sizeof (private_encrypt),fp2);
            size1=size1-1;
        }
    }
    fclose(fp2);

    FILE * fp = fopen ("rsa_priv.txt", "rb");

    if (fp){
        fseek (fp, 0, SEEK_END);
        length = ftell (fp);
        fseek (fp, 0, SEEK_SET);
        buffer = calloc (1,length+1);
        if (buffer){
            fread (buffer, 1, length, fp);
        }
        fclose (fp);
    }

    // initialize rsaContext
    rsa_context rsaContext;
    rsa_init(&rsaContext,RSA_PKCS_V15, 0);
    rsaContext.len=KEY_LEN;

    // spliting keys and load into rsa context
    const char s[3] = "= ";
    char *token;
    int k=0, size;

    // get the first token
    token = strtok(buffer, s);

    // walk through other tokens
    while( token != NULL ) {
        size = strlen(token);

        switch (k) {
            case 1:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.N, 16, token);
                break;

            case 3:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.E, 16, token);
                break;

            case 5:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.D, 16, token);
                break;

            case 7:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.P, 16, token);
                break;

            case 9:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.Q, 16, token);
                break;

            case 11:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.DP, 16, token);
                break;

            case 13:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.DQ, 16, token);
                break;

            case 15:
                token[size-1]='\0';
                mpi_read_string(&rsaContext.QP, 16, token);
                break;
        }
        k=k+1;
        token = strtok(NULL, "= \n");
    }
    printf("Here\n");

    while (total_dec>=0) {
        if( rsa_private(&rsaContext,private_encrypt, msg_decrypted) != 0 ) {
            printf( "Decryption failed! %d\n", rsa_private(&rsaContext,private_encrypt, msg_decrypted));
        }else{
            printf("%d Decrypted plaintext-----> %s\n",total_dec, msg_decrypted );
        }
        total_dec--;
    }
    
}


int main(){ 
    int i;
    clock_t t;

    // total number of thread
    pthread_t ths[NUM_OF_THREAD];

    // start time count
    t = clock();

    for (i = 0; i < NUM_OF_THREAD; i++) {
        pthread_create(&ths[i], NULL, thread_function, NULL);
    }

    for (i = 0; i < NUM_OF_THREAD; i++) {
        void* res;
        pthread_join(ths[i], &res);
    }

    // end time count
    t = clock() - t;
    double time_taken = ((double)t)/CLOCKS_PER_SEC; // in seconds
    printf("Took %f seconds to execute \n", time_taken);
    printf("Total Cycle %f  \n", t);
    return 0;
}

When I run the code with a single thread, the code works fine and produces desirable results. If I set NUM_OF_THREAD to different values, my program starts acting weird. Some times all the thread created and works perfectly, sometimes threads are created but get segmentation fault after that. Sometimes a couple of threads work, a couple of threads don't.

Error type: 1 (set NUM_OF_THREAD 8), output:

So, I try to run the code with 8 threads. Different attempts give me different results

$./multiThread 
Test thread
Test thread
Test thread
Test thread
Test thread
Test thread
Test thread
Here
Here
Here
Here
Segmentation fault (core dumped)

Error type: 2 (set NUM_OF_THREAD 8), output:

$./multiThread 
Test thread
Test thread
Test thread
Test thread
Test thread
Here
Here
Test thread
Here
Here
Here
Decryption failed! -17156
Decryption failed! -17156
Segmentation fault (core dumped)

Error type: 3 (set NUM_OF_THREAD 8), output:

In this trial, a couple of thread fails but some of them produce the desire results.

$./multiThread
 Decryption failed! -17156
 Decryption failed! -17156
 .
 .
 .
 .
3 Decrypted plaintext-----> xxxxxxxxxxx

2 Decrypted plaintext-----> xxxxxxxxxxx

2 Decrypted plaintext-----> xxxxxxxxxxx

1 Decrypted plaintext-----> xxxxxxxxxxx

1 Decrypted plaintext-----> xxxxxxxxxxx

0 Decrypted plaintext-----> xxxxxxxxxxx

0 Decrypted plaintext-----> xxxxxxxxxxx

Took 174.413690 seconds to execute 
Total Cycle 174.413690  

My hunch, the problem is coming from rsa_private(). This is declared in #include "rsa/rsa.h". Is this considered a shared resource? If I'm not correct, can someone please tell me what I'm doing wrong here? What Can I do to fix this issue? I do not want to use a lock since it causes my code to slow down.

My CPU :Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz, Quad Core, 2 thread per core.

user45698746
  • 305
  • 2
  • 13
  • You need to check the documentation of `rsa_private()` to find out if it's async-safe. If not, you should protect it with a mutex. – Barmar Jul 15 '21 at 16:15
  • 2
    Intuitively I'd expect it to be, since it has the `context` argument, and each thread has its own context. – Barmar Jul 15 '21 at 16:16
  • This probably isn't directly causing a problem (yet...), but your error checking on your file open calls and your read calls is sorely lacking. If any of your IO calls fail, or even don't return as many bytes as you expect, your processing will just continue processing garbage. – Andrew Henle Jul 15 '21 at 17:05
  • @Barmar Also, forcing RSA decryption to be single-threaded borders on sadistic. This looks like mbed TLS, which is a respectable non-sadistic library from what I know of it. – Andrew Henle Jul 15 '21 at 17:09

2 Answers2

2

strtok() is not thread safe because it uses a static buffer internally. Use strtok_r() instead.

From the man page

   ┌────────────────────────┬───────────────┬───────────────────────┐
   │Interface               │ Attribute     │ Value                 │
   ├────────────────────────┼───────────────┼───────────────────────┤
   │strtok()                │ Thread safety │ MT-Unsafe race:strtok │
   ├────────────────────────┼───────────────┼───────────────────────┤
   │strtok_r()              │ Thread safety │ MT-Safe               │
   └────────────────────────┴───────────────┴───────────────────────┘
Ingo Leonhardt
  • 9,435
  • 2
  • 24
  • 33
2

Your theory is correct, but strtok() doesn't respect your assumptions, strtok_r() is the function that does that.

The strtok() function is not MT-safe, meaning you should not be using it in a multi-threaded environment. The reentrant (and therefore MT-safe) version of strtok is strtok_r and that's the one you should be using.

Manual for strtok_r here.

strtok stores its state using static (which is never MT-safe) and updates that state unsynchronized. This makes it critically exposed to race conditions when used in multi-threaded applications and results in unpredicted behavior if you do.

strtok_r saves its state locally (hence the additional saveptr parameter), making it reentrant and MT-safe implicitly.

If you want to see for yourself, check how strtok is implemented here:

char *
strtok (char *s, const char *delim)
{
  static char *olds;
  return __strtok_r (s, delim, &olds);
}
AlexM
  • 154
  • 5