From aa6bd6dcdeddfd729da2945c1ae9fcb58880e5af Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Sat, 19 Sep 2026 20:48:10 +0800 Subject: [PATCH] Validate external core shutdown timeouts Signed-off-by: Loren Eteval --- Furious/Backends/ExternalCore/Configuration.py | 9 +++++++-- tests/README.md | 2 +- tests/test_external_core.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Furious/Backends/ExternalCore/Configuration.py b/Furious/Backends/ExternalCore/Configuration.py index c599d2e..1f54e28 100644 --- a/Furious/Backends/ExternalCore/Configuration.py +++ b/Furious/Backends/ExternalCore/Configuration.py @@ -232,9 +232,14 @@ class ConfigExternalCore(CoreConfiguration): 'Shutdown timeout must be a number of seconds' ) - value = float(value) + try: + value = float(value) + except OverflowError as ex: + raise ExternalCoreConfigurationError( + 'Shutdown timeout must be between 0.1 and 60 seconds' + ) from ex - if value < 0.1 or value > 60: + if not 0.1 <= value <= 60: raise ExternalCoreConfigurationError( 'Shutdown timeout must be between 0.1 and 60 seconds' ) diff --git a/tests/README.md b/tests/README.md index ddeab31..979a5fd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -91,7 +91,7 @@ worker. Choose tests by the changed contract rather than by filename alone. | [test_connection_startup_async.py](test_connection_startup_async.py) | Real local-listener readiness, timeout/cancel/replacement, semantic exits, DNS reply lifetime, mocked platform-specific TUN sequencing. | | [test_controllers.py](test_controllers.py) | Connection state/error/reconnect transitions, startup restoration, shared settings, routing fallback persistence and tray/selector agreement after custom-routing disable/re-enable. | | [test_runtime_lifecycle.py](test_runtime_lifecycle.py) | Qt-thread exit dispatch, commit/exit races, duplicate and late exits, idempotent release, spawn failure, queue/timer disposal on preparation failure. | -| [test_external_core.py](test_external_core.py) | Harmless real process launch/output/shutdown, partial thread-start rollback, readiness/TUN metadata, Windows paths with spaces, subscription rejection of executable profiles, bounded DNS references. | +| [test_external_core.py](test_external_core.py) | Harmless real process launch/output/shutdown, partial thread-start rollback, non-finite timeout rejection, readiness/TUN metadata, Windows paths with spaces, subscription rejection of executable profiles, bounded DNS references. | | [test_frozenlib.py](test_frozenlib.py) | Nested state guards, cleanup isolation, bounded caches/throttling, dual-stack probe selection, mocked proxy/DNS/routes/startup/session boundaries and failure handling. | | [test_native_tun_semantics.py](test_native_tun_semantics.py) | Xray/Hysteria2 runtime-copy TUN preservation/replacement, managed-TUN failures, download-test stripping, prevention of a second tun2socks owner. | | [test_subscription_sync.py](test_subscription_sync.py) | Group-local preparation/commit, stable duplicate identity, atomic failure, preservation of newer local metadata, rejection of changed source state. | diff --git a/tests/test_external_core.py b/tests/test_external_core.py index fa40e4e..f00b0b5 100644 --- a/tests/test_external_core.py +++ b/tests/test_external_core.py @@ -101,6 +101,22 @@ class ExternalCoreProcessTest(unittest.TestCase): } ) + def testShutdownTimeoutRejectsNonFiniteAndOverflowingValues(self): + """Reject invalid wait values before any executable can be acquired.""" + for value in (float('nan'), float('inf'), -float('inf'), 10**400): + with self.subTest(value=str(value)[:20]): + config = self.configuration(['-c', 'pass'], str(Path.cwd())) + config['shutdownTimeout'] = value + self.assertTrue(config.validateProcess()) + runtime = ExternalCoreProcess(config) + with mock.patch( + 'Furious.Backends.ExternalCore.Process.subprocess.Popen' + ) as spawn: + with self.assertRaises(RuntimeStartError): + runtime.start() + spawn.assert_not_called() + runtime.dispose() + def testDisposedRuntimeCannotAcquireAnotherProcess(self): """Disposal is terminal even when the stored launch specification is valid.""" runtime = ExternalCoreProcess(