Skip to content

Commit f53c9c6

Browse files
authored
Replace Valgrind check with sanitizers (#188)
1 parent beca6f8 commit f53c9c6

12 files changed

Lines changed: 246 additions & 55 deletions

File tree

.github/workflows/tests.yml

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -222,30 +222,30 @@ jobs:
222222
working-directory: dart
223223
run: dart test
224224

225-
valgrind:
226-
name: Testing with Valgrind on ${{ matrix.os }}
227-
runs-on: ${{ matrix.os }}
228-
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository)
229-
strategy:
230-
matrix:
231-
include:
232-
- os: ubuntu-latest
225+
test_with_sanitizers:
226+
runs-on: ubuntu-latest
233227
steps:
234228
- uses: actions/checkout@v6
235229
with:
236230
persist-credentials: false
237-
- name: Install Rust
231+
- uses: dart-lang/setup-dart@v1
232+
- name: Install Rust Nightly
238233
run: rustup install
234+
- name: Install LLVM toolchain
235+
run: |
236+
sudo apt update
237+
sudo apt install -y llvm clang lld
239238
240-
- name: Install valgrind
241-
run: sudo apt update && sudo apt install -y valgrind
239+
- name: Build with sanitizers
240+
run: tool/build_linux_sanitized.sh
242241

243-
- name: Install Cargo Valgrind
244-
# TODO: Use released version. Currently we rely on the git version while we wait for this
245-
# to be released: https://github.com/jfrimmel/cargo-valgrind/commit/408c0b4fb56e84eddc2bb09c88a11ba3adc0c188
246-
run: cargo install --git https://github.com/jfrimmel/cargo-valgrind cargo-valgrind
247-
#run: cargo install cargo-valgrind
242+
- run: dart pub get
243+
working-directory: dart
248244

249-
- name: Test Core
250-
run: |
251-
cargo valgrind test -p powersync_core
245+
- name: Test with MemorySanitizer
246+
working-directory: dart
247+
run: dart tool/run_tests.dart msan
248+
249+
- name: Test with AddressSanitizer
250+
working-directory: dart
251+
run: dart tool/run_tests.dart asan

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ target/
1010
*.tar.xz
1111
*.zip
1212
.build
13+
/sanitized/

crates/core/src/pre_close_vtab.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use powersync_sqlite_nostd as sqlite;
88
use sqlite::{Connection, ResultCode};
99

1010
use crate::state::DatabaseState;
11+
use crate::update_hooks::uninstall_update_hooks;
1112
use crate::vtab_util::*;
1213

1314
/// A virtual table hack to implement a "pre-close hook" for databases.
@@ -20,6 +21,7 @@ use crate::vtab_util::*;
2021
struct VirtualTable {
2122
base: sqlite::vtab,
2223
state: Rc<DatabaseState>,
24+
db: *mut sqlite::sqlite3,
2325
}
2426

2527
extern "C" fn connect(
@@ -42,6 +44,7 @@ extern "C" fn connect(
4244
zErrMsg: core::ptr::null_mut(),
4345
},
4446
state: DatabaseState::clone_from(aux),
47+
db,
4548
}));
4649
*vtab = tab.cast::<sqlite::vtab>();
4750
let _ = sqlite::vtab_config(db, 0);
@@ -57,6 +60,7 @@ extern "C" fn disconnect(vtab: *mut sqlite::vtab) -> c_int {
5760
// So we can use this as a "pre-close" hook and ensure we clear prepared statements the core
5861
// extension might hold.
5962
vtab.state.release_resources();
63+
uninstall_update_hooks(vtab.db, &vtab.state);
6064

6165
ResultCode::OK as c_int
6266
}

