fold/tools/fold-swift/fold_swift.cpp
2026-07-20 14:52:50 -04:00

541 lines
20 KiB
C++

// 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 <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
#include <vector>
#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<SubprocessResult> runTool(const std::vector<std::string>& 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<char*> c_argv;
c_argv.reserve(argv.size() + 1);
for (const auto& arg : argv)
c_argv.push_back(const_cast<char*>(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<std::size_t>(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<std::size_t>(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<int>(
std::chrono::duration_cast<std::chrono::milliseconds>(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 <sym>` echoes "<mangled> ---> <demangled>"; 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<SubprocessResult> 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<SubprocessResult>& 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 <file.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 <file.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;
}