C SIGSEGV on pthread_mutex_trylock when compiling for FreeBSD

Hi, i've recently installed FreeBSD on a VM so I could test and port my (Linux/Windows) C program to FreeBSD as needed.

After a few #define tweaks it compiled just fine but it keeps getting a segfault just as it begins the multithreaded part of the codepath.

After investigating it a bit with a debugger, right after it spawns the first thread it gives me this output (GF2):
Code:
Thread 2 received signal SIGSEGV, Segmentation fault.
Invalid permissions for mapped object.
[Switching to LWP 103304 of process 1941]
__Tthr_mutex_trylock (mutex=0x800803278)
    at /usr/src/lib/libthr/thread/thr_mutex.c:625

Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
[Switching to LWP 101079 of process 1941]
0x0000000000205399 in convertFiles (files=0x80067ee40, args=0x80067d000, 
    stats=0x7fffffffe230) at ./src/convert.c:141

I then tried looking for problems with the stage where we link with pthreads and also changed some permissions when creating the general mutex and condition attributes, but unfortunately saw no change in the end.

Anyways, here's the relevant source file and build script for the program (the relevant functions are convertFiles() and _callFFmpeg()).

convert.c:

C:
#include <convert.h>

extern Arena *globalArena;

static __mt_call_conv _callFFmpeg(void *arg);
static bool _fileExists(const char *fileName);
static int _formatOutputFileName(
    char *name, const char *outFormat, const char *path
);
static inline void _updateProgBar(Arguments *args, ProcessInfo *stats);
static inline void _clearProgBar(void);

#ifndef _WIN32
static inline void _threadAttrInit(pthread_attr_t *attr);
#endif