crates/core/src/state.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use crate::{
2525
#[derive(Default)]
2626
pub struct DatabaseState {
2727
pub is_in_sync_local: Cell<bool>,
28+
/// Whether the core extension has installed update, commit and rollback hooks.
29+
pub core_extension_has_update_hooks: Cell<bool>,
2830
schema: RefCell<Option<Schema>>,
2931
pending_updates: RefCell<BTreeSet<String>>,
3032
commited_updates: RefCell<BTreeSet<String>>,

crates/core/src/update_hooks.rs

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use core::{
2-
cell::Cell,
32
ffi::{CStr, c_char, c_int, c_void},
43
ptr::null_mut,
54
};
@@ -19,13 +18,9 @@ use crate::{constants::SUBTYPE_JSON, error::PowerSyncError, state::DatabaseState
1918
/// and comitted since the last `powersync_update_hooks` call.
2019
///
2120
/// The update hooks don't have to be uninstalled manually, that happens when the connection is
22-
/// closed and the function is unregistered.
21+
/// closed (`powersync_init()` installs a vtab calling `uninstall_update_hooks()`).
2322
pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<(), ResultCode> {
24-
let state = Box::new(HookState {
25-
has_registered_hooks: Cell::new(false),
26-
db,
27-
state,
28-
});
23+
let state = Box::new(HookState { db, state });
2924

3025
db.create_function_v2(
3126
"powersync_update_hooks",
@@ -41,31 +36,13 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<()
4136
}
4237

4338
struct HookState {
44-
has_registered_hooks: Cell<bool>,
4539
db: *mut sqlite::sqlite3,
4640
state: Rc<DatabaseState>,
4741
}
4842

4943
extern "C" fn destroy_function(ctx: *mut c_void) {
5044
let state = unsafe { Box::from_raw(ctx as *mut HookState) };
51-
52-
if state.has_registered_hooks.get() {
53-
check_previous(
54-
"update",
55-
&state.state,
56-
state.db.update_hook(None, null_mut()),
57-
);
58-
check_previous(
59-
"commit",
60-
&state.state,
61-
state.db.commit_hook(None, null_mut()),
62-
);
63-
check_previous(
64-
"rollback",
65-
&state.state,
66-
state.db.rollback_hook(None, null_mut()),
67-
);
68-
}
45+
uninstall_update_hooks(state.db, &state.state);
6946
}
7047

7148
extern "C" fn powersync_update_hooks(
@@ -107,7 +84,7 @@ extern "C" fn powersync_update_hooks(
10784
Rc::into_raw(db_state.clone()) as *mut c_void,
10885
),
10986
);
110-
state.has_registered_hooks.set(true);
87+
db_state.core_extension_has_update_hooks.set(true);
11188
}
11289
"get" => {
11390
let state = unsafe { user_data.as_ref().unwrap_unchecked() };
@@ -155,6 +132,14 @@ unsafe extern "C" fn rollback_hook_impl(ctx: *mut c_void) {
155132
state.track_rollback();
156133
}
157134

135+
pub fn uninstall_update_hooks(db: *mut sqlite::sqlite3, state: &Rc<DatabaseState>) {
136+
if state.core_extension_has_update_hooks.take() {
137+
check_previous("update", state, db.update_hook(None, null_mut()));
138+
check_previous("commit", state, db.commit_hook(None, null_mut()));
139+
check_previous("rollback", state, db.rollback_hook(None, null_mut()));
140+
}
141+
}
142+
158143
fn check_previous(desc: &'static str, expected: &Rc<DatabaseState>, previous: *const c_void) {
159144
let expected = Rc::as_ptr(expected);
160145

dart/pubspec.lock

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,10 @@ packages:
237237
dependency: transitive
238238
description:
239239
name: native_toolchain_c
240-
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
240+
sha256: "8aaead321425bd3f03bd5894aa27c8ea6993eab95531da7e59f5d39c6e5708ec"
241241
url: "https://pub.dev"
242242
source: hosted
243-
version: "0.17.4"
243+
version: "0.18.0"
244244
node_preamble:
245245
dependency: transitive
246246
description:
@@ -364,11 +364,12 @@ packages:
364364
sqlite3:
365365
dependency: "direct main"
366366
description:
367-
name: sqlite3
368-
sha256: c6cfe9b1cc159c9eb8ba174b533a60b5126f9db8c6e34efb127d2bc04bc45034
369-
url: "https://pub.dev"
370-
source: hosted
371-
version: "3.1.4"
367+
path: sqlite3
368+
ref: main
369+
resolved-ref: "97268a5b32cf6313dd670e6fa57ae88d38a6564b"
370+
url: "https://github.com/simolus3/sqlite3.dart.git"
371+
source: git
372+
version: "3.3.2"
372373
sqlite3_test:
373374
dependency: "direct dev"
374375
description:
@@ -514,4 +515,4 @@ packages:
514515
source: hosted
515516
version: "3.1.3"
516517
sdks:
517-
dart: ">=3.9.999 <4.0.0"
518+
dart: ">=3.10.0 <4.0.0"

dart/pubspec.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ dev_dependencies:
1919
meta: ^1.16.0
2020
path: ^1.9.1
2121

22+
dependency_overrides:
23+
# TODO: Remove after https://github.com/simolus3/sqlite3.dart/commit/97268a5b32cf6313dd670e6fa57ae88d38a6564b
24+
# gets released.
25+
sqlite3:
26+
git:
27+
url: https://github.com/simolus3/sqlite3.dart.git
28+
path: sqlite3
29+
ref: main
30+
2231
# See https://pub.dev/documentation/sqlite3/latest/topics/hook-topic.html
2332
hooks:
2433
user_defines:

dart/test/sync_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,10 @@ void _syncTests<T>({
978978
expect(db.select('SELECT * FROM ps_buckets'), isEmpty);
979979
});
980980

981-
group('recoverable', () {
981+
group('recoverable',
982+
skip: testingWithSanitizers != null
983+
? 'Unsupported in memory VFS'
984+
: null, () {
982985
late CommonDatabase secondary;
983986
final checkpoint = {
984987
'checkpoint': {

dart/test/utils/native_test_utils.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:ffi';
22
import 'dart:io';
3+
import 'dart:math';
34

45
import 'package:clock/clock.dart';
56
import 'package:fake_async/fake_async.dart';
@@ -14,12 +15,26 @@ const defaultSqlitePath = 'libsqlite3.so.0';
1415
const cargoDebugPath = '../target/debug';
1516
var didLoadExtension = false;
1617

18+
String? testingWithSanitizers = null;
19+
1720
CommonDatabase openTestDatabase(
1821
{VirtualFileSystem? vfs, String fileName = ':memory:'}) {
1922
if (!didLoadExtension) {
2023
loadExtension();
2124
}
2225

26+
if (testingWithSanitizers != null && vfs == null) {
27+
// When testing with sanitizers, always use a Dart VFS since we use an
28+
// uninstrumented libc that wouldn't report buffers for read files as
29+
// initialized.
30+
final inMemory =
31+
InMemoryFileSystem(name: 'in-memory-${Random().nextInt(1 << 32)}');
32+
sqlite3.registerVirtualFileSystem(inMemory);
33+
addTearDown(() => sqlite3.unregisterVirtualFileSystem(inMemory));
34+
35+
vfs = inMemory;
36+
}
37+
2338
final db = sqlite3.open(fileName, vfs: vfs?.name);
2439
addTearDown(db.close);
2540
return db;
@@ -64,6 +79,10 @@ String resolvePowerSyncLibrary() {
6479
}
6580

6681
String _getLibraryForPlatform({String? path = cargoDebugPath}) {
82+
if (testingWithSanitizers case final sanitizer?) {
83+
return '../sanitized/core_extension/libpowersync_$sanitizer.linux.so';
84+
}
85+
6786
return switch (Abi.current()) {
6887
Abi.androidArm ||
6988
Abi.androidArm64 ||

dart/tool/all_tests.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:test/test.dart';
2+
3+
import '../test/crud_test.dart' as crud_test;
4+
import '../test/error_test.dart' as error_test;
5+
import '../test/js_key_encoding_test.dart' as json_key_encoding_test;
6+
import '../test/migration_test.dart' as migration_test;
7+
import '../test/schema_test.dart' as schema_test;
8+
// Skipping sync_local_performance_test because it's slow. It's functionality is
9+
// covered by other tests.
10+
import '../test/sync_stream_test.dart' as sync_stream_test;
11+
import '../test/sync_test.dart' as sync_test;
12+
import '../test/update_hooks_test.dart' as update_hooks_test;
13+
14+
import '../test/utils/native_test_utils.dart';
15+
16+
/// Runs all native tests.
17+
///
18+
/// We aot-compile this file to run tests with different sanitizers.
19+
void main(List<String> args) {
20+
testingWithSanitizers = args.single;
21+
22+
group('crud_test.dart', crud_test.main);
23+
group('error_test.dart', error_test.main);
24+
group('js_key_encoding_test.dart', json_key_encoding_test.main);
25+
group('migration_test.dart', migration_test.main);
26+
group('schema_test.dart', schema_test.main);
27+
group('sync_stream_test.dart', sync_stream_test.main);
28+
group('sync_test.dart', sync_test.main);
29+
group('update_hooks_test.dart', update_hooks_test.main);
30+
}

0 commit comments

Comments
 (0)