From d222b9f528b9e0a782a81a5f301ef6f8243dd4f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:03:07 +0900 Subject: [PATCH 1/4] fix: sanitize analysis engine error payloads --- .../src/bandscope_analysis/api.py | 28 +++++++---- services/analysis-engine/tests/test_api.py | 49 +++++++++++++------ 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/services/analysis-engine/src/bandscope_analysis/api.py b/services/analysis-engine/src/bandscope_analysis/api.py index a193bce2..3013598e 100644 --- a/services/analysis-engine/src/bandscope_analysis/api.py +++ b/services/analysis-engine/src/bandscope_analysis/api.py @@ -4,6 +4,7 @@ import hashlib import json +import logging import multiprocessing as mp import queue import time @@ -24,6 +25,8 @@ FEATURE_CACHE_SCHEMA_VERSION = 1 STEM_SEPARATION_TIMEOUT_SECONDS = 20.0 +logger = logging.getLogger(__name__) + AnalysisJobState = Literal["queued", "running", "succeeded", "failed"] AnalysisJobStage = Literal["queued", "decode", "separate", "analyze", "persist", "ready"] AnalysisCacheStatus = Literal["disabled", "miss", "hit", "stored"] @@ -834,14 +837,18 @@ def _stem_separation_worker( ) return result_queue.put(("ok", separation_result)) - except FileNotFoundError as error: - result_queue.put(("file_not_found", str(error))) - except ValueError as error: - result_queue.put(("value_error", str(error))) - except RuntimeError as error: - result_queue.put(("runtime_error", str(error))) - except Exception as error: - result_queue.put(("runtime_error", str(error))) + except FileNotFoundError: + logger.error("Stem separation failed because the source file was missing.", exc_info=True) + result_queue.put(("file_not_found", "Audio source file not found.")) + except ValueError: + logger.error("Stem separation rejected invalid audio source data.", exc_info=True) + result_queue.put(("value_error", "Invalid audio source data.")) + except RuntimeError: + logger.error("Stem separation failed with a runtime error.", exc_info=True) + result_queue.put(("runtime_error", "Runtime error occurred during stem separation.")) + except Exception: + logger.error("Stem separation failed unexpectedly.", exc_info=True) + result_queue.put(("runtime_error", "An unexpected error occurred during stem separation.")) def _multiprocessing_context() -> mp.context.BaseContext: @@ -1082,7 +1089,8 @@ def run_analysis_job_updates( ) ) audio_features = None - except (FileNotFoundError, ValueError) as error: + except (FileNotFoundError, ValueError): + logger.error("Stem separation failed before analysis job completion.", exc_info=True) updates.append( _build_job_status( job_id=job_id, @@ -1094,7 +1102,7 @@ def run_analysis_job_updates( cache_status=cache_status, error={ "code": "engine_unavailable", - "message": f"Stem separation failed: {error}", + "message": "Stem separation failed", }, ) ) diff --git a/services/analysis-engine/tests/test_api.py b/services/analysis-engine/tests/test_api.py index ea55cba2..f74a7e51 100644 --- a/services/analysis-engine/tests/test_api.py +++ b/services/analysis-engine/tests/test_api.py @@ -463,7 +463,10 @@ def test_run_analysis_job_updates_report_progress_and_cache(tmp_path) -> None: def test_run_analysis_job_updates_fail_safely_when_local_separation_fails() -> None: """Ensure unsafe or undecodable local audio returns a typed failure envelope.""" - with patch("bandscope_analysis.api.AudioStemSeparator") as separator_class: + with ( + patch("bandscope_analysis.api.AudioStemSeparator") as separator_class, + patch("bandscope_analysis.api.logger") as logger, + ): separator_class.return_value.separate.side_effect = ValueError( "Audio file is too large for stem separation: 16 bytes (max 8 bytes)" ) @@ -495,11 +498,13 @@ def test_run_analysis_job_updates_fail_safely_when_local_separation_fails() -> N assert updates[-1]["progressPercent"] == 45 assert updates[-1]["error"] == { "code": "engine_unavailable", - "message": ( - "Stem separation failed: Audio file is too large for stem separation: " - "16 bytes (max 8 bytes)" - ), + "message": "Stem separation failed", } + assert "/Users/test/Music" not in str(updates[-1]["error"]) + logger.error.assert_called_once_with( + "Stem separation failed before analysis job completion.", + exc_info=True, + ) def test_cached_analysis_helpers_treat_invalid_cache_as_miss(tmp_path) -> None: @@ -848,18 +853,34 @@ def put(self, item: tuple[str, object]) -> None: self.items.append(item) cases = [ - (FileNotFoundError("missing"), "file_not_found"), - (ValueError("bad media"), "value_error"), - (RuntimeError("oom"), "runtime_error"), - (Exception("unexpected"), "runtime_error"), + (FileNotFoundError("missing /secret/audio.wav"), "file_not_found"), + (ValueError("bad media /secret/audio.wav"), "value_error"), + (RuntimeError("oom /secret/audio.wav"), "runtime_error"), + (Exception("unexpected /secret/audio.wav"), "runtime_error"), ] + expected_messages = { + FileNotFoundError: "Audio source file not found.", + ValueError: "Invalid audio source data.", + RuntimeError: "Runtime error occurred during stem separation.", + Exception: "An unexpected error occurred during stem separation.", + } for error, expected_kind in cases: fake_queue = FakeQueue() - with patch("bandscope_analysis.api.AudioStemSeparator") as separator_class: + with ( + patch("bandscope_analysis.api.AudioStemSeparator") as separator_class, + patch("bandscope_analysis.api.logger") as logger, + ): separator_class.return_value.separate.side_effect = error _stem_separation_worker("/tmp/audio.wav", fake_queue) - assert fake_queue.items == [(expected_kind, str(error))] + expected_message = next( + message + for exception_type, message in expected_messages.items() + if isinstance(error, exception_type) + ) + assert fake_queue.items == [(expected_kind, expected_message)] + assert "/secret" not in str(fake_queue.items) + logger.error.assert_called_once() fake_queue = FakeQueue() with patch("bandscope_analysis.api.AudioStemSeparator") as separator_class: @@ -871,7 +892,7 @@ def put(self, item: tuple[str, object]) -> None: with patch("bandscope_analysis.api.AudioStemSeparator") as separator_class: separator_class.return_value.separate.return_value = {"stems": {}} _stem_separation_worker("/tmp/audio.wav", fake_queue, "/tmp/stems.npz") - assert fake_queue.items == [("runtime_error", "Stem separation returned invalid stems.")] + assert fake_queue.items == [("runtime_error", "Runtime error occurred during stem separation.")] fake_queue = FakeQueue() with patch("bandscope_analysis.api.AudioStemSeparator") as separator_class: @@ -880,9 +901,7 @@ def put(self, item: tuple[str, object]) -> None: "stem_role_types": {"bass": "percussion"}, } _stem_separation_worker("/tmp/audio.wav", fake_queue, "/tmp/stems.npz") - assert fake_queue.items == [ - ("runtime_error", "Stem separation returned invalid stem role metadata.") - ] + assert fake_queue.items == [("runtime_error", "Runtime error occurred during stem separation.")] def test_stem_separation_worker_writes_large_stems_to_file_envelope(tmp_path) -> None: From 63f95323399aa8bda41a626416c794af58adabae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 20:16:26 +0900 Subject: [PATCH 2/4] fix: centralize stem separation error mapping --- .../src/bandscope_analysis/api.py | 46 ++++++++++++++----- services/analysis-engine/tests/test_api.py | 43 ++++++++++------- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/services/analysis-engine/src/bandscope_analysis/api.py b/services/analysis-engine/src/bandscope_analysis/api.py index 3013598e..846d4e08 100644 --- a/services/analysis-engine/src/bandscope_analysis/api.py +++ b/services/analysis-engine/src/bandscope_analysis/api.py @@ -30,6 +30,7 @@ AnalysisJobState = Literal["queued", "running", "succeeded", "failed"] AnalysisJobStage = Literal["queued", "decode", "separate", "analyze", "persist", "ready"] AnalysisCacheStatus = Literal["disabled", "miss", "hit", "stored"] +StemSeparationFailureKind = Literal["file_not_found", "value_error", "runtime_error"] class AnalysisJobRequest(TypedDict): @@ -837,18 +838,39 @@ def _stem_separation_worker( ) return result_queue.put(("ok", separation_result)) - except FileNotFoundError: - logger.error("Stem separation failed because the source file was missing.", exc_info=True) - result_queue.put(("file_not_found", "Audio source file not found.")) - except ValueError: - logger.error("Stem separation rejected invalid audio source data.", exc_info=True) - result_queue.put(("value_error", "Invalid audio source data.")) - except RuntimeError: - logger.error("Stem separation failed with a runtime error.", exc_info=True) - result_queue.put(("runtime_error", "Runtime error occurred during stem separation.")) - except Exception: - logger.error("Stem separation failed unexpectedly.", exc_info=True) - result_queue.put(("runtime_error", "An unexpected error occurred during stem separation.")) + except Exception as error: + kind, safe_message, log_message = _stem_separation_failure(error) + logger.exception(log_message) + result_queue.put((kind, safe_message)) + + +def _stem_separation_failure( + error: Exception, +) -> tuple[StemSeparationFailureKind, str, str]: + """Map worker exceptions to safe parent payloads and stable log messages.""" + if isinstance(error, FileNotFoundError): + return ( + "file_not_found", + "Audio source file not found.", + "Stem separation failed because the source file was missing.", + ) + if isinstance(error, ValueError): + return ( + "value_error", + "Invalid audio source data.", + "Stem separation rejected invalid audio source data.", + ) + if isinstance(error, RuntimeError): + return ( + "runtime_error", + "Runtime error occurred during stem separation.", + "Stem separation failed with a runtime error.", + ) + return ( + "runtime_error", + "An unexpected error occurred during stem separation.", + "Stem separation failed unexpectedly.", + ) def _multiprocessing_context() -> mp.context.BaseContext: diff --git a/services/analysis-engine/tests/test_api.py b/services/analysis-engine/tests/test_api.py index f74a7e51..f8da523d 100644 --- a/services/analysis-engine/tests/test_api.py +++ b/services/analysis-engine/tests/test_api.py @@ -853,19 +853,33 @@ def put(self, item: tuple[str, object]) -> None: self.items.append(item) cases = [ - (FileNotFoundError("missing /secret/audio.wav"), "file_not_found"), - (ValueError("bad media /secret/audio.wav"), "value_error"), - (RuntimeError("oom /secret/audio.wav"), "runtime_error"), - (Exception("unexpected /secret/audio.wav"), "runtime_error"), + ( + FileNotFoundError("missing /secret/audio.wav"), + "file_not_found", + "Audio source file not found.", + "Stem separation failed because the source file was missing.", + ), + ( + ValueError("bad media /secret/audio.wav"), + "value_error", + "Invalid audio source data.", + "Stem separation rejected invalid audio source data.", + ), + ( + RuntimeError("oom /secret/audio.wav"), + "runtime_error", + "Runtime error occurred during stem separation.", + "Stem separation failed with a runtime error.", + ), + ( + Exception("unexpected /secret/audio.wav"), + "runtime_error", + "An unexpected error occurred during stem separation.", + "Stem separation failed unexpectedly.", + ), ] - expected_messages = { - FileNotFoundError: "Audio source file not found.", - ValueError: "Invalid audio source data.", - RuntimeError: "Runtime error occurred during stem separation.", - Exception: "An unexpected error occurred during stem separation.", - } - for error, expected_kind in cases: + for error, expected_kind, expected_message, expected_log_message in cases: fake_queue = FakeQueue() with ( patch("bandscope_analysis.api.AudioStemSeparator") as separator_class, @@ -873,14 +887,9 @@ def put(self, item: tuple[str, object]) -> None: ): separator_class.return_value.separate.side_effect = error _stem_separation_worker("/tmp/audio.wav", fake_queue) - expected_message = next( - message - for exception_type, message in expected_messages.items() - if isinstance(error, exception_type) - ) assert fake_queue.items == [(expected_kind, expected_message)] assert "/secret" not in str(fake_queue.items) - logger.error.assert_called_once() + logger.exception.assert_called_once_with(expected_log_message) fake_queue = FakeQueue() with patch("bandscope_analysis.api.AudioStemSeparator") as separator_class: From ae04dc8bb044332a0084a5b07bd85ef3ffa589dc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 15:20:17 +0900 Subject: [PATCH 3/4] fix: update anyhow for RustSec 2026-0190 --- apps/desktop/src-tauri/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index 4d9ae737..0df254ea 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "atk" From cec79fe98c00d5cbf58bf07ff53cb227d5a7d12a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 2 Jul 2026 19:45:56 +0900 Subject: [PATCH 4/4] fix: document quick-xml advisory exceptions --- apps/desktop/src-tauri/.cargo/audit.toml | 2 ++ apps/desktop/src-tauri/osv-scanner.toml | 8 ++++++++ docs/security/dependency-policy.md | 1 + 3 files changed, 11 insertions(+) diff --git a/apps/desktop/src-tauri/.cargo/audit.toml b/apps/desktop/src-tauri/.cargo/audit.toml index 9fc2a4f3..861e0aa5 100644 --- a/apps/desktop/src-tauri/.cargo/audit.toml +++ b/apps/desktop/src-tauri/.cargo/audit.toml @@ -17,4 +17,6 @@ ignore = [ "RUSTSEC-2025-0100", # unic-ucd-ident: unmaintained "RUSTSEC-2025-0098", # unic-ucd-version: unmaintained "RUSTSEC-2024-0429", # glib 0.18.5: VariantStrIter unsoundness, transitive via Tauri/wry/webkit2gtk/gtk GTK3 stack; remove when upstream drops or patches the chain + "RUSTSEC-2026-0194", # quick-xml 0.39.4: inherited via Tauri/plist and rfd/wayland-scanner; no compatible upstream release has moved both chains to quick-xml >=0.41.0 yet + "RUSTSEC-2026-0195", # quick-xml 0.39.4: same owner chain and removal condition as RUSTSEC-2026-0194 ] diff --git a/apps/desktop/src-tauri/osv-scanner.toml b/apps/desktop/src-tauri/osv-scanner.toml index 16b3b20e..c8fc5e44 100644 --- a/apps/desktop/src-tauri/osv-scanner.toml +++ b/apps/desktop/src-tauri/osv-scanner.toml @@ -65,3 +65,11 @@ reason = "Inherited through the current Tauri GTK3 owner chain and already track [[IgnoredVulns]] id = "RUSTSEC-2024-0429" reason = "glib 0.18.5 VariantStrIter advisory inherited through Tauri/wry/webkit2gtk/gtk; allowed only until upstream drops or patches the chain, with scope guarded by scripts/checks/verify_supply_chain.py." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0194" +reason = "quick-xml 0.39.4 duplicate-attribute advisory is inherited through Tauri/plist and rfd/wayland-scanner; current compatible upstream crates do not yet allow quick-xml >=0.41.0, and this app does not expose those XML parser paths to untrusted user XML." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0195" +reason = "quick-xml 0.39.4 namespace-allocation advisory is inherited through the same Tauri/plist and rfd/wayland-scanner owner chain as RUSTSEC-2026-0194; remove once compatible upstream crates move to quick-xml >=0.41.0." diff --git a/docs/security/dependency-policy.md b/docs/security/dependency-policy.md index d3a9680e..d7c7acad 100644 --- a/docs/security/dependency-policy.md +++ b/docs/security/dependency-policy.md @@ -104,6 +104,7 @@ Current controlled exceptions: - No Python vulnerability exceptions are active. `GHSA-5239-wwwm-4pmq` (`Pygments <2.20.0`) was removed by locking `Pygments` to `2.20.0`; the CI `security-audit` workflow must run `pip-audit --local --strict` against the synced `uv` environment without a targeted ignore for that advisory. - Cargo audit warnings for legacy `gtk3` vulnerabilities (e.g. `RUSTSEC-2024-0413`) inherited through Tauri v2 `wry`/`webkit2gtk` integration are explicitly allowed. These are deep framework dependencies with no alternative, so they are documented exceptions and ignored by default. - `RUSTSEC-2024-0429` for `glib 0.18.5` is allowed only for the `VariantStrIter` advisory inherited through the Tauri/wry/webkit2gtk/gtk GTK3 stack. A compatible lockfile refresh can move the desktop stack to `tauri 2.11.3`, `wry 0.55.1`, `tao 0.35.3`, `muda 0.19.3`, and related transitive patches, but it still does not move this stack to patched `glib >=0.20.0`; the exception must remain encoded in repo-controlled audit configuration and guarded by `scripts/checks/verify_supply_chain.py`, and it must be removed when upstream drops or patches the chain. +- `RUSTSEC-2026-0194` and `RUSTSEC-2026-0195` for `quick-xml 0.39.4` are allowed only while the current compatible upstream owner chains still require vulnerable `quick-xml`: `plist 1.9.0` through Tauri, and `wayland-scanner 0.31.10` through Linux `rfd`/Wayland dependencies. `quick-xml >=0.41.0` is patched, but `plist 1.9.0` requires `quick-xml ^0.39.2` and the current `wayland-scanner` release also has no compatible patched path. BandScope does not expose either owner chain as a user-controlled XML ingestion surface; the exception must stay encoded in repo-controlled cargo-audit and OSV configuration, and must be removed once compatible upstream crates publish a patched dependency path. Retired third-party deprecation and advisory signal: