From b862e5ffd9caaa36b3147979b0da4e230e72833f Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 25 Oct 2026 13:03:46 +0300 Subject: [PATCH 1/2] fix(platform): openDirectoryInFileManager returns false when it cannot create the directory; the Mongo TLS temp key is 0600 before it is written and a running instance's temp directory is kept (#1768) --- lib/core/platform/open_directory.dart | 8 +- .../security/ssl_certificate_support.dart | 79 +++++++++++++------ lib/main.dart | 7 ++ test/core/platform/open_directory_test.dart | 17 ++++ .../ssl_certificate_support_test.dart | 36 +++++++++ 5 files changed, 119 insertions(+), 28 deletions(-) diff --git a/lib/core/platform/open_directory.dart b/lib/core/platform/open_directory.dart index 692b6fcb..4e8dd71f 100644 --- a/lib/core/platform/open_directory.dart +++ b/lib/core/platform/open_directory.dart @@ -9,8 +9,12 @@ Future openDirectoryInFileManager( Future Function(String path)? opener, }) async { final dir = Directory(directoryPath); - if (!await dir.exists()) { - await dir.create(recursive: true); + try { + if (!await dir.exists()) { + await dir.create(recursive: true); + } + } on FileSystemException { + return false; } final open = opener ?? _defaultOpener; diff --git a/lib/core/security/ssl_certificate_support.dart b/lib/core/security/ssl_certificate_support.dart index b51022f4..f4169753 100644 --- a/lib/core/security/ssl_certificate_support.dart +++ b/lib/core/security/ssl_certificate_support.dart @@ -167,36 +167,39 @@ Future resolveMongoTlsCertificateKeyFile({ final certBytes = await File(certPath).readAsString(); final keyBytes = await File(keyPath).readAsString(); - final dir = await Directory.systemTemp.createTemp(kMongoTlsTempPrefix); - if (!Platform.isWindows) { - try { - await Process.run('chmod', ['700', dir.path]); - } catch (e) { - logIgnored('ssl_certificate_support', e); - } - } + // The pid in the name lets another instance's start-up cleanup see that this + // directory is in use. + final dir = await Directory.systemTemp + .createTemp('$kMongoTlsTempPrefix${pid}_'); final merged = File('${dir.path}/client.pem'); - await merged.writeAsString('$certBytes\n$keyBytes\n', flush: true); - if (!Platform.isWindows) { - // `Process.run` does not throw on a non-zero exit, so check it: a key - // file that stayed at the default mode must not be handed out silently. - try { - final result = await Process.run('chmod', ['600', merged.path]); - if (result.exitCode != 0) { - throw FileSystemException( - 'Could not restrict permissions of the TLS client key file: ' - '${result.stderr}'.trim(), - merged.path, - ); - } - } catch (_) { - await cleanupMongoTlsTempFile(merged.path); - rethrow; + try { + if (!Platform.isWindows) { + await _chmod('700', dir.path, 'TLS temp directory'); + // Empty and 0600 before the key goes in, so it is never readable with + // the default mode. + await merged.create(); + await _chmod('600', merged.path, 'TLS client key file'); } + await merged.writeAsString('$certBytes\n$keyBytes\n', flush: true); + } catch (_) { + await cleanupMongoTlsTempFile(merged.path); + rethrow; } return merged.path; } +/// `Process.run` does not throw on a non-zero exit, so check it: a key file +/// that stayed at the default mode must not be handed out silently. +Future _chmod(String mode, String path, String what) async { + final result = await Process.run('chmod', [mode, path]); + if (result.exitCode != 0) { + throw FileSystemException( + 'Could not restrict permissions of the $what: ${result.stderr}'.trim(), + path, + ); + } +} + /// Reliably deletes a temporary MongoDB TLS certificate file and its parent temp directory. Future cleanupMongoTlsTempFile(String? filePath) async { if (filePath == null || filePath.trim().isEmpty) return; @@ -220,8 +223,30 @@ Future cleanupMongoTlsTempFile(String? filePath) async { } } -/// Cleans up any stale temporary MongoDB TLS directories left from previous sessions. -Future cleanupStaleMongoTlsTempFiles() async { +final _tlsTempOwnerPid = RegExp('^$kMongoTlsTempPrefix(\\d+)_'); + +Future _isProcessAlive(int owner) async { + if (owner == pid) return true; + try { + if (Platform.isWindows) { + final result = + await Process.run('tasklist', ['/FI', 'PID eq $owner', '/NH']); + return result.exitCode != 0 || '${result.stdout}'.contains('$owner'); + } + return (await Process.run('kill', ['-0', '$owner'])).exitCode == 0; + } catch (_) { + // Cannot tell: leave the directory alone. + return true; + } +} + +/// Cleans up stale temporary MongoDB TLS directories left from previous +/// sessions. A directory named for a process that is still running belongs to +/// another Querya instance and is kept; [isProcessAlive] is for tests. +Future cleanupStaleMongoTlsTempFiles({ + Future Function(int pid)? isProcessAlive, +}) async { + final alive = isProcessAlive ?? _isProcessAlive; try { final tempDir = Directory.systemTemp; if (!await tempDir.exists()) return; @@ -230,6 +255,8 @@ Future cleanupStaleMongoTlsTempFiles() async { final name = entity.uri.pathSegments.where((s) => s.isNotEmpty).lastOrNull ?? ''; if (name.startsWith(kMongoTlsTempPrefix)) { + final owner = _tlsTempOwnerPid.firstMatch(name)?.group(1); + if (owner != null && await alive(int.parse(owner))) continue; try { await entity.delete(recursive: true); } catch (e) { diff --git a/lib/main.dart b/lib/main.dart index 3268a7d7..9c7da344 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:bitsdojo_window/bitsdojo_window.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; +import 'package:mcp_toolkit/mcp_toolkit.dart'; import 'app/app.dart'; import 'app/app_wiring.dart'; @@ -25,6 +27,11 @@ import 'features/updater/update_controller.dart'; void main([List args = const []]) async { unawaited(runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); + if (kDebugMode) { + MCPToolkitBinding.instance + ..initialize() + ..initializeFlutterToolkit(); + } installAppWiring(); FileLaunchService.instance.processLaunchArguments(args); await MacOsFileOpenChannel.install(); diff --git a/test/core/platform/open_directory_test.dart b/test/core/platform/open_directory_test.dart index b458ebeb..d69bbc16 100644 --- a/test/core/platform/open_directory_test.dart +++ b/test/core/platform/open_directory_test.dart @@ -31,6 +31,23 @@ void main() { expect(await Directory(target).exists(), isTrue); }); + test('returns false when the directory cannot be created (#1768)', + () async { + final root = await Directory.systemTemp + .createTemp('querya_open_directory_blocked_test_'); + addTearDown(() => root.delete(recursive: true)); + final file = File(p.join(root.path, 'a_file'))..writeAsStringSync('x'); + var called = false; + + final opened = await openDirectoryInFileManager( + p.join(file.path, 'child'), + opener: (_) async => called = true, + ); + + expect(opened, isFalse); + expect(called, isFalse); + }); + test('returns false when opener reports failure', () async { final root = await Directory.systemTemp .createTemp('querya_open_directory_fail_test_'); diff --git a/test/core/security/ssl_certificate_support_test.dart b/test/core/security/ssl_certificate_support_test.dart index 239e5450..5f57f02c 100644 --- a/test/core/security/ssl_certificate_support_test.dart +++ b/test/core/security/ssl_certificate_support_test.dart @@ -110,6 +110,42 @@ void main() { expect(await orphanDir.exists(), isFalse); }); + test('cleanup keeps the directory of a process that is still running (#1768)', + () async { + final live = await Directory.systemTemp + .createTemp('${kMongoTlsTempPrefix}4242_'); + final dead = await Directory.systemTemp + .createTemp('${kMongoTlsTempPrefix}4243_'); + addTearDown(() async { + if (await live.exists()) await live.delete(recursive: true); + if (await dead.exists()) await dead.delete(recursive: true); + }); + + await cleanupStaleMongoTlsTempFiles( + isProcessAlive: (pid) async => pid == 4242, + ); + + expect(await live.exists(), isTrue); + expect(await dead.exists(), isFalse); + }); + + test('the merged key is created in a directory named for this process (#1768)', + () async { + final input = await Directory.systemTemp.createTemp('querya_test_input_'); + addTearDown(() => input.delete(recursive: true)); + await File('${input.path}/c.pem').writeAsString('CERT'); + await File('${input.path}/k.pem').writeAsString('KEY'); + + final path = await resolveMongoTlsCertificateKeyFile( + clientCert: '${input.path}/c.pem', + clientKey: '${input.path}/k.pem', + ); + addTearDown(() => cleanupMongoTlsTempFile(path)); + + expect(File(path!).parent.path, contains('$kMongoTlsTempPrefix$pid')); + expect(await File(path).readAsString(), contains('KEY')); + }); + test('buildSecurityContext returns null when paths.hasAny is false', () { const paths = SslCertificatePaths(); final ctx = buildSecurityContext(paths); From 2f097d0344c62abe840c3aed15da49348d39e763 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 25 Oct 2026 13:08:04 +0300 Subject: [PATCH 2/2] fix: take lib/main.dart back out of this PR (local mcp_toolkit change committed by mistake) --- lib/main.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 9c7da344..3268a7d7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; -import 'package:mcp_toolkit/mcp_toolkit.dart'; import 'app/app.dart'; import 'app/app_wiring.dart'; @@ -27,11 +25,6 @@ import 'features/updater/update_controller.dart'; void main([List args = const []]) async { unawaited(runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); - if (kDebugMode) { - MCPToolkitBinding.instance - ..initialize() - ..initializeFlutterToolkit(); - } installAppWiring(); FileLaunchService.instance.processLaunchArguments(args); await MacOsFileOpenChannel.install();