Skip to content

Commit 8657e5a

Browse files
committed
[green] preflight recovery metadata bounds on database isolate
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent 18eee79 commit 8657e5a

2 files changed

Lines changed: 68 additions & 2 deletions

File tree

‎app/lib/data/drift_draft_repository.dart‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,19 @@ class DriftDraftRepository implements DraftRepository {
108108
Future<DraftRead> load() => _serialized(() => _database.transaction(_read));
109109

110110
Future<DraftRead> _read() async {
111+
// Bound untrusted SQLite values on the database isolate before sending
112+
// strings to the UI isolate or decoding their JSON. This also keeps a
113+
// corrupt-but-readable database from monopolizing startup's timer.
114+
final QueryRow settingsBound = await _database
115+
.customSelect(
116+
'SELECT COUNT(*) AS row_count, COALESCE(MAX(LENGTH(CAST(value AS BLOB))), 0) AS max_bytes FROM settings WHERE key = ?',
117+
variables: <Variable<Object>>[const Variable<String>(_slotKey)],
118+
)
119+
.getSingle();
120+
if (settingsBound.read<int>('row_count') != 1 ||
121+
settingsBound.read<int>('max_bytes') > 4096) {
122+
throw const DraftStorageException(DraftStorageFailure.corrupt);
123+
}
111124
final List<Setting> settings = await (_database.select(
112125
_database.settings,
113126
)..where((Settings t) => t.key.equals(_slotKey))).get();
@@ -136,11 +149,28 @@ class DriftDraftRepository implements DraftRepository {
136149
}
137150
final QueryRow bound = await _database
138151
.customSelect(
139-
'SELECT COUNT(*) AS row_count, COALESCE(MAX(LENGTH(content)), 0) AS max_bytes FROM project_files',
152+
'SELECT COUNT(*) AS row_count, COALESCE(MAX(LENGTH(CAST(content AS BLOB))), 0) AS max_bytes, '
153+
'COALESCE(MAX(MAX(LENGTH(CAST(id AS BLOB)), LENGTH(CAST(project_id AS BLOB)), '
154+
'LENGTH(CAST(path AS BLOB)), LENGTH(CAST(content_hash AS BLOB)))), 0) AS metadata_bytes, '
155+
"COALESCE(MIN(TYPEOF(size) = 'integer' AND TYPEOF(updated_at) = 'integer'), 1) AS valid_numbers FROM project_files",
140156
)
141157
.getSingle();
142158
if (bound.read<int>('row_count') > 1 ||
143-
bound.read<int>('max_bytes') > maxDraftBytes) {
159+
bound.read<int>('max_bytes') > maxDraftBytes ||
160+
bound.read<int>('metadata_bytes') > 128 ||
161+
bound.read<int>('valid_numbers') != 1) {
162+
throw const DraftStorageException(DraftStorageFailure.corrupt);
163+
}
164+
final QueryRow projectsBound = await _database
165+
.customSelect(
166+
'SELECT COUNT(*) AS row_count, COALESCE(MAX(MAX(LENGTH(CAST(id AS BLOB)), '
167+
'LENGTH(CAST(name AS BLOB)))), 0) AS max_bytes, '
168+
"COALESCE(MIN(TYPEOF(created_at) = 'integer' AND TYPEOF(updated_at) = 'integer'), 1) AS valid_numbers FROM projects",
169+
)
170+
.getSingle();
171+
if (projectsBound.read<int>('row_count') > 1 ||
172+
projectsBound.read<int>('max_bytes') > 128 ||
173+
projectsBound.read<int>('valid_numbers') != 1) {
144174
throw const DraftStorageException(DraftStorageFailure.corrupt);
145175
}
146176
final List<ProjectFile> files = await _database

‎app/test/data/draft_repository_test.dart‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,42 @@ void main() {
452452
},
453453
);
454454

455+
test(
456+
'oversized internal project metadata is refused before hydration',
457+
() async {
458+
await repository.write(snapshot('safe'), expectedGeneration: 0);
459+
await repository.close();
460+
final sqlite.Database db = sqlite.sqlite3.open(file.path);
461+
db.execute('UPDATE projects SET name = ?', <Object>['x' * 8192]);
462+
db.close();
463+
final List<int> before = await file.readAsBytes();
464+
await expectLater(
465+
DriftDraftRepository.open(file),
466+
failure(DraftStorageFailure.corrupt),
467+
);
468+
expect(await file.readAsBytes(), before);
469+
},
470+
);
471+
472+
test(
473+
'corrupt text in numeric metadata is refused before row mapping',
474+
() async {
475+
await repository.write(snapshot('safe'), expectedGeneration: 0);
476+
await repository.close();
477+
final sqlite.Database db = sqlite.sqlite3.open(file.path);
478+
db.execute('UPDATE project_files SET updated_at = ?', <Object>[
479+
'x' * 8192,
480+
]);
481+
db.close();
482+
final List<int> before = await file.readAsBytes();
483+
await expectLater(
484+
DriftDraftRepository.open(file),
485+
failure(DraftStorageFailure.corrupt),
486+
);
487+
expect(await file.readAsBytes(), before);
488+
},
489+
);
490+
455491
test(
456492
'process kill before, during and after commit yields only whole checkpoints',
457493
() async {

0 commit comments

Comments
 (0)