diff --git a/.github/workflows/run_sanitizers_leak.suppressions b/.github/workflows/run_sanitizers_leak.suppressions new file mode 100644 index 00000000000000..49ef9963d5dfbb --- /dev/null +++ b/.github/workflows/run_sanitizers_leak.suppressions @@ -0,0 +1,13 @@ +# websocket tests +leak:mbedtls_ssl_setup + +# sqlite tests +leak:*sqlite* + +# v +leak:v__* +leak:os__* +leak:string__plus +leak:malloc_uncollectable +leak:new_array_from_c_array +leak:_vinit diff --git a/.github/workflows/sanitized_ci.yml b/.github/workflows/sanitized_ci.yml index 19b416901b08cd..a69e6490b755a2 100644 --- a/.github/workflows/sanitized_ci.yml +++ b/.github/workflows/sanitized_ci.yml @@ -75,7 +75,7 @@ concurrency: jobs: tests-sanitize-undefined-clang: runs-on: ubuntu-20.04 - timeout-minutes: 240 + timeout-minutes: 120 env: VFLAGS: -cc clang VJOBS: 1 @@ -101,7 +101,7 @@ jobs: tests-sanitize-undefined-gcc: runs-on: ubuntu-20.04 - timeout-minutes: 240 + timeout-minutes: 120 env: VFLAGS: -cc gcc VJOBS: 1 @@ -126,7 +126,7 @@ jobs: tests-sanitize-address-clang: runs-on: ubuntu-20.04 - timeout-minutes: 240 + timeout-minutes: 300 env: VFLAGS: -cc clang VJOBS: 1 @@ -146,16 +146,16 @@ jobs: - name: Recompile V with -cstrict run: ./v -cg -cstrict -o v cmd/v - name: Self tests (-fsanitize=address) - run: ASAN_OPTIONS=detect_leaks=0 ./v -cflags "-fsanitize=address,pointer-compare,pointer-subtract" test-self vlib + run: ASAN_OPTIONS=detect_leaks=1:fast_unwind_on_malloc=0 LSAN_OPTIONS=max_leaks=1:print_suppressions=0:suppressions=.github/workflows/run_sanitizers_leak.suppressions ./v -cflags "-fsanitize=address,pointer-compare,pointer-subtract" test-self vlib - name: Self tests (V compiled with -fsanitize=address) run: ./v -cflags -fsanitize=address -o v cmd/v && - ASAN_OPTIONS=detect_leaks=0 ./v -cc tcc test-self -asan-compiler vlib + ASAN_OPTIONS=detect_leaks=1:fast_unwind_on_malloc=0 LSAN_OPTIONS=max_leaks=1:print_suppressions=0:suppressions=.github/workflows/run_sanitizers_leak.suppressions ./v -cc tcc test-self -asan-compiler vlib - name: Build examples (V compiled with -fsanitize=address) - run: ASAN_OPTIONS=detect_leaks=0 ./v build-examples + run: ASAN_OPTIONS=detect_leaks=1:fast_unwind_on_malloc=0 LSAN_OPTIONS=max_leaks=1:print_suppressions=0:suppressions=.github/workflows/run_sanitizers_leak.suppressions ./v build-examples tests-sanitize-address-msvc: runs-on: windows-2019 - timeout-minutes: 240 + timeout-minutes: 30 env: VFLAGS: -cc msvc VJOBS: 1 @@ -179,7 +179,7 @@ jobs: tests-sanitize-address-gcc: runs-on: ubuntu-20.04 - timeout-minutes: 240 + timeout-minutes: 300 env: VFLAGS: -cc gcc VJOBS: 1 @@ -199,13 +199,13 @@ jobs: - name: Recompile V with -cstrict run: ./v -cg -cstrict -o v cmd/v - name: Self tests (-fsanitize=address) - run: ASAN_OPTIONS=detect_leaks=0 ./v -cflags -fsanitize=address test-self vlib + run: ASAN_OPTIONS=detect_leaks=1:fast_unwind_on_malloc=0 LSAN_OPTIONS=max_leaks=1:print_suppressions=0:suppressions=.github/workflows/run_sanitizers_leak.suppressions ./v -cflags -fsanitize=address test-self vlib - name: Self tests (V compiled with -fsanitize=address) run: ./v -cflags -fsanitize=address,pointer-compare,pointer-subtract -o v cmd/v && - ASAN_OPTIONS=detect_leaks=0 ./v -cc tcc test-self -asan-compiler vlib + ASAN_OPTIONS=detect_leaks=1:fast_unwind_on_malloc=0 LSAN_OPTIONS=max_leaks=1:print_suppressions=0:suppressions=.github/workflows/run_sanitizers_leak.suppressions ./v -cc tcc test-self -asan-compiler vlib - name: Build examples (V compiled with -fsanitize=address) - run: ASAN_OPTIONS=detect_leaks=0 ./v build-examples + run: ASAN_OPTIONS=detect_leaks=1:fast_unwind_on_malloc=0 LSAN_OPTIONS=max_leaks=1:print_suppressions=0:suppressions=.github/workflows/run_sanitizers_leak.suppressions ./v build-examples tests-sanitize-memory-clang: runs-on: ubuntu-20.04 diff --git a/vlib/builtin/backtraces_nix.c.v b/vlib/builtin/backtraces_nix.c.v index 04ceb64c86348d..b3b2355cf731c2 100644 --- a/vlib/builtin/backtraces_nix.c.v +++ b/vlib/builtin/backtraces_nix.c.v @@ -106,6 +106,9 @@ fn print_backtrace_skipping_top_frames_linux(skipframes int) bool { output = output.replace(' (discriminator', ': (d.') eprintln('${output:-55s} | ${addr:14s} | ${beforeaddr}') } + if sframes.len > 0 { + unsafe { C.free(csymbols) } + } } } } diff --git a/vlib/compress/compress.c.v b/vlib/compress/compress.c.v index 8206874beb0fb3..a5f0ffe6ffe05e 100644 --- a/vlib/compress/compress.c.v +++ b/vlib/compress/compress.c.v @@ -24,7 +24,11 @@ pub fn compress(data []u8, flags int) ![]u8 { if u64(out_len) > max_size { return error('compressed data is too large (${out_len} > ${max_size})') } - return unsafe { address.vbytes(int(out_len)) } + unsafe { + ret := address.vbytes(int(out_len)).clone() + C.free(address) + return ret + } } // decompresses an array of bytes based on providing flags and returns the decompressed bytes in a new array @@ -40,5 +44,10 @@ pub fn decompress(data []u8, flags int) ![]u8 { if u64(out_len) > max_size { return error('decompressed data is too large (${out_len} > ${max_size})') } - return unsafe { address.vbytes(int(out_len)) } + + unsafe { + ret := address.vbytes(int(out_len)).clone() + C.free(address) + return ret + } } diff --git a/vlib/crypto/ecdsa/ecdsa.v b/vlib/crypto/ecdsa/ecdsa.v index 424fad19fbb6ea..ae90980c090c8b 100644 --- a/vlib/crypto/ecdsa/ecdsa.v +++ b/vlib/crypto/ecdsa/ecdsa.v @@ -227,3 +227,8 @@ pub fn (pub_key PublicKey) equal(other PublicKey) bool { res := C.EC_POINT_cmp(group, point1, point2, ctx) return res == 0 } + +// Clear allocated memory for key +pub fn key_free(ec_key &C.EC_KEY) { + C.EC_KEY_free(ec_key) +} diff --git a/vlib/crypto/ecdsa/ecdsa_test.v b/vlib/crypto/ecdsa/ecdsa_test.v index 94fb996c89dd20..18e3d7ab3cf214 100644 --- a/vlib/crypto/ecdsa/ecdsa_test.v +++ b/vlib/crypto/ecdsa/ecdsa_test.v @@ -12,6 +12,7 @@ fn test_ecdsa() { is_valid := pub_key.verify(message, signature) or { panic(err) } println('Signature valid: ${is_valid}') assert is_valid + key_free(priv_key.key) } fn test_generate_key() ! { @@ -19,6 +20,7 @@ fn test_generate_key() ! { pub_key, priv_key := generate_key() or { panic(err) } assert pub_key.key != unsafe { nil } assert priv_key.key != unsafe { nil } + key_free(priv_key.key) } fn test_new_key_from_seed() ! { @@ -27,6 +29,7 @@ fn test_new_key_from_seed() ! { priv_key := new_key_from_seed(seed) or { panic(err) } retrieved_seed := priv_key.seed() or { panic(err) } assert seed == retrieved_seed + key_free(priv_key.key) } fn test_sign_and_verify() ! { @@ -36,6 +39,7 @@ fn test_sign_and_verify() ! { signature := priv_key.sign(message) or { panic(err) } is_valid := pub_key.verify(message, signature) or { panic(err) } assert is_valid + key_free(priv_key.key) } fn test_seed() ! { @@ -43,6 +47,7 @@ fn test_seed() ! { _, priv_key := generate_key() or { panic(err) } seed := priv_key.seed() or { panic(err) } assert seed.len > 0 + key_free(priv_key.key) } fn test_public_key() ! { @@ -51,6 +56,9 @@ fn test_public_key() ! { pub_key1 := priv_key.public_key() or { panic(err) } pub_key2, _ := generate_key() or { panic(err) } assert !pub_key1.equal(pub_key2) + key_free(priv_key.key) + key_free(pub_key1.key) + key_free(pub_key2.key) } fn test_private_key_equal() ! { @@ -59,6 +67,8 @@ fn test_private_key_equal() ! { seed := priv_key1.seed() or { panic(err) } priv_key2 := new_key_from_seed(seed) or { panic(err) } assert priv_key1.equal(priv_key2) + key_free(priv_key1.key) + key_free(priv_key2.key) } fn test_public_key_equal() ! { @@ -67,6 +77,9 @@ fn test_public_key_equal() ! { pub_key1 := priv_key.public_key() or { panic(err) } pub_key2 := priv_key.public_key() or { panic(err) } assert pub_key1.equal(pub_key2) + key_free(priv_key.key) + key_free(pub_key1.key) + key_free(pub_key2.key) } fn test_sign_with_new_key_from_seed() ! { @@ -78,6 +91,8 @@ fn test_sign_with_new_key_from_seed() ! { pub_key := priv_key.public_key() or { panic(err) } is_valid := pub_key.verify(message, signature) or { panic(err) } assert is_valid + key_free(priv_key.key) + key_free(pub_key.key) } fn test_invalid_signature() ! { @@ -88,9 +103,11 @@ fn test_invalid_signature() ! { result := pub_key.verify(message, invalid_signature) or { // Expecting verification to fail assert err.msg() == 'Failed to verify signature' + key_free(pub_key.key) return } assert !result + key_free(pub_key.key) } fn test_different_keys_not_equal() ! { @@ -98,4 +115,6 @@ fn test_different_keys_not_equal() ! { _, priv_key1 := generate_key() or { panic(err) } _, priv_key2 := generate_key() or { panic(err) } assert !priv_key1.equal(priv_key2) + key_free(priv_key1.key) + key_free(priv_key2.key) } diff --git a/vlib/db/sqlite/parent_child_test.v b/vlib/db/sqlite/parent_child_test.v index 684808a5789d61..d164674704f6a2 100644 --- a/vlib/db/sqlite/parent_child_test.v +++ b/vlib/db/sqlite/parent_child_test.v @@ -20,7 +20,7 @@ struct Baby { } fn test_main() { - db := sqlite.connect(':memory:')! + mut db := sqlite.connect(':memory:')! sql db { create table Parent @@ -80,4 +80,6 @@ fn test_main() { assert parent[0].children[0].id == 1 assert parent[0].children[1].id == 2 assert parent[0].children.len == 2 + + db.close()! } diff --git a/vlib/db/sqlite/sqlite_comptime_field_test.v b/vlib/db/sqlite/sqlite_comptime_field_test.v index bbe5b33ae19770..eeb191d07c5322 100644 --- a/vlib/db/sqlite/sqlite_comptime_field_test.v +++ b/vlib/db/sqlite/sqlite_comptime_field_test.v @@ -22,7 +22,7 @@ fn records_by_field[T](db sqlite.DB, fieldname string, value string) ![]T { } fn test_main() { - db := sqlite.connect(':memory:')! + mut db := sqlite.connect(':memory:')! sql db { create table Blog }! @@ -40,4 +40,5 @@ fn test_main() { return } assert rows.len == 1 + db.close()! } diff --git a/vlib/db/sqlite/sqlite_test.v b/vlib/db/sqlite/sqlite_test.v index 7b334fd16f84bf..436ce50330ce0d 100644 --- a/vlib/db/sqlite/sqlite_test.v +++ b/vlib/db/sqlite/sqlite_test.v @@ -11,7 +11,7 @@ pub: type Content = []u8 | string struct Host { -pub: +pub mut: db Connection } @@ -92,7 +92,8 @@ fn test_can_access_sqlite_result_consts() { } fn test_alias_db() { - create_host(sqlite.connect(':memory:')!)! + mut host := create_host(sqlite.connect(':memory:')!)! + host.db.close()! assert true } @@ -110,7 +111,9 @@ fn test_exec_param_many() { 'Sam', ]) or { assert err.code() == 19 // constraint failure + db.close()! return } + db.close()! assert false } diff --git a/vlib/json/cjson/cjson_test.v b/vlib/json/cjson/cjson_test.v index f50c7131c87013..53e7e812a56716 100644 --- a/vlib/json/cjson/cjson_test.v +++ b/vlib/json/cjson/cjson_test.v @@ -5,6 +5,7 @@ fn test_object_with_null() { root.add_item_to_object('name', cjson.create_string('Andre')) root.add_item_to_object('age', cjson.create_null()) assert root.print_unformatted() == '{"name":"Andre","age":null}' + unsafe { cjson.delete(root) } } fn test_creating_complex_json() { @@ -18,4 +19,5 @@ fn test_creating_complex_json() { println(result) assert result == '["user",{"username":"foo","password":"bar"}]' + unsafe { cjson.delete(root) } } diff --git a/vlib/json/cjson/cjson_wrapper.c.v b/vlib/json/cjson/cjson_wrapper.c.v index 640dae27a18323..0650da1f61ab1b 100644 --- a/vlib/json/cjson/cjson_wrapper.c.v +++ b/vlib/json/cjson/cjson_wrapper.c.v @@ -82,6 +82,8 @@ fn C.cJSON_Print(object &C.cJSON) &char fn C.cJSON_PrintUnformatted(object &C.cJSON) &char +fn C.cJSON_free(voidptr) + // // version returns the version of cJSON as a string @@ -168,7 +170,9 @@ pub fn (mut obj Node) print() string { // print serialises the node to a string, without formatting its structure, so the resulting string is shorter/cheaper to transmit. pub fn (mut obj Node) print_unformatted() string { mut s := C.cJSON_PrintUnformatted(obj) - return unsafe { tos3(s) } + ret := unsafe { tos_clone(&u8(s)) } + C.cJSON_free(s) + return ret } // str returns the unformatted serialisation to string of the given Node. diff --git a/vlib/net/websocket/websocket_test.v b/vlib/net/websocket/websocket_test.v index dcd14ffd87f169..82930b8efce4b8 100644 --- a/vlib/net/websocket/websocket_test.v +++ b/vlib/net/websocket/websocket_test.v @@ -3,6 +3,7 @@ import net import net.websocket import time +@[heap] struct WebsocketTestResults { pub mut: nr_messages int diff --git a/vlib/v/tests/fns/fn_literal_type_test.v b/vlib/v/tests/fns/fn_literal_type_test.v index 54a7d4068370b9..10a9c0934725a3 100644 --- a/vlib/v/tests/fns/fn_literal_type_test.v +++ b/vlib/v/tests/fns/fn_literal_type_test.v @@ -10,7 +10,7 @@ const const_users_offset = 1 const const_users_offset2 = 1 fn test_orm() { - db := sqlite.connect(':memory:') or { panic(err) } + mut db := sqlite.connect(':memory:') or { panic(err) } upper_1 := User{ name: 'Test' @@ -31,4 +31,5 @@ fn test_orm() { } or { panic(err) } assert result[0].name == 'Test' + db.close()! } diff --git a/vlib/v/tests/orm_sub_struct_test.v b/vlib/v/tests/orm_sub_struct_test.v index b520074c2600c7..0b0769e0c8196f 100644 --- a/vlib/v/tests/orm_sub_struct_test.v +++ b/vlib/v/tests/orm_sub_struct_test.v @@ -11,7 +11,7 @@ struct SubStruct { } fn test_orm_sub_structs() { - db := sqlite.connect(':memory:') or { panic(err) } + mut db := sqlite.connect(':memory:') or { panic(err) } sql db { create table Upper }! @@ -34,4 +34,5 @@ fn test_orm_sub_structs() { }! assert uppers.first().sub.name == upper_1.sub.name + db.close()! } diff --git a/vlib/v/tests/orm_table_name_test.v b/vlib/v/tests/orm_table_name_test.v index faecad405781b1..5cb5e346a738b5 100644 --- a/vlib/v/tests/orm_table_name_test.v +++ b/vlib/v/tests/orm_table_name_test.v @@ -6,12 +6,14 @@ struct ORMTableSpecificName { } fn test_orm_table_name() { - db := sqlite.connect(':memory:') or { panic(err) } + mut db := sqlite.connect(':memory:') or { panic(err) } r := sql db { select from ORMTableSpecificName } or { + db.close()! assert true return } + db.close()! assert false } diff --git a/vlib/v/tests/sql_statement_inside_fn_call_test.v b/vlib/v/tests/sql_statement_inside_fn_call_test.v index 19636bb874d2a4..fac27f9224f135 100644 --- a/vlib/v/tests/sql_statement_inside_fn_call_test.v +++ b/vlib/v/tests/sql_statement_inside_fn_call_test.v @@ -10,7 +10,7 @@ fn x(m Movie) int { } fn test_sql_statement_inside_fn_call() { - db := sqlite.connect(':memory:') or { panic('failed') } + mut db := sqlite.connect(':memory:') or { panic('failed') } sql db { create table Movie }! @@ -21,4 +21,5 @@ fn test_sql_statement_inside_fn_call() { dump(x(sql db { select from Movie where id == 1 }!.first())) + db.close()! }