541 lines
20 KiB
C++
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;
|
|
}
|