その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

CのShared LibraryにしたGoのコードで簡単にダングリングポインタを発生させる

例えば以下のようなGoのコードを書き、

package main

import "C"
import (
	"fmt"
)

//export DoSomething
func DoSomething(cstr *C.char) {
	fmt.Printf("Go: %s\n", C.GoString(cstr))
}

func main() {
}

go build -buildmode c-shared -o libdosomething.so ./ としてCのShared Libraryを生成して、以下のようにCのプログラムに組み込みます。

#include <stdio.h>
#include <stdlib.h>
#include "libdosomething.h"

int main() {
    char *str = (char *)malloc(sizeof(char) * 4);
    str[0]='f';
    str[1]='o';
    str[2]='o';
    str[3]='\0';

    printf("C: %s\n", str);
    DoSomething(str);

    free(str);

    return 0;
}

そして gcc -L . main.c -ldosomething とこのCのコードをコンパイルし、LD_LIBRARY_PATH=. ./a.out のように実行すると

C: foo
Go: foo

という風に、もちろんこのコードは期待通りに動作します。

さて、Go側の DoSomething() の処理をgoroutineを使うように変更してみましょう。

package main

import "C"
import (
	"fmt"
	"time"
)

//export DoSomething
func DoSomething(cstr *C.char) {
	fmt.Println("Go: DoSomething is called")
	go func() {
		time.Sleep(3 * time.Second)
		fmt.Printf("Go: %s\n", C.GoString(cstr))
	}()
}

func main() {
}

そして説明のためにCのコードも以下のようにします。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "libdosomething.h"

int main() {
    char *str = (char *)malloc(sizeof(char) * 4);
    str[0]='f';
    str[1]='o';
    str[2]='o';
    str[3]='\0';

    printf("C: %s\n", str);
    DoSomething(str);

    free(str);

    printf("C: C string has been freed and it's sleeping...\n");

    sleep(5);

    return 0;
}

そして先程と同じようにCのプログラムをコンパイルして実行すると、

C: foo
Go: DoSomething is called
C: C string has been freed and it's sleeping...
Go: 3/_

という風に、めでたくGoの C.GoString(cstr) がダングリングポインタを参照したおかげでおかしな文字列が出力されました。

理由としてはおわかりのように単純で、goroutineの中でsleep (これは何らかの処理を模しています) している最中にGoの関数の引数として渡されたCの char *str がCの側で解放されてしまったため、いざsleepから目覚めてgoroutineの中で C.GoString(cstr) を実行するとダングリングポインタを参照してしまう、ということになります。なまじ変数をGoの世界に引き込んだので油断してしまい、Goの関数に渡された時点でGo世界にallocateされてcopyされているだろうとタカをくくっていたところそんなはずは無く、そりゃ *C.char なんだから明らかにそうなんですが、まあ普通にpointerでしたね......という感じです。

これを避けるためには以下のように同期的に (すなわちgoroutineの外で) Goの文字列にしてしまえば良いでしょう。goroutineの引数としてキャプチャしてしまっても良いと思います。

func DoSomething(cstr *C.char) {
	gostr := C.GoString(cstr)
	go func() {
		time.Sleep(3 * time.Second)
		fmt.Printf("Go %s\n", gostr)
	}()
}


こうして見ると原因は単純ですし、この例のようにsleepを使った露骨なコードだとわかりやすいのですが、実際のコードでこういう事が起こるとなかなかわかりにくいですね。ハマりました。疲れますね。