|
| 1 | +{ |
| 2 | + "module": "llm", |
| 3 | + "scan_timestamp": "2026-08-25T16:12:53Z", |
| 4 | + "verifier_version": "wave3-v1.0", |
| 5 | + "source_files_scanned": 113, |
| 6 | + "summary": { |
| 7 | + "total_raw": 155, |
| 8 | + "verified_gaps": 5, |
| 9 | + "false_positives_removed": 150, |
| 10 | + "downgrades": 3, |
| 11 | + "severity_distribution": { |
| 12 | + "CRITICAL": 2, |
| 13 | + "HIGH": 3, |
| 14 | + "MEDIUM": 0, |
| 15 | + "INFO": 0 |
| 16 | + }, |
| 17 | + "fp_breakdown": { |
| 18 | + "braces_imbalance_all": 37, |
| 19 | + "circular_lock_ordering_all": 108, |
| 20 | + "data_race_unconfirmed": 11, |
| 21 | + "sql_injection_mock_strings": 7, |
| 22 | + "stub_compile_gated": 1, |
| 23 | + "other": 0 |
| 24 | + } |
| 25 | + }, |
| 26 | + "findings": [ |
| 27 | + { |
| 28 | + "id": "LLM-CRIT-001", |
| 29 | + "file": "src/llm/docs_assistant.cpp", |
| 30 | + "lines": [678, 683], |
| 31 | + "pattern": "prompt_injection", |
| 32 | + "original_severity": "CRITICAL", |
| 33 | + "verified_severity": "CRITICAL", |
| 34 | + "classification": "Real Gap", |
| 35 | + "rationale": "User-supplied 'topic' and 'error_description' parameters are concatenated directly into LLM prompt strings without passing through PromptPolicy::evaluate() or any other sanitisation. Attacker-controlled input can redirect LLM behaviour, exfiltrate context, or bypass guardrails.", |
| 36 | + "code_snippet": "std::string query = \"How do I configure \" + topic + \" in ThemisDB?...\";\nstd::string query = \"I'm experiencing this issue: \" + error_description + \"...\";" |
| 37 | + }, |
| 38 | + { |
| 39 | + "id": "LLM-CRIT-002", |
| 40 | + "file": "src/llm/ai_orchestrator.cpp", |
| 41 | + "lines": [264, 269, 280, 289], |
| 42 | + "pattern": "deadlock_risk", |
| 43 | + "original_severity": "CRITICAL", |
| 44 | + "verified_severity": "CRITICAL", |
| 45 | + "classification": "Real Gap", |
| 46 | + "rationale": "PluginAdapterApplyService::applyAdapter() acquires non-reentrant mutex_ at line 264 and then calls plugin->unloadLoRA(), path_resolver_(), and plugin->loadLoRA() while holding it. Any of these callbacks may re-enter AIOrchestrator::currentAdapter() or ::isModelLoaded() which also acquire mutex_, producing a non-reentrant deadlock. The trigger path is confirmed: currentAdapter() (line 299) acquires mutex_ unconditionally.", |
| 47 | + "code_snippet": "std::lock_guard<std::mutex> lock(mutex_); // line 264 — locked\n...\nconst bool unload_ok = plugin->unloadLoRA(...); // line 269 — external call under lock\nconst auto resolved = path_resolver_(...); // line 280 — callback under lock\nconst bool ok = plugin->loadLoRA(...); // line 289 — external call under lock" |
| 48 | + }, |
| 49 | + { |
| 50 | + "id": "LLM-HIGH-001", |
| 51 | + "file": "src/llm/model_downloader.cpp", |
| 52 | + "lines": [150, 239], |
| 53 | + "pattern": "path_traversal", |
| 54 | + "original_severity": "CRITICAL", |
| 55 | + "verified_severity": "HIGH", |
| 56 | + "classification": "Real Gap", |
| 57 | + "rationale": "config.model_name is not validated for '../', '/', '\\', or null-byte sequences before constructing filesystem paths. model_name can be supplied via YAML config (line 626) or as an API parameter. Downgraded from CRITICAL because model_name is typically operator-configured rather than directly user-supplied at runtime.", |
| 58 | + "code_snippet": "std::string expected_path = config.download_dir + \"/\" + config.model_name + \".gguf\";" |
| 59 | + }, |
| 60 | + { |
| 61 | + "id": "LLM-HIGH-002", |
| 62 | + "file": "src/llm/model_downloader.cpp", |
| 63 | + "lines": [595, 108, 135], |
| 64 | + "pattern": "insecure_model_url", |
| 65 | + "original_severity": "CRITICAL", |
| 66 | + "verified_severity": "HIGH", |
| 67 | + "classification": "Real Gap", |
| 68 | + "rationale": "Default Ollama URL is http://localhost:11434 (plaintext). validateOllamaUrl() issues only a WARN for non-local HTTP targets and returns true (permitting the connection). Production deployments pointing at a remote Ollama endpoint can transmit model weights over an unencrypted channel. Downgraded from CRITICAL because localhost deployments (the common case) are unaffected.", |
| 69 | + "code_snippet": ": \"http://localhost:11434\"; // line 595 — plaintext default\n...\nTHEMIS_WARN(\"plain HTTP used for non-local endpoint\"); // line 135 — warn only, not reject\nreturn true; // always accepts" |
| 70 | + }, |
| 71 | + { |
| 72 | + "id": "LLM-HIGH-003", |
| 73 | + "file": "src/llm/llm_prefix_cache.cpp", |
| 74 | + "lines": [46], |
| 75 | + "pattern": "hardcoded_path", |
| 76 | + "original_severity": "CRITICAL", |
| 77 | + "verified_severity": "HIGH", |
| 78 | + "classification": "Real Gap", |
| 79 | + "rationale": "Cache directory hardcoded to /tmp/themis_llm_prefix_cache with no configurable override. In multi-instance deployments, all processes share the same /tmp path, causing race conditions on cache files. World-readable /tmp also exposes cached embeddings. Downgraded from CRITICAL because exploitation requires multi-tenant shared-host deployments.", |
| 80 | + "code_snippet": "embed_config.cache_dir = \"/tmp/themis_llm_prefix_cache\";" |
| 81 | + } |
| 82 | + ], |
| 83 | + "false_positives": [ |
| 84 | + { |
| 85 | + "group": "braces_imbalance", |
| 86 | + "count": 37, |
| 87 | + "pattern": "braces_imbalance + braces_imbalance_midfile", |
| 88 | + "original_severity": "CRITICAL", |
| 89 | + "verified_severity": null, |
| 90 | + "classification": "False-Positive", |
| 91 | + "rationale": "State-machine brace counter confirmed all 113 .cpp files terminate at depth 0. Scanner FP causes: (1) C++14 digit separators like 10'000 parsed as char literals '000' by simple regex; (2) Multi-line raw string literals R\"(...)\" containing unescaped { and } characters; (3) char literals like '\"' and '\\\\' causing string-stripping regex to misidentify string boundaries. All flagged files end correctly with } // namespace llm / } // namespace themis." |
| 92 | + }, |
| 93 | + { |
| 94 | + "group": "circular_lock_ordering", |
| 95 | + "count": 108, |
| 96 | + "pattern": "circular_lock_ordering", |
| 97 | + "original_severity": "CRITICAL", |
| 98 | + "verified_severity": null, |
| 99 | + "classification": "False-Positive", |
| 100 | + "rationale": "Heuristic fires on any file using more than one mutex name. Manual inspection of all top mutex-heavy files (inference_engine_enhanced.cpp 70 uses, multi_lora_manager.cpp 65 uses, gpu_memory_manager.cpp 47 uses, async_inference_engine.cpp 45 uses, ml_model_manager.cpp 28 uses) found zero ABBA lock-ordering inversions. Most files use a single mutex_ throughout; those with multiple mutexes acquire them in consistent documented order or in non-overlapping scopes." |
| 101 | + }, |
| 102 | + { |
| 103 | + "group": "data_race", |
| 104 | + "count": 11, |
| 105 | + "pattern": "data_race", |
| 106 | + "original_severity": "CRITICAL", |
| 107 | + "verified_severity": null, |
| 108 | + "classification": "False-Positive", |
| 109 | + "rationale": "All shared mutable state in inspected files is protected: counters use std::atomic<>, collections are guarded by std::mutex or std::shared_mutex. gpu_available_ in GPUMemoryManager is written only during constructor (single-threaded) and read-only thereafter. Scanner FP fires on non-const member variables in classes that happen to contain a std::thread member." |
| 110 | + }, |
| 111 | + { |
| 112 | + "group": "sql_injection", |
| 113 | + "count": 7, |
| 114 | + "pattern": "sql_injection", |
| 115 | + "original_severity": "CRITICAL", |
| 116 | + "verified_severity": null, |
| 117 | + "classification": "False-Positive", |
| 118 | + "rationale": "All 7 instances are either: (a) hardcoded mock AQL strings in llm_client_default.cpp (test/stub code, no user input concatenated); (b) RPC protocol envelope strings in distributed_training_coordinator.cpp named 'rpc_query' that are not SQL; (c) internal audit log fields in moral_analyzer.cpp. No parameterised SQL construction from user input was found." |
| 119 | + }, |
| 120 | + { |
| 121 | + "group": "embedded_llm_stub", |
| 122 | + "count": 1, |
| 123 | + "pattern": "simulation_stub_marker", |
| 124 | + "original_severity": "CRITICAL", |
| 125 | + "verified_severity": null, |
| 126 | + "classification": "False-Positive", |
| 127 | + "rationale": "embedded_llm_stub.cpp stub path (success=true with hardcoded text) is permanently gated by #ifdef THEMIS_LLM_STUB_MODE. Documentation confirms this flag is never set in release build presets. The production else-branch correctly returns success=false with a diagnostic error message." |
| 128 | + } |
| 129 | + ] |
| 130 | +} |
0 commit comments