int convertFiles(const char **files, Arguments *args, ProcessInfo *stats) {
#ifndef _WIN32
    pthread_attr_t attr;
    _threadAttrInit(&attr);
#endif

    char *outPath = NULL;
    size_t numberOfThreads = args->numberOfThreads ?
        args->numberOfThreads : getNumberOfOnlineThreads();
    Thread *threads = GlobalArenaPush(numberOfThreads * sizeof(*threads));
    size_t fileIdx = 0;

    do {
        for (size_t i = 0; i < numberOfThreads && files[fileIdx]; i++) {
            if (threads[i].handle) continue; // thread is busy (kiwi business)

            _updateProgBar(args, stats);

            const char *inputFormat = NULL;

            for (int f = 0; args->inFormats[f]; f++) {
                if (strstr(files[fileIdx], args->inFormats[f])) {
                    inputFormat = args->inFormats[f];
                    break;
                }
            }

            assert(inputFormat);

            const char *fullPath = files[fileIdx];
            const char *pathDelimPoint = (fullPath + strlen(fullPath));

            while (*--pathDelimPoint != PATH_SEP);

            char *filePath = GlobalArenaPushStringN(
                fullPath, (pathDelimPoint - fullPath)
            );
            char *baseName = GlobalArenaPushString(pathDelimPoint + 1);

            assert(filePath);
            assert(baseName);

            outPath = filePath;

            const char *newFolderName = (args->options & OPT_CUSTOMFOLDERNAME) ?
                args->outPath.customFolder : args->outFormat;

            if (args->options & OPT_NEWFOLDER || args->options & OPT_NEWPATH) {
                char *newPath =
                    args->options & OPT_NEWFOLDER ? GlobalArenaSprintf(
                        "%s%c%s", filePath, PATH_SEP, newFolderName
                    ) : args->outPath.customPath;

                if (mkdir(newPath, S_IRWXU) != EXIT_SUCCESS && errno != EEXIST) {
                    printErr("unable to create subdirectory", strerror(errno));
                    return EXIT_FAILURE;
                }

                outPath = newPath;
            }

            char *fileNameNoExt = GlobalArenaPushStringN(
                baseName, strlen(baseName) - strlen(inputFormat) - 1
            );

            const char *overwriteFlag = "-n";

            if (args->options & OPT_OVERWRITE) {
                overwriteFlag = "-y";
            } else {
                _formatOutputFileName(fileNameNoExt, args->outFormat, outPath);
            }

            char *fullOutPath = GlobalArenaSprintf(
                "%s%c%s.%s", outPath, PATH_SEP, fileNameNoExt, args->outFormat
            );

            char *ffmpegCall = GlobalArenaSprintf(
                "ffmpeg -nostdin -hide_banner -loglevel error "
                "%s -i \"%s\" %s \"%s\"",
                overwriteFlag, fullPath, args->ffOptions, fullOutPath
            );

            threads[i].callArg = ffmpegCall;
            threads[i].targetFile = trimUTF8StringTo(
                fullPath + PREFIX_LEN, LINE_LEN - 36
            );
            threads[i].outFileID = fileIdx + 1;

#ifdef _WIN32
            int callBuf = UTF8toUTF16(ffmpegCall, -1, NULL, 0);
            wchar_t *ffmpegCallW = GlobalArenaPush(callBuf * sizeof(wchar_t));
            UTF8toUTF16(ffmpegCall, -1, ffmpegCallW, callBuf);

            threads[i].callArg = ffmpegCallW;
            threads[i].handle = (HANDLE)_beginthreadex(
                NULL, 0, &_callFFmpeg, &threads[i], 0, NULL
            );

            if (!threads[i].handle) {
                printErr("unable to spawn new thread", strerror(errno));
                exit(errno);
            }

#else
            pthread_mutex_init(&threads[i].mutex, NULL);
            pthread_cond_init(&threads[i].cond, NULL);

            int err = pthread_create(
                &threads[i].handle, &attr, &_callFFmpeg, &threads[i]
            );

            if (err) {
                printErr("unable to spawn new thread", strerror(err));
                exit(err);
            }
#endif

            fileIdx += 1;
        }

        for (size_t i = 0; i < numberOfThreads; i++) {
            if (!threads[i].handle) continue;

#ifdef _WIN32
            if (
                WaitForSingleObject(threads[i].handle, TIMEOUT_MS) ==
                WAIT_TIMEOUT
            ) continue;

            CloseHandle(threads[i].handle);
#else
            struct timespec time;
            clock_gettime(CLOCK_REALTIME, &time);
            time.tv_nsec += (TIMEOUT_MS * 1e6);

            /* NOTE: making sure tv_nsec doesn't exceed it's maximum value */
            if (time.tv_nsec > TV_NSEC_MAX) time.tv_nsec = TV_NSEC_MAX;

            int wait = pthread_cond_timedwait(
                &threads[i].cond, &threads[i].mutex, &time
            );

            if (wait == ETIMEDOUT) {
                continue;
            }

            int err = pthread_join(threads[i].handle, NULL);

            if (err) {
                printErr("unable to join threads", strerror(err));
                exit(err);
            }

            if ((err = pthread_cond_destroy(&threads[i].cond))) {
                printErr("unable to destroy condition", strerror(err));
                abort();
            }

            if ((err = pthread_mutex_destroy(&threads[i].mutex))) {
                printErr("unable to destroy mutex", strerror(err));
                abort();
            }
#endif

            stats->convertedFiles += 1;

            _updateProgBar(args, stats);

            memset(&threads[i], 0, sizeof(*threads));
        }
    } while (
        !isZeroMemory(threads, numberOfThreads * sizeof(*threads)) ||
        files[fileIdx]
    );

    putchar('\n');

    for (int i = 0; files[i]; i++) {
        if (args->options & OPT_CLEANUP) {
            if (remove(files[i])) {
                printErr("unable to delete original file", strerror(errno));
            } else {
                stats->deletedFiles += 1;
            }
        }
    }

    /* Delete new folder in case it exists and no conversions succeeded */
    if (
        ((args->options & OPT_NEWFOLDER) || (args->options & OPT_NEWPATH)) &&
        stats->convertedFiles == 0
    ) {
        if (remove(outPath) != 0) {
            printErr("unable to delete unused directory", strerror(errno));
        }
    }

    return 0;
}

