// fold-swift — Swift SIL extraction frontend for fold. // // Invokes `swiftc -emit-sil` on .swift inputs and // emits fold NDJSON with function findings and interface metadata. // // Two analysis paths: // 1. Primary: SWAN-inspired SIL parser (swan_sil_parser) — structured, // recursive-descent parser that produces sil-free RustExprNode IR // trees and detects security-relevant SIL patterns. // 2. Fallback: regex-based heuristic scan — catches SIL constructs // the structured parser may miss (uncommon patterns, new SIL // instructions in newer Swift versions). // // Detectors (both paths): // - unchecked_ref_cast / unchecked_addr_cast → swift-unsafe-cast // - load through UnsafePointer → swift-unsafe-load // - cond_fail → swift-assert-failure (force-unwrap, precondition) // // Output format matches fold-rust's NDJSON so RustBackend can // consume these lines alongside Rust findings. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "swan_sil_parser.hpp" namespace fs = std::filesystem; using json = nlohmann::json; extern char** environ; namespace { struct SubprocessResult { std::string stdout_text; int exit_status = -1; bool exited_normally = false; bool timed_out = false; }; void emitInfo(const std::string& unit) { json line; line["kind"] = "info"; line["data"] = { {"message", "fold-swift MVP — SIL heuristic scan"}, {"version", "0.1.0"}, {"unit", unit}, }; std::cout << line.dump() << '\n'; } void emitError(const std::string& unit, const std::string& message) { json line; line["kind"] = "error"; line["data"] = { {"message", message}, {"unit", unit}, }; std::cout << line.dump() << '\n'; } // NDJSON wire format: the key names emitted here and in // swan_sil_parser.cpp are the cross-language contract consumed by // src/frontend/swift_backend.cpp. The single source of truth is // include/fold/frontend/ndjson_keys.hpp (fold::frontend::ndjson_keys); // finding_kind values must match findingKindName() in // src/report/finding.cpp. Add new keys to that header FIRST. void emitFinding(const std::string& file, int line_no, int col, const std::string& function, const std::string& kind, const std::string& hazard, const std::string& message) { json line; line["kind"] = "finding"; line["data"] = { {"finding_kind", kind}, {"confidence", "Heuristic"}, {"function", function}, {"source_file", file}, {"line", line_no}, {"column", col}, {"hazard", hazard}, {"message", message}, }; std::cout << line.dump() << '\n'; } void emitFunction(const std::string& file, int start_line, int end_line, const std::string& name) { json line; line["kind"] = "function"; line["data"] = { {"qualified_name", name}, {"source_file", file}, {"start_line", start_line}, {"end_line", end_line}, {"callees", json::array()}, }; std::cout << line.dump() << '\n'; } void emitIr(const std::string& function, const json& root) { json line; line["kind"] = "ir"; line["data"] = {{"function", function}, {"root", root}}; std::cout << line.dump() << '\n'; } void emitInterface(const std::string& function, const std::string& file, int lno, int param_count) { json line; auto params = json::array(); for (int i = 0; i < param_count; ++i) { params.push_back( {{"name", "swift_arg" + std::to_string(i)}, {"kind", "primitive"}, {"ty", "unknown"}}); } line["kind"] = "interface"; line["data"] = {{"function", function}, {"source_file", file}, {"line", lno}, {"params", params}, {"fuzzable", param_count > 0}}; std::cout << line.dump() << '\n'; } std::string getEnvNonEmpty(const char* name) { const char* value = std::getenv(name); return (value && *value) ? std::string(value) : std::string(); } std::optional runTool(const std::vector& argv, int timeout_seconds = 60) { if (argv.empty()) return std::nullopt; int pipe_fds[2]; if (::pipe(pipe_fds) != 0) return std::nullopt; int flags = ::fcntl(pipe_fds[0], F_GETFL, 0); if (flags >= 0) ::fcntl(pipe_fds[0], F_SETFL, flags | O_NONBLOCK); posix_spawn_file_actions_t actions; if (::posix_spawn_file_actions_init(&actions) != 0) { ::close(pipe_fds[0]); ::close(pipe_fds[1]); return std::nullopt; } bool actions_ok = true; actions_ok = actions_ok && (::posix_spawn_file_actions_adddup2(&actions, pipe_fds[1], STDOUT_FILENO) == 0); actions_ok = actions_ok && (::posix_spawn_file_actions_addclose(&actions, pipe_fds[0]) == 0); actions_ok = actions_ok && (::posix_spawn_file_actions_addclose(&actions, pipe_fds[1]) == 0); if (!actions_ok) { ::posix_spawn_file_actions_destroy(&actions); ::close(pipe_fds[0]); ::close(pipe_fds[1]); return std::nullopt; } std::vector c_argv; c_argv.reserve(argv.size() + 1); for (const auto& arg : argv) c_argv.push_back(const_cast(arg.c_str())); c_argv.push_back(nullptr); pid_t pid = -1; int spawn_rc = ::posix_spawnp(&pid, argv[0].c_str(), &actions, nullptr, c_argv.data(), environ); ::posix_spawn_file_actions_destroy(&actions); ::close(pipe_fds[1]); if (spawn_rc != 0) { ::close(pipe_fds[0]); return std::nullopt; } SubprocessResult result; bool stdout_closed = false; bool child_reaped = false; int wait_status = 0; char buffer[4096]; auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_seconds); while (!stdout_closed || !child_reaped) { int status = 0; pid_t wait_rc = ::waitpid(pid, &status, WNOHANG); if (wait_rc == pid) { child_reaped = true; wait_status = status; } else if (wait_rc < 0) { break; } while (!stdout_closed) { ssize_t nread = ::read(pipe_fds[0], buffer, sizeof(buffer)); if (nread > 0) { result.stdout_text.append(buffer, static_cast(nread)); continue; } if (nread == 0) { stdout_closed = true; break; } if (errno == EAGAIN || errno == EWOULDBLOCK) break; stdout_closed = true; break; } if (stdout_closed && child_reaped) break; auto now = std::chrono::steady_clock::now(); if (now >= deadline) { result.timed_out = true; ::kill(pid, SIGKILL); int kill_status = 0; if (::waitpid(pid, &kill_status, 0) == pid) { child_reaped = true; wait_status = kill_status; } while (!stdout_closed) { ssize_t nread = ::read(pipe_fds[0], buffer, sizeof(buffer)); if (nread > 0) { result.stdout_text.append(buffer, static_cast(nread)); } else if (nread == 0) { stdout_closed = true; } else if (errno == EAGAIN || errno == EWOULDBLOCK) { struct pollfd pfd = {pipe_fds[0], POLLIN | POLLHUP, 0}; int poll_rc = ::poll(&pfd, 1, 50); if (poll_rc <= 0) break; } else { stdout_closed = true; } } break; } int remaining_ms = static_cast( std::chrono::duration_cast(deadline - now).count()); if (remaining_ms < 0) remaining_ms = 0; struct pollfd pfd = {pipe_fds[0], POLLIN | POLLHUP, 0}; int poll_rc = ::poll(&pfd, 1, remaining_ms); if (poll_rc < 0 && errno != EINTR) break; } ::close(pipe_fds[0]); if (child_reaped) { if (WIFEXITED(wait_status)) { result.exited_normally = true; result.exit_status = WEXITSTATUS(wait_status); } else if (WIFSIGNALED(wait_status)) { result.exit_status = WTERMSIG(wait_status); } } return result; } std::string toolPathOrDefault(const char* env_var, const char* default_name) { std::string tool = getEnvNonEmpty(env_var); return tool.empty() ? std::string(default_name) : tool; } // Demangle a Swift symbol using `swift demangle`. Returns the // human-readable form or the original name on failure. std::string demangleSwiftName(const std::string& mangled) { if (mangled.size() < 2 || (mangled[0] != '$' && mangled.find("$s") != 0 && mangled.find("$S") != 0 && mangled.find("_$s") == std::string::npos)) return mangled; auto result = runTool({toolPathOrDefault("FOLD_SWIFT_DEMANGLE_BIN", "swift"), "demangle", mangled}); if (!result || result->timed_out || !result->exited_normally || result->exit_status != 0) return mangled; while (!result->stdout_text.empty() && (result->stdout_text.back() == '\n' || result->stdout_text.back() == '\r')) { result->stdout_text.pop_back(); } // `swift demangle ` echoes " ---> "; keep only // the demangled side so qualified names stay readable. std::string out = result->stdout_text; const std::string sep = " ---> "; if (const auto pos = out.find(sep); pos != std::string::npos) out = out.substr(pos + sep.size()); return out.empty() || out == mangled ? mangled : out; } std::optional runSwiftSil(const std::string& path) { // -g is required: without it swiftc emits SIL with NO source-location // attributes, and every downstream finding would report line 0 (which // fold's report filter drops as location-less). return runTool({toolPathOrDefault("FOLD_SWIFTC_BIN", "swiftc"), "-emit-sil", "-g", path}); } std::string describeSwiftcFailure(const std::optional& result) { if (!result) { return "failed to spawn swiftc -emit-sil for unit"; } if (result->timed_out) { return "swiftc -emit-sil timed out after 60 seconds"; } if (!result->exited_normally) { return "swiftc -emit-sil terminated abnormally"; } if (result->exit_status != 0) { return "swiftc -emit-sil exited with status " + std::to_string(result->exit_status); } if (result->stdout_text.empty()) { return "swiftc -emit-sil produced no SIL output"; } return "swiftc -emit-sil failed for unit"; } // Parse SIL output, emit findings for dangerous operations. void analyzeSil(const std::string& sil, const std::string& file) { static const std::regex fn_rx( R"(sil(?:\s+(?:\[[^\]]+\]|[[:alnum:]_]+))*\s+@([^\s:(]+)\s*:\s*\$@convention)"); static const std::regex line_rx(R"(//.*?(\d+):(\d+)\s+$)"); // With -g, SIL carries inline source locations: // %5 = load %4, loc "/path/file.swift":1:54, scope 2 // user: %7 // These are far more precise than the trailing-comment heuristic, so // prefer them whenever present on the line. static const std::regex inline_loc_rx(R"__(\bloc \*? "([^"]+)":([0-9]+):([0-9]+))__"); static const std::regex cast_rx( R"((unchecked_ref_cast|unchecked_addr_cast|unchecked_trivial_bit_cast|unchecked_bitwise_cast|unchecked_ref_cast_addr))"); static const std::regex unsafe_pointer_rx( R"((UnsafePointer|UnsafeMutablePointer|UnsafeRawPointer|UnsafeMutableRawPointer|UnsafeBufferPointer|UnsafeMutableBufferPointer|UnsafeRawBufferPointer|UnsafeMutableRawBufferPointer|OpaquePointer|AutoreleasingUnsafeMutablePointer|CVaListPointer|UnsafeRawSpan))"); static const std::regex load_store_rx(R"(\b(load|load_borrow|load_weak|store)\b)"); static const std::regex cond_fail_rx(R"(\bcond_fail\b)"); static const std::regex cond_fail_msg_rx(R"__(cond_fail\s+.*?,\s*"([^"]*)")__"); static const std::regex assert_failure_rx( R"((_assertionFailure|_diagnoseUnexpectedNilOptional))"); std::string current_fn; std::string current_display_name; int current_line = 0; int current_col = 0; bool current_fn_emitted = false; bool current_fn_has_pointer_signature = false; std::istringstream iss(sil); std::string sil_line; auto emitPendingFunction = [&]() { if (current_fn.empty() || current_fn_emitted) return; emitFunction(file, current_line, current_line + 1, current_display_name.empty() ? current_fn : current_display_name); emitInterface(current_display_name.empty() ? current_fn : current_display_name, file, current_line, 0); emitIr(current_display_name.empty() ? current_fn : current_display_name, json::object()); current_fn_emitted = true; }; while (std::getline(iss, sil_line)) { // Prefer inline `loc "file":line:col` attributes (-g output) and // fall back to trailing `// file:line:col` comments. std::smatch inline_loc; if (std::regex_search(sil_line, inline_loc, inline_loc_rx)) { try { current_line = std::stoi(inline_loc[2].str()); current_col = std::stoi(inline_loc[3].str()); } catch (const std::exception&) { // Invalid line/col in inline loc; keep previous annotation. } } else { std::smatch loc_match; if (std::regex_search(sil_line, loc_match, line_rx) && loc_match.size() >= 3) { try { current_line = std::stoi(loc_match[1].str()); current_col = std::stoi(loc_match[2].str()); } catch (const std::exception&) { // Invalid line/col in SIL comment; skip this annotation. } } } if (!current_fn.empty() && !current_fn_emitted && current_line > 0) { emitPendingFunction(); } // Function declarations. std::smatch fn_match; if (std::regex_search(sil_line, fn_match, fn_rx)) { emitPendingFunction(); current_fn = fn_match[1].str(); current_display_name = demangleSwiftName(current_fn); current_fn_emitted = false; current_fn_has_pointer_signature = std::regex_search(sil_line, unsafe_pointer_rx); continue; } if (current_fn.empty()) continue; // Unchecked casts. std::smatch cast_match; if (std::regex_search(sil_line, cast_match, cast_rx)) { emitFinding(file, current_line, current_col, current_fn, "swift-unsafe-cast", "swift-" + cast_match[1].str(), cast_match[1].str() + " bypasses Swift type safety; verify layout and provenance"); continue; } // Load through raw pointer. if (std::regex_search(sil_line, load_store_rx) && std::regex_search(sil_line, unsafe_pointer_rx)) { const bool is_store = sil_line.find("store") != std::string::npos; emitFinding( file, current_line, current_col, current_fn, "swift-unsafe-load", is_store ? "swift-unsafe-store" : "swift-unsafe-load", is_store ? "store through pointer type; verify pointer validity and lifetime for writes" : "load through pointer type; verify pointer validity"); continue; } if (sil_line.find("store") != std::string::npos && current_fn_has_pointer_signature) { emitFinding( file, current_line, current_col, current_fn, "swift-unsafe-load", "swift-unsafe-store", "store through pointer type; verify pointer validity and lifetime for writes"); continue; } if (std::regex_search(sil_line, cond_fail_rx)) { std::string message = "cond_fail (force-unwrap / precondition failure)"; std::smatch msg_match; if (std::regex_search(sil_line, msg_match, cond_fail_msg_rx) && msg_match.size() >= 2) { message = "cond_fail: " + msg_match[1].str(); } emitFinding(file, current_line, current_col, current_fn, "swift-assert-failure", "swift-cond_fail", message); continue; } if (std::regex_search(sil_line, assert_failure_rx)) { emitFinding(file, current_line, current_col, current_fn, "swift-assert-failure", "swift-cond_fail", "assertion failure path (force-unwrap / precondition failure)"); continue; } } emitPendingFunction(); } } // namespace int main(int argc, char** argv) { if (argc < 2) { std::cerr << "usage: fold-swift \n"; std::cerr << " fold-swift --help\n"; return 2; } // Handle --help / -h before any path validation. std::string first_arg(argv[1]); if (first_arg == "--help" || first_arg == "-h") { std::cout << "fold-swift — Swift SIL extraction frontend for fold\n" << "\n" << "USAGE:\n" << " fold-swift \n" << " fold-swift --help\n" << "\n" << "Invokes swiftc -emit-sil on each .swift input and emits\n" << "fold NDJSON with function, finding, summary, and interface\n" << "records to stdout.\n" << "\n" << "DETECTORS (heuristic):\n" << " unchecked_ref_cast / unchecked_addr_cast \u2192 swift-unsafe-cast\n" << " load through UnsafePointer \u2192 swift-unsafe-load\n" << " force-unwrap / cond_fail \u2192 swift-assert-failure\n" << "\n" << "ENVIRONMENT:\n" << " FOLD_SWIFTC_BIN override swiftc used for SIL extraction\n" << " FOLD_SWIFT_DEMANGLE_BIN override swift used for demangling\n" << "\n" << "Note: FOLD_SWIFT_BIN is used by the fold C++ backend to locate\n" << "this frontend binary; fold-swift itself does not read it.\n"; return 0; } bool had_unit_error = false; for (int i = 1; i < argc; ++i) { std::string path(argv[i]); if (path == "--help" || path == "-h") continue; if (!fs::exists(path)) { std::cerr << "fold-swift: " << path << " does not exist\n"; return 2; } auto sil_result = runSwiftSil(path); if (!sil_result || sil_result->timed_out || !sil_result->exited_normally || sil_result->exit_status != 0 || sil_result->stdout_text.empty()) { emitError(path, describeSwiftcFailure(sil_result)); had_unit_error = true; continue; } std::string sil = std::move(sil_result->stdout_text); // Primary: SWAN-inspired structured SIL parser bool swan_success = false; try { fold_swift::SilTextParser parser(sil, path); parser.setDemangleHook(demangleSwiftName); auto mod = parser.parseModule(); if (mod.parse_success && !mod.functions.empty()) { fold_swift::SilToJsonConverter converter; converter.convertModule(mod, std::cout); swan_success = true; } } catch (const std::exception& e) { std::cerr << "fold-swift: SWAN parser error: " << e.what() << "\n"; std::cerr << "fold-swift: falling back to regex heuristics\n"; } // Fallback: regex-based heuristics for anything the // structured parser did not cover. if (!swan_success) { emitInfo(path); analyzeSil(sil, path); } } return had_unit_error ? 1 : 0; }