Search code examples
gofficgo

Segmentation violation error when calling fts_open via cgo


I'm testing cgo and every simple hello world like code works well.
but i have a problem with C code below.
The C code is that traverse a directory tree and sums file size.
if i build with go command, then the build is OK with no error.
but when running, there is a "segmentation violation" error occurred

bash$./walkdir 
fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x1 addr=0x1 pc=0x7f631e077c1a]
. . . .

-------------------------------------------------------------

package main
/*
#include <stdint.h>
#include <fts.h>
#include <sys/stat.h>

uintmax_t get_total_size(char *path)
{
    uintmax_t total_size = 0;
    FTS *fts = fts_open(&path, FTS_PHYSICAL, NULL);
    FTSENT *fent;
    while ((fent = fts_read(fts)) != NULL)
        if (fent->fts_info == FTS_F)
            total_size += fent->fts_statp->st_size;
    fts_close(fts);
    return total_size;
}
*/
import "C"
import "fmt"

func main() {
    fmt.Println(C.get_total_size(C.CString("/usr")))
}

Solution

  • fts_open is defined like this:

    fts_open()
    The fts_open() function takes a pointer to an array of character pointers naming one or more paths which make up a logical file hierarchy to be traversed. The array must be terminated by a null pointer.

    C does not have direct support for arrays; it only has pointers. In your case you pass fts_open a single valid pointer but it is not located in an array which has a NULL pointer as the immediately following element, so fts_open continues to scan the memory past &path — looking for a NULL pointer, — and eventually tries to read memory at some address it is forbidden to do so (usually because the page at that address was not allocated).

    A way to fix it is to create that array and initialize it on the C side.
    Looks like you're using a reasonably up-to-date standard of C, so let's just use direct literal to initialize the array:

    package main
    
    /*
    #include <stddef.h> // for NULL
    #include <stdint.h>
    #include <stdlib.h> // for C.free
    #include <fts.h>
    #include <sys/stat.h>
    
    uintmax_t get_total_size(char *path)
    {
        uintmax_t total_size = 0;
        char * path_argv[2] = {path, NULL};
        FTS *fts = fts_open(path_argv, FTS_PHYSICAL, NULL);
        FTSENT *fent;
        while ((fent = fts_read(fts)) != NULL)
            if (fent->fts_info == FTS_F)
                total_size += fent->fts_statp->st_size;
        fts_close(fts);
        return total_size;
    }
    */
    import "C"
    
    import (
        "fmt"
        "unsafe"
    )
    
    func main() {
        cpath := C.CString("/usr")
        defer C.free(unsafe.Pointer(cpath))
        fmt.Println(C.get_total_size(cpath))
    }
    

    Note that your program has one bug and one possible problem:

    • A bug is that the call C.CString allocates a chunk of memory by performing a call to malloc(3) from the linked C library, and you did not free that memory block.
    • The symbol NULL is defined in "stddef.h"; you might or might not get an error when compiling.

    I've fixed both problems in my example.

    A further improvement over our example might be leveraging the ability of fts_* functions to scan multiple paths in a single run; if we were to implement that, it would have more sense to allocate the array for the 1st argument of fts_open on the Go's side:

    package main
    
    /*
    #include <stddef.h>
    #include <stdint.h>
    #include <stdlib.h>
    #include <fts.h>
    #include <sys/stat.h>
    
    uintmax_t get_total_size(char * const *path_argv)
    {
        uintmax_t total_size = 0;
        FTS *fts = fts_open(path_argv, FTS_PHYSICAL, NULL);
        FTSENT *fent;
        while ((fent = fts_read(fts)) != NULL)
            if (fent->fts_info == FTS_F)
                total_size += fent->fts_statp->st_size;
        fts_close(fts);
        return total_size;
    }
    */
    import "C"
    import (
        "fmt"
        "unsafe"
    )
    
    func main() {
        fmt.Println(getTotalSize("/usr", "/etc"))
    }
    
    func getTotalSize(paths ...string) uint64 {
        argv := make([]*C.char, len(paths)+1)
        for i, path := range paths {
            argv[i] = C.CString(path)
            defer C.free(unsafe.Pointer(argv[i]))
        }
    
        return uint64(C.get_total_size(&argv[0]))
    }
    

    Note that here we did not explicitly zero out the last argument of argv because — contrary to C, — Go initializes each allocated memory block with zeroes, so once argv is allocated, all its memory is already zeroed.