static __mt_call_conv _callFFmpeg(void *arg) {
    Thread *thread = (Thread*)arg;

#ifdef _WIN32
    wchar_t *ffmpegCallW = thread->callArg;
    STARTUPINFOW ffmpegStartupInfo = {0};
    PROCESS_INFORMATION ffmpegProcessInfo = {0};

    bool createdProcess = CreateProcessW(
        NULL, ffmpegCallW, NULL, NULL, FALSE, 0, NULL, NULL,
        &ffmpegStartupInfo, &ffmpegProcessInfo
    );

    if (!createdProcess) {
        printWinErrMsg("unable to call FFmpeg", GetLastError());
         _endthreadex(-1);
        return -1;
    } else {
        WaitForSingleObject(ffmpegProcessInfo.hProcess, INFINITE);
        CloseHandle(ffmpegProcessInfo.hProcess);
        CloseHandle(ffmpegProcessInfo.hThread);
    }

    _endthreadex(0);
    return 0;
#else
    const char *ffmpegCall = thread->callArg;

    pid_t procId = fork();

    if (procId == 0) {
        /* NOTE: using system() for now cause i'm too lazy
         * to build a dynamic array of args due to ffOptions
         * having to be popped from the array in case it's
         * an empty string (._.)  */
        int err = system(ffmpegCall);
        exit(err);
    } else {
        int status;
        waitpid(procId, &status, 0);

        int exitStatus = 0;

        if (!WIFEXITED(status))
            exitStatus = WEXITSTATUS(status);

        if (exitStatus != 0) {
            char status[FILE_BUF];
            snprintf(status, FILE_BUF, "exit status: %d", exitStatus);
            printErr("unable to call FFmpeg", status);
            exit(exitStatus);
        }
    }

    static struct timespec timeout = { .tv_nsec = TIMEOUT_MS * 1e7 / 2 };

    while (pthread_mutex_trylock(&thread->mutex) == EBUSY)
        nanosleep(&timeout, NULL);

    pthread_cond_broadcast(&thread->cond);
    pthread_mutex_unlock(&thread->mutex);

    return NULL;
#endif
}

static int _formatOutputFileName(
    char *name, const char *format, const char *path
) {
    size_t fullPathSize =
        snprintf(NULL, 0, "%s%c%s.-xxx%s", path, PATH_SEP, name, format) + 1;

    char *fullPath = GlobalArenaPush(fullPathSize * sizeof(char));
    sprintf(fullPath, "%s%c%s.%s", path, PATH_SEP, name, format);

    char newName[FILE_BUF];

    if (_fileExists(fullPath)) {
        size_t index = 0;

        while (_fileExists(fullPath)) {
            sprintf(
                fullPath, "%s%c%s-%03zu.%s", path,
                PATH_SEP, name, ++index, format
            );
        }

        snprintf(newName, FILE_BUF, "%s-%03zu", name, index);
        strncpy(name, newName, FILE_BUF);
    }

    return EXIT_SUCCESS;
}

static bool _fileExists(const char *fileName) {
#ifdef _WIN32
    int len = UTF8toUTF16(fileName, -1, NULL, 0);
    wchar_t *fileNameW = GlobalArenaPush(len * sizeof(wchar_t));
    UTF8toUTF16(fileName, -1, fileNameW, len);

    WIN32_FIND_DATAW fileData;
    return FindFirstFileW(fileNameW, &fileData) != INVALID_HANDLE_VALUE;
#else /* POSIX */
    struct stat statBuffer;
    return stat(fileName, &statBuffer) == 0;
#endif
}

#define PROGBAR_LINES 1
#define BAR_LEN (LINE_LEN - 21)

enum ProgBarStatus {
    CLEARED,
    VISIBLE
} progBarStatus = CLEARED;

/* NOTE: progress bar/stats printing function (style subject to change) */
static inline void _updateProgBar(Arguments *args, ProcessInfo *stats) {
    (void)args; // will use these later

    if (progBarStatus == VISIBLE) _clearProgBar();

    char progBar[LINE_LEN * PROGBAR_LINES] = {0};
    size_t progBarIdx = 0;

    progBarIdx += sprintf(
        progBar, " Progress: [%s", COLOR_ACCENT
    );

    size_t fill = (BAR_LEN * stats->convertedFiles) / stats->totalFiles;

    for (size_t i = 0; i < BAR_LEN; i++) {
        if (i < fill) {
            progBar[progBarIdx++] = '#';
        } else {
            progBar[progBarIdx++] = ' ';
        }
    }

    sprintf(
        (progBar + progBarIdx),
        "%s] %6.2f%%", COLOR_DEFAULT,
        (double)(100.0F * stats->convertedFiles / stats->totalFiles)
    );

    puts(progBar);
    progBarStatus = VISIBLE;
}

static inline void _clearProgBar(void) {
    for (size_t i = 0; i < PROGBAR_LINES; i++)
        printf(LINE_ERASE LINE_MOVE_UP);

    printf(LINE_ERASE CARRIAGE_RET);

    progBarStatus = CLEARED;
}

#ifndef _WIN32
static inline void _threadAttrInit(pthread_attr_t *attr) {
    int err = 0;

    if ((err = pthread_attr_init(attr))) {
        printErr("unable to initialize thread attributes", strerror(err));
        exit(err);
    }
    if ((err = pthread_attr_setstacksize(attr, PTHREAD_STACK_MIN))) {
        printErr("unable to set threads' stack size", strerror(err));
        exit(err);
    }
}
#endif

build.sh:

sh:
#!/bin/sh
# build script for POSIX systems (Linux/MacOS/FreeBSD)

DEFINES=" -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE"
  FLAGS="-I./lib -std=c99 -lpthread $DEFINES -Wall -Wextra -pedantic -Wno-unused-function"
D_FLAGS="-g"
P_FLAGS="-finstrument-functions -ldl -rdynamic -DINSTRUMENTATION"
R_FLAGS="-DNDEBUG -O2 -s"

files=""
   CC="clang"


for f in $(find . -name "*.c"); do
    files="$files $f";
done

mkdir -p "./bin"

if [ "$1" == "rel" ]; then
    echo "Building rffmpeg in RELEASE mode..."
    echo
    set -x
    $CC -o "bin/rffmpeg" $files $FLAGS $R_FLAGS
elif [ "$1" == "prof" ]; then
    echo "Building rffmpeg in PROFILING mode..."
    echo
    set -x
    $CC -o "bin/rffmpeg" $files $FLAGS $D_FLAGS $P_FLAGS
else
    echo "Building rffmpeg in DEBUG mode..."
    echo
    set -x
    $CC -o "bin/rffmpeg" $files $FLAGS $D_FLAGS
fi

exit 0

I'd be really grateful if someone has some insight on this because I'm honestly at a loss as to what's going wrong here...
 
If you are both compiling and linking files I would use -pthread rather than -lpthread.

You don't check whether your fork is failing or not.

You don't show how _callFFmpeg gets called so we can't see if it is receiving a well-formed argument.

You should be able to see more in lldb or gdb. Sanitizers and Valgrind might help if the source of the error is far from the crash.
 
If you are both compiling and linking files I would use -pthread rather than -lpthread.

You don't check whether your fork is failing or not.

You don't show how _callFFmpeg gets called so we can't see if it is receiving a well-formed argument.

You should be able to see more in lldb or gdb. Sanitizers and Valgrind might help if the source of the error is far from the crash.

So I did try switching to -pthread in the meantime but it made no difference.

The fork and well-formed argument parts are pretty unlikely since they run perfectly fine on Linux already.

I did actually use gdb to get that error logging output I pasted in the post (GF2 is a gdb frontend), but that's as much as I could get from it.

Here's a link to the latest branch of the repository though, it's not a lot of actual code: https://github.com/cyanide0081/rffmpeg/tree/progbar
 
Back
Top