diff --git a/resolve-facts/CMakeLists.txt b/resolve-facts/CMakeLists.txt index b8102da84..ac91088b9 100644 --- a/resolve-facts/CMakeLists.txt +++ b/resolve-facts/CMakeLists.txt @@ -150,6 +150,7 @@ file(GLOB_RECURSE SRC ) file(GLOB_RECURSE LIB + "${CMAKE_CURRENT_SOURCE_DIR}/include/reach/*.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/reach/*.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/libs/reach/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/libs/reach/*.hpp" @@ -159,6 +160,7 @@ file(GLOB_RECURSE LIB add_library(libreach libs/reach/distmap.cpp libs/reach/facts.cpp + libs/reach/ffi.cpp libs/reach/graph.cpp libs/reach/search.cpp libs/reach/util.cpp diff --git a/resolve-facts/include/reach/ffi.h b/resolve-facts/include/reach/ffi.h new file mode 100644 index 000000000..c54a8c624 --- /dev/null +++ b/resolve-facts/include/reach/ffi.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Riverside Research. + * LGPL-3; See LICENSE.txt in the repo root for details. + */ + +#pragma once + +#include +#include + +#ifdef __cplusplus +#include "facts_rs.hpp" +using ReachFactsBuf = facts_rs::FactsBuf; +extern "C" { +#else +typedef struct ReachFactsBuf ReachFactsBuf; +#endif + +typedef struct ReachGraph ReachGraph; +typedef struct ReachQueryResult ReachQueryResult; +typedef struct ReachError ReachError; + +typedef struct ReachStringView { + const uint8_t *data; + size_t len; +} ReachStringView; + +typedef struct ReachLoadedSymbol { + ReachStringView symbol; + ReachStringView library; +} ReachLoadedSymbol; + +typedef struct ReachBuildOptions { + const ReachLoadedSymbol *loaded_symbols; + size_t loaded_symbol_count; + uint8_t dynlink; + uint8_t filter_loaded_symbols; +} ReachBuildOptions; + +typedef struct ReachNodeId { + uint32_t module; + uint32_t node; +} ReachNodeId; + +typedef uint8_t ReachEdgeType; +enum { + REACH_EDGE_DIRECT_CALL = 0, + REACH_EDGE_INDIRECT_CALL = 1, + REACH_EDGE_CONTAINS = 2, + REACH_EDGE_SUCCESSOR = 3, + REACH_EDGE_EXTERNAL = 4, + REACH_EDGE_EXTERNAL_INDIRECT_CALL = 5, +}; + +typedef struct ReachPathView { + const ReachNodeId *nodes; + size_t node_count; + const ReachEdgeType *edges; + size_t edge_count; +} ReachPathView; + +// Borrows facts and all option slices only for this call. The returned graph +// owns only the derived reachability graph and must be freed by the caller. +ReachGraph *reach_graph_build(const ReachFactsBuf *facts, + const ReachBuildOptions *options, + ReachError **error); +void reach_graph_free(ReachGraph *graph); +size_t reach_graph_edge_count(const ReachGraph *graph); + +ReachQueryResult *reach_graph_query(const ReachGraph *graph, ReachNodeId src, + ReachNodeId dst, size_t max_paths, + ReachError **error); +void reach_query_result_free(ReachQueryResult *result); +size_t reach_query_result_path_count(const ReachQueryResult *result); +// The returned slices borrow result and remain valid until it is freed. +uint8_t reach_query_result_path(const ReachQueryResult *result, size_t index, + ReachPathView *path); + +const uint8_t *reach_error_data(const ReachError *error); +size_t reach_error_len(const ReachError *error); +void reach_error_free(ReachError *error); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/resolve-facts/libs/reach/ffi.cpp b/resolve-facts/libs/reach/ffi.cpp new file mode 100644 index 000000000..c3733e3a1 --- /dev/null +++ b/resolve-facts/libs/reach/ffi.cpp @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2025 Riverside Research. + * LGPL-3; See LICENSE.txt in the repo root for details. + */ + +#include "reach/ffi.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "reach/graph.hpp" +#include "reach/search.hpp" + +struct ReachGraph { + graph::T value; +}; + +struct ReachPath { + std::vector nodes; + std::vector edges; +}; + +struct ReachQueryResult { + std::vector paths; +}; + +struct ReachError { + std::string message; +}; + +namespace { + +void clear_error(ReachError **error) { + if (error) { + *error = nullptr; + } +} + +void set_error(ReachError **error, std::string message) { + if (error) { + *error = new ReachError{std::move(message)}; + } +} + +std::string copy_string(const ReachStringView string) { + if (string.len == 0) { + return {}; + } + if (!string.data) { + throw std::invalid_argument("null loaded-symbol string"); + } + return {reinterpret_cast(string.data), string.len}; +} + +std::optional> +loaded_symbols(const ReachBuildOptions *options) { + if (!options || options->filter_loaded_symbols == 0) { + return {}; + } + if (options->loaded_symbol_count != 0 && !options->loaded_symbols) { + throw std::invalid_argument("null loaded-symbol array"); + } + + std::vector symbols; + symbols.reserve(options->loaded_symbol_count); + for (size_t i = 0; i < options->loaded_symbol_count; ++i) { + const auto &symbol = options->loaded_symbols[i]; + symbols.push_back( + {copy_string(symbol.symbol), copy_string(symbol.library)}); + } + return symbols; +} + +NNodeId node_id(const ReachNodeId id) { return {id.module, id.node}; } + +ReachNodeId node_id(const NNodeId id) { return {id.first, id.second}; } + +ReachEdgeType edge_type(const graph::EdgeType type) { + switch (type) { + case graph::EdgeType::DirectCall: + return REACH_EDGE_DIRECT_CALL; + case graph::EdgeType::IndirectCall: + return REACH_EDGE_INDIRECT_CALL; + case graph::EdgeType::Contains: + return REACH_EDGE_CONTAINS; + case graph::EdgeType::Succ: + return REACH_EDGE_SUCCESSOR; + case graph::EdgeType::Extern: + return REACH_EDGE_EXTERNAL; + case graph::EdgeType::ExternIndirectCall: + return REACH_EDGE_EXTERNAL_INDIRECT_CALL; + case graph::EdgeType::Self: + throw std::logic_error("self edge cannot appear between path nodes"); + } + throw std::logic_error("unknown reach edge type"); +} + +ReachPath convert_path(const std::vector &path) { + ReachPath result; + result.nodes.reserve(path.size()); + result.edges.reserve(path.empty() ? 0 : path.size() - 1); + + for (const auto &edge : path) { + result.nodes.push_back(node_id(edge.node)); + } + std::reverse(result.nodes.begin(), result.nodes.end()); + + for (auto it = path.rbegin(); it != path.rend(); ++it) { + if (std::next(it) != path.rend()) { + result.edges.push_back(edge_type(it->type)); + } + } + return result; +} + +} // namespace + +extern "C" ReachGraph *reach_graph_build(const ReachFactsBuf *facts, + const ReachBuildOptions *options, + ReachError **error) { + clear_error(error); + try { + const auto symbols = loaded_symbols(options); + const auto dynlink = options && options->dynlink != 0; + return new ReachGraph{ + graph::build_from_program_facts(facts, dynlink, symbols)}; + } catch (const std::exception &exception) { + set_error(error, exception.what()); + } catch (...) { + set_error(error, "unknown error while building reach graph"); + } + return nullptr; +} + +extern "C" void reach_graph_free(ReachGraph *graph) { delete graph; } + +extern "C" size_t reach_graph_edge_count(const ReachGraph *graph) { + if (!graph) { + return 0; + } + + size_t count = 0; + for (const auto &[_, edges] : graph->value.edges) { + count += edges.size(); + } + return count; +} + +extern "C" ReachQueryResult *reach_graph_query(const ReachGraph *graph, + const ReachNodeId src, + const ReachNodeId dst, + const size_t max_paths, + ReachError **error) { + clear_error(error); + try { + if (!graph) { + throw std::invalid_argument("null reach graph"); + } + + const auto paths = search::k_paths_yen(graph->value.edges, node_id(dst), + node_id(src), max_paths); + auto result = std::make_unique(); + result->paths.reserve(paths.size()); + for (const auto &path : paths) { + result->paths.push_back(convert_path(path)); + } + return result.release(); + } catch (const std::exception &exception) { + set_error(error, exception.what()); + } catch (...) { + set_error(error, "unknown error while querying reach graph"); + } + return nullptr; +} + +extern "C" void reach_query_result_free(ReachQueryResult *result) { + delete result; +} + +extern "C" size_t +reach_query_result_path_count(const ReachQueryResult *result) { + return result ? result->paths.size() : 0; +} + +extern "C" uint8_t reach_query_result_path(const ReachQueryResult *result, + const size_t index, + ReachPathView *path) { + if (!result || !path || index >= result->paths.size()) { + return 0; + } + + const auto &value = result->paths[index]; + *path = { + value.nodes.data(), + value.nodes.size(), + value.edges.data(), + value.edges.size(), + }; + return 1; +} + +extern "C" const uint8_t *reach_error_data(const ReachError *error) { + return error ? reinterpret_cast(error->message.data()) + : nullptr; +} + +extern "C" size_t reach_error_len(const ReachError *error) { + return error ? error->message.size() : 0; +} + +extern "C" void reach_error_free(ReachError *error) { delete error; } diff --git a/resolve-facts/vendor/json/CMakeLists.txt b/resolve-facts/vendor/json/CMakeLists.txt index 28805f904..9d2225a0a 100644 --- a/resolve-facts/vendor/json/CMakeLists.txt +++ b/resolve-facts/vendor/json/CMakeLists.txt @@ -1,6 +1,6 @@ add_library(json INTERFACE) target_include_directories(json INTERFACE - "$/include" + "$" "$" )