From 5e4e9d31d5492d28ce2bee2e8a9b44252bc70a3e Mon Sep 17 00:00:00 2001 From: Termux User Date: Sat, 13 Jun 2026 12:27:42 -0400 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20Round=204=20slop=20audit=20=E2=80=94?= =?UTF-8?q?=20filesystem=20reload=20sync,=20diff=20output,=20JS=20error=20?= =?UTF-8?q?propagation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H-1: Add filesystem capability sync to governance mid-run reload (governance_config.cpp) — FS_READ/FS_WRITE now removed when config tightens filesystem.mode, matching startup syncGovernanceToSandbox(). H-2: --diff formatter flag now shows unified diff output (main.cpp) instead of printing the full formatted text. M-1: JS executor re-throws on error (js_executor_adapter.cpp) instead of swallowing exceptions and returning null — matches Python executor. M-2: Document watch evaluation limitation in debugger (debugger.cpp). Co-Authored-By: Claude Opus 4.6 --- src/cli/main.cpp | 36 ++++++++++++++++++++++++++++- src/debugger/debugger.cpp | 2 ++ src/runtime/governance_config.cpp | 6 +++++ src/runtime/js_executor_adapter.cpp | 2 +- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/cli/main.cpp b/src/cli/main.cpp index e4498d02..c047f35e 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -2851,7 +2851,41 @@ int main(int argc, char** argv) { } else { fmt::print("✗ {} needs formatting\n", filename); if (show_diff) { - fmt::print("\nFormatted output:\n{}\n", formatted); + // Generate unified diff between source and formatted + std::istringstream src_stream(source); + std::istringstream fmt_stream(formatted); + std::vector src_lines, fmt_lines; + std::string line; + while (std::getline(src_stream, line)) src_lines.push_back(line); + while (std::getline(fmt_stream, line)) fmt_lines.push_back(line); + fmt::print("\n--- {}\n+++ {} (formatted)\n", filename, filename); + size_t si = 0, fi = 0; + while (si < src_lines.size() || fi < fmt_lines.size()) { + if (si < src_lines.size() && fi < fmt_lines.size() && + src_lines[si] == fmt_lines[fi]) { + fmt::print(" {}\n", src_lines[si]); + si++; fi++; + } else { + // Find next matching line + size_t match_s = si, match_f = fi; + bool found = false; + for (size_t d = 1; d < 10 && !found; d++) { + for (size_t ds = 0; ds <= d && !found; ds++) { + size_t df = d - ds; + if (si + ds < src_lines.size() && fi + df < fmt_lines.size() && + src_lines[si + ds] == fmt_lines[fi + df]) { + match_s = si + ds; match_f = fi + df; found = true; + } + } + } + while (si < match_s) { fmt::print("-{}\n", src_lines[si++]); } + while (fi < match_f) { fmt::print("+{}\n", fmt_lines[fi++]); } + if (!found) { + if (si < src_lines.size()) fmt::print("-{}\n", src_lines[si++]); + if (fi < fmt_lines.size()) fmt::print("+{}\n", fmt_lines[fi++]); + } + } + } } return 1; } diff --git a/src/debugger/debugger.cpp b/src/debugger/debugger.cpp index 6ec5b51c..1afdb511 100644 --- a/src/debugger/debugger.cpp +++ b/src/debugger/debugger.cpp @@ -287,6 +287,8 @@ std::map Debugger::listGlobalVariables() { int Debugger::addWatch(const std::string& expression) { int id = next_watch_id_++; watches_[id] = expression; + // Note: watch expressions are stored but evaluation is not yet implemented. + // evaluateWatches() will return an error for each watch until this is completed. return id; } diff --git a/src/runtime/governance_config.cpp b/src/runtime/governance_config.cpp index b35af5b6..82675db3 100644 --- a/src/runtime/governance_config.cpp +++ b/src/runtime/governance_config.cpp @@ -3268,6 +3268,12 @@ bool GovernanceEngine::reloadIfChanged() { sb->setAllowExec(false); sb->removeCapability(security::Capability::SYS_EXEC); } + if (new_rp->capabilities.filesystem.mode == "none") { + sb->removeCapability(security::Capability::FS_READ); + sb->removeCapability(security::Capability::FS_WRITE); + } else if (new_rp->capabilities.filesystem.mode == "read") { + sb->removeCapability(security::Capability::FS_WRITE); + } } // Re-configure BSD/CDD detectors with new rules diff --git a/src/runtime/js_executor_adapter.cpp b/src/runtime/js_executor_adapter.cpp index 5bc86e98..a732d826 100644 --- a/src/runtime/js_executor_adapter.cpp +++ b/src/runtime/js_executor_adapter.cpp @@ -60,7 +60,7 @@ interpreter::NaabVal JsExecutorAdapter::executeWithReturn( " - NAAb strings are passed as JS strings\n\n"); } - return interpreter::NaabVal::makeNull(); // Return null on error + throw std::runtime_error(std::string("JavaScript execution error: ") + error_msg); } } From 0655c8eb4270c5075b208896c6a3f13265d8acf3 Mon Sep 17 00:00:00 2001 From: Termux User Date: Sat, 13 Jun 2026 13:40:16 -0400 Subject: [PATCH 2/3] fix: improve python_c_executor callFunction() stub error message (M-4) Replaces terse "not yet implemented" with actionable guidance pointing to executeWithReturn() or the subprocess-based executor as alternatives. Co-Authored-By: Claude Opus 4.6 --- src/runtime/python_c_executor.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/runtime/python_c_executor.cpp b/src/runtime/python_c_executor.cpp index aafcd82f..65981c0c 100644 --- a/src/runtime/python_c_executor.cpp +++ b/src/runtime/python_c_executor.cpp @@ -654,7 +654,13 @@ interpreter::NaabVal PythonCExecutor::callFunction( const std::string& function_name, const std::vector& args ) { - throw std::runtime_error("PythonCExecutor::callFunction() not yet implemented for C API"); + (void)function_name; + (void)args; + throw std::runtime_error( + "Python C API error: callFunction() is not implemented\n\n" + " Help:\n" + " - Use executeWithReturn() to run Python code instead\n" + " - Or use the subprocess-based Python executor (default)\n"); } /** From 4bcfb31f384f899bee474500f2cd2a7afadd441f Mon Sep 17 00:00:00 2001 From: Termux User Date: Sat, 13 Jun 2026 15:19:57 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20Round=205=20slop=20audit=20=E2=80=94?= =?UTF-8?q?=20Unicode=20bypass,=20import=20scanning,=20config=20type=20gua?= =?UTF-8?q?rds,=20lockfile=20safety?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H-1: Add subscript (U+2090-U+209C) and superscript (U+2070-U+207F, U+00B2/B3/B9) normalization to governance Unicode scanner. Closes bypass vector where os.systeₘ() evaded pattern matching. M-1: Add dangerous Python import patterns (import os/subprocess/shutil/ ctypes/pty/commands) to checkCodeInjection() pre-execution scan. M-2: Add .is_string()/.is_boolean()/.is_number_integer() type guards to critical governance_config.cpp fields (version, extends, description, per-language timeout/max_lines/max_output_size, require_explicit, mode). M-3: Replace bare catch(...) in lockfile.cpp with logged exception + parse_failed flag that prevents save() from overwriting valid data. Co-Authored-By: Claude Opus 4.6 --- include/naab/lockfile.h | 1 + src/runtime/governance_checks.cpp | 48 +++++++++++++++++++++++++++++++ src/runtime/governance_config.cpp | 18 ++++++------ src/runtime/lockfile.cpp | 13 ++++++++- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/include/naab/lockfile.h b/include/naab/lockfile.h index 62ee83a2..7e4b7cc3 100644 --- a/include/naab/lockfile.h +++ b/include/naab/lockfile.h @@ -27,6 +27,7 @@ struct Lockfile { std::string naab_version; // NAAb version string (e.g., "0.7.0") std::string platform; // "linux/arm64", "darwin/arm64", etc. std::vector runtimes; + bool parse_failed = false; // True if lockfile JSON was malformed // Load from file. Returns empty Lockfile if file doesn't exist. static Lockfile load(const std::string& path); diff --git a/src/runtime/governance_checks.cpp b/src/runtime/governance_checks.cpp index 9efd80f5..cc8d05f2 100644 --- a/src/runtime/governance_checks.cpp +++ b/src/runtime/governance_checks.cpp @@ -205,6 +205,11 @@ static std::string normalizeUnicode(const std::string& code) { // U+00AD SOFT HYPHEN if (cp == 0x00AD) { i += 1; continue; } + // Superscript digits in Latin-1 Supplement + if (cp == 0x00B2) { result += '2'; i += 1; continue; } // ² + if (cp == 0x00B3) { result += '3'; i += 1; continue; } // ³ + if (cp == 0x00B9) { result += '1'; i += 1; continue; } // ¹ + // Cyrillic confusables (U+0400-U+04FF) char replacement = 0; switch (cp) { @@ -270,6 +275,46 @@ static std::string normalizeUnicode(const std::string& code) { continue; } + // Superscript letters/digits (U+2070-U+207F) + // U+2070=⁰, U+00B9=¹ (2-byte), U+00B2=² (2-byte), U+00B3=³ (2-byte) + // U+2074=⁴, U+2075=⁵, U+2076=⁶, U+2077=⁷, U+2078=⁸, U+2079=⁹ + // U+207F=ⁿ + if (cp >= 0x2074 && cp <= 0x2079) { + result += static_cast('0' + (cp - 0x2070)); + i += 2; continue; + } + if (cp == 0x2070) { result += '0'; i += 2; continue; } + if (cp == 0x207F) { result += 'n'; i += 2; continue; } + + // Subscript letters/digits (U+2080-U+209C) + // U+2080=₀..U+2089=₉, U+2090=ₐ, U+2091=ₑ, U+2092=ₒ, + // U+2093=ₓ, U+2094=ₔ, U+2095=ₕ, U+2096=ₖ, U+2097=ₗ, + // U+2098=ₘ, U+2099=ₙ, U+209A=ₚ, U+209B=ₛ, U+209C=ₜ + if (cp >= 0x2080 && cp <= 0x2089) { + result += static_cast('0' + (cp - 0x2080)); + i += 2; continue; + } + { + char sub_rep = 0; + switch (cp) { + case 0x2090: sub_rep = 'a'; break; // ₐ + case 0x2091: sub_rep = 'e'; break; // ₑ + case 0x2092: sub_rep = 'o'; break; // ₒ + case 0x2093: sub_rep = 'x'; break; // ₓ + case 0x2094: sub_rep = 'e'; break; // ₔ (schwa → e) + case 0x2095: sub_rep = 'h'; break; // ₕ + case 0x2096: sub_rep = 'k'; break; // ₖ + case 0x2097: sub_rep = 'l'; break; // ₗ + case 0x2098: sub_rep = 'm'; break; // ₘ + case 0x2099: sub_rep = 'n'; break; // ₙ + case 0x209A: sub_rep = 'p'; break; // ₚ + case 0x209B: sub_rep = 's'; break; // ₛ + case 0x209C: sub_rep = 't'; break; // ₜ + default: break; + } + if (sub_rep) { result += sub_rep; i += 2; continue; } + } + // Fullwidth Latin letters (U+FF21-FF3A = A-Z, U+FF41-FF5A = a-z) if (cp >= 0xFF21 && cp <= 0xFF3A) { result += static_cast('A' + (cp - 0xFF21)); @@ -5276,6 +5321,9 @@ std::string GovernanceEngine::checkCodeInjection(const std::string& language, pats.push_back("os\\.system\\s*\\("); pats.push_back("subprocess\\.(?:call|Popen|run|check_output|check_call)\\s*\\("); pats.push_back("ctypes\\.(?:CDLL|cdll)\\s*\\("); + // Block dangerous imports (bare import grants access to system calls) + pats.push_back("\\bimport\\s+(?:os|subprocess|shutil|ctypes|pty|commands)\\b"); + pats.push_back("\\bfrom\\s+(?:os|subprocess|shutil|ctypes|pty|commands)\\b"); } else if (language == "go" || language == "golang") { pats.push_back("exec\\.Command\\s*\\("); } else if (language == "rust") { diff --git a/src/runtime/governance_config.cpp b/src/runtime/governance_config.cpp index 82675db3..445b9442 100644 --- a/src/runtime/governance_config.cpp +++ b/src/runtime/governance_config.cpp @@ -499,17 +499,17 @@ static void loadFromJson(const nlohmann::json& j, GovernanceRules& rules_) { } // --- V3.0 Expanded Sections --- - if (j.contains("version")) + if (j.contains("version") && j["version"].is_string()) rules_.version = j["version"].get(); - if (j.contains("extends")) + if (j.contains("extends") && j["extends"].is_string()) rules_.extends_path = j["extends"].get(); - if (j.contains("description")) + if (j.contains("description") && j["description"].is_string()) rules_.description = j["description"].get(); // V3 Languages: per_language configs if (j.contains("languages") && j["languages"].is_object()) { auto& lang = j["languages"]; - if (lang.contains("require_explicit")) { + if (lang.contains("require_explicit") && lang["require_explicit"].is_boolean()) { rules_.languages.require_explicit = lang["require_explicit"].get(); rules_.explicitly_set.insert("languages.require_explicit"); } // Sync to new struct too @@ -519,10 +519,10 @@ static void loadFromJson(const nlohmann::json& j, GovernanceRules& rules_) { if (lang.contains("per_language") && lang["per_language"].is_object()) { for (auto& [lang_name, cfg] : lang["per_language"].items()) { LanguageConfig lc; - if (cfg.contains("timeout")) lc.timeout = cfg["timeout"].get(); - if (cfg.contains("max_lines")) lc.max_lines = cfg["max_lines"].get(); - if (cfg.contains("max_output_size")) lc.max_output_size = cfg["max_output_size"].get(); - if (cfg.contains("version_hint")) lc.version_hint = cfg["version_hint"].get(); + if (cfg.contains("timeout") && cfg["timeout"].is_number_integer()) lc.timeout = cfg["timeout"].get(); + if (cfg.contains("max_lines") && cfg["max_lines"].is_number_integer()) lc.max_lines = cfg["max_lines"].get(); + if (cfg.contains("max_output_size") && cfg["max_output_size"].is_number_integer()) lc.max_output_size = cfg["max_output_size"].get(); + if (cfg.contains("version_hint") && cfg["version_hint"].is_string()) lc.version_hint = cfg["version_hint"].get(); if (cfg.contains("dangerous_calls")) { auto [en, lv] = parseEnforcementLevel(cfg["dangerous_calls"]); @@ -555,7 +555,7 @@ static void loadFromJson(const nlohmann::json& j, GovernanceRules& rules_) { } if (cfg.contains("imports") && cfg["imports"].is_object()) { auto& imp = cfg["imports"]; - if (imp.contains("mode")) lc.imports.mode = imp["mode"].get(); + if (imp.contains("mode") && imp["mode"].is_string()) lc.imports.mode = imp["mode"].get(); if (imp.contains("blocked")) for (auto& b : imp["blocked"]) lc.imports.blocked.push_back(b.get()); if (imp.contains("allowed")) diff --git a/src/runtime/lockfile.cpp b/src/runtime/lockfile.cpp index f35fe417..daa0da96 100644 --- a/src/runtime/lockfile.cpp +++ b/src/runtime/lockfile.cpp @@ -86,8 +86,13 @@ Lockfile Lockfile::load(const std::string& path) { lf.runtimes.push_back(e); } } + } catch (const std::exception& e) { + fprintf(stderr, "[lockfile] Warning: failed to parse %s: %s\n", + path.c_str(), e.what()); + lf.parse_failed = true; } catch (...) { - // Malformed lockfile — return with only the data we could parse + fprintf(stderr, "[lockfile] Warning: failed to parse %s\n", path.c_str()); + lf.parse_failed = true; } return lf; } @@ -97,6 +102,12 @@ Lockfile Lockfile::load(const std::string& path) { // ============================================================================ void Lockfile::save(const std::string& path) const { + // Don't overwrite a valid lockfile with data from a failed parse + if (parse_failed) { + fprintf(stderr, "[lockfile] Skipping save — parse failed, preserving existing %s\n", + path.c_str()); + return; + } // Create parent directory if needed std::filesystem::path p(path); if (p.has_parent_path()) {