diff --git a/internal/api/lib.go b/internal/api/lib.go index 3cdf4a0c1..e7f685eb4 100644 --- a/internal/api/lib.go +++ b/internal/api/lib.go @@ -6,6 +6,7 @@ import "C" import ( "fmt" + "os" "runtime" "strings" "syscall" @@ -32,12 +33,32 @@ type ( ) type Cache struct { - ptr *C.cache_t + ptr *C.cache_t + lockfile os.File } type Querier = types.Querier func InitCache(dataDir string, supportedCapabilities []string, cacheSize uint32, instanceMemoryLimit uint32) (Cache, error) { + err := os.MkdirAll(dataDir, 0755) + if err != nil { + return Cache{}, fmt.Errorf("Could not create base directory") + } + + lockfile, err := os.OpenFile(dataDir+"/exclusive.lock", os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return Cache{}, fmt.Errorf("Could not open exclusive.lock") + } + _, err = lockfile.WriteString("This is a lockfile that prevent two VM instances to operate on the same directory in parallel.\nSee codebase at github.com/CosmWasm/wasmvm for more information.\nSafety first – brought to you by Confio ❤️\n") + if err != nil { + return Cache{}, fmt.Errorf("Error writing to exclusive.lock") + } + + err = syscall.Flock(int(lockfile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if err != nil { + return Cache{}, fmt.Errorf("Could not lock exclusive.lock. Is a different VM running in the same directory already?") + } + dataDirBytes := []byte(dataDir) supportedCapabilitiesBytes := []byte(strings.Join(supportedCapabilities, ",")) @@ -52,11 +73,15 @@ func InitCache(dataDir string, supportedCapabilities []string, cacheSize uint32, if err != nil { return Cache{}, errorWithMessage(err, errmsg) } - return Cache{ptr: ptr}, nil + return Cache{ptr: ptr, lockfile: *lockfile}, nil } func ReleaseCache(cache Cache) { C.release_cache(cache.ptr) + + // Release directory lock + syscall.Flock(int(cache.lockfile.Fd()), syscall.LOCK_UN) + cache.lockfile.Close() } func StoreCode(cache Cache, wasm []byte) ([]byte, error) { diff --git a/internal/api/lib_test.go b/internal/api/lib_test.go index c08d9701d..251b173e5 100644 --- a/internal/api/lib_test.go +++ b/internal/api/lib_test.go @@ -55,7 +55,47 @@ func TestInitCacheErrorsForBrokenDir(t *testing.T) { // On Unix we should not have permission to create this. cannotBeCreated := "/foo:bar" _, err := InitCache(cannotBeCreated, TESTING_CAPABILITIES, TESTING_CACHE_SIZE, TESTING_MEMORY_LIMIT) - require.ErrorContains(t, err, "Error creating state directory") + require.ErrorContains(t, err, "Could not create base directory") +} + +func TestInitLockingPreventsConcurrentAccess(t *testing.T) { + tmpdir, err := os.MkdirTemp("", "wasmvm-testing") + require.NoError(t, err) + defer os.RemoveAll(tmpdir) + + cache1, err1 := InitCache(tmpdir, TESTING_CAPABILITIES, TESTING_CACHE_SIZE, TESTING_MEMORY_LIMIT) + require.NoError(t, err1) + + _, err2 := InitCache(tmpdir, TESTING_CAPABILITIES, TESTING_CACHE_SIZE, TESTING_MEMORY_LIMIT) + require.ErrorContains(t, err2, "Could not lock exclusive.lock") + + ReleaseCache(cache1) + + // Now we can try again + cache3, err3 := InitCache(tmpdir, TESTING_CAPABILITIES, TESTING_CACHE_SIZE, TESTING_MEMORY_LIMIT) + require.NoError(t, err3) + ReleaseCache(cache3) +} + +func TestInitLockingAllowsMultipleInstancesInDifferentDirs(t *testing.T) { + tmpdir1, err := os.MkdirTemp("", "wasmvm-testing1") + tmpdir2, err := os.MkdirTemp("", "wasmvm-testing2") + tmpdir3, err := os.MkdirTemp("", "wasmvm-testing3") + require.NoError(t, err) + defer os.RemoveAll(tmpdir1) + defer os.RemoveAll(tmpdir2) + defer os.RemoveAll(tmpdir3) + + cache1, err1 := InitCache(tmpdir1, TESTING_CAPABILITIES, TESTING_CACHE_SIZE, TESTING_MEMORY_LIMIT) + require.NoError(t, err1) + cache2, err2 := InitCache(tmpdir2, TESTING_CAPABILITIES, TESTING_CACHE_SIZE, TESTING_MEMORY_LIMIT) + require.NoError(t, err2) + cache3, err3 := InitCache(tmpdir3, TESTING_CAPABILITIES, TESTING_CACHE_SIZE, TESTING_MEMORY_LIMIT) + require.NoError(t, err3) + + ReleaseCache(cache1) + ReleaseCache(cache2) + ReleaseCache(cache3) } func TestInitCacheEmptyCapabilities(t *testing.T) {