Skip to content

Commit 6101978

Browse files
committed
Add base project.
1 parent 64a7f0b commit 6101978

17 files changed

+821
-1
lines changed

.github/workflows/ci.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
8+
jobs:
9+
test:
10+
name: Tests for MetaCall
11+
strategy:
12+
fail-fast: false
13+
matrix:
14+
build: [Debug, Release]
15+
os: [macos-13, macos-14, macos-15, ubuntu-22.04, ubuntu-24.04, windows-2019, windows-2022, windows-2025]
16+
runs-on: ${{ matrix.os }}
17+
steps:
18+
- uses: actions/checkout@v4
19+
- name: Build Project
20+
uses: threeal/[email protected]
21+
with:
22+
build-dir: build
23+
options: |
24+
CMAKE_BUILD_TYPE=${{ matrix.build }}
25+
26+
- name: Run Tests
27+
shell: bash
28+
run: |
29+
cd build
30+
ctest -VV -C ${{ matrix.build }}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
build
2+
.vscode
3+
.cache

CMakeLists.txt

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
cmake_minimum_required(VERSION 3.10)
2+
3+
project(MetaCallPoC VERSION 1.0)
4+
set(CMAKE_C_STANDARD 99)
5+
set(CMAKE_CXX_STANDARD 11)
6+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
7+
8+
# Default build type
9+
if(NOT CMAKE_BUILD_TYPE)
10+
set(CMAKE_BUILD_TYPE "Debug")
11+
endif()
12+
13+
# Debug
14+
if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
15+
if(WIN32)
16+
add_compile_options(/fsanitize=address)
17+
else()
18+
add_compile_options(-fsanitize=address)
19+
add_compile_options(-fsanitize=undefined)
20+
add_link_options(-fsanitize=address)
21+
add_link_options(-fsanitize=undefined)
22+
endif()
23+
endif()
24+
25+
# Test
26+
include(CTest)
27+
28+
# Windows exports
29+
if(WIN32)
30+
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
31+
endif()
32+
33+
# PLTHook
34+
include(FetchContent)
35+
36+
set(PLTHook_SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/plthook")
37+
38+
FetchContent_Declare(PLTHook
39+
GIT_REPOSITORY https://github.com/metacall/plthook.git
40+
GIT_TAG master
41+
SOURCE_DIR ${PLTHook_SOURCE_DIR}
42+
)
43+
44+
FetchContent_MakeAvailable(PLTHook)
45+
46+
set(PLTHook_INCLUDE_DIR "${PLTHook_SOURCE_DIR}")
47+
48+
if(APPLE)
49+
set(PLTHOOK_SOURCE "${PLTHook_SOURCE_DIR}/plthook_osx.c")
50+
elseif(WIN32 OR MINGW)
51+
set(PLTHOOK_SOURCE "${PLTHook_SOURCE_DIR}/plthook_win32.c")
52+
else()
53+
set(PLTHOOK_SOURCE "${PLTHook_SOURCE_DIR}/plthook_elf.c")
54+
endif()
55+
56+
# MetaCall Library
57+
add_library(metacall SHARED source/metacall.c ${PLTHOOK_SOURCE})
58+
target_include_directories(metacall PRIVATE ${PLTHook_INCLUDE_DIR})
59+
target_link_libraries(metacall ${CMAKE_DL_LIBS})
60+
61+
# NodeJS library of NodeJS static
62+
add_library(libnode-static source/libnode.c)
63+
target_compile_definitions(libnode-static PUBLIC NODE_STATIC=1) # Add definition for compiling as "node-static"
64+
65+
# NodeJS static
66+
add_executable(node-static source/node-static.c)
67+
target_link_libraries(node-static libnode-static ${CMAKE_DL_LIBS} metacall)
68+
set_property(TARGET node-static PROPERTY ENABLE_EXPORTS ON) # Required to export the symbols on the executable
69+
add_test(NAME node-static
70+
COMMAND $<TARGET_FILE:node-static>
71+
)
72+
73+
# NodeJS library of NodeJS dynamic
74+
add_library(libnode SHARED source/libnode.c)
75+
76+
# NodeJS dynamic
77+
add_executable(node-dynamic source/node-dynamic.c)
78+
target_link_libraries(node-dynamic ${CMAKE_DL_LIBS} metacall)
79+
add_test(NAME node-dynamic
80+
COMMAND $<TARGET_FILE:node-dynamic>
81+
)
82+
83+
# Normal Executable
84+
add_executable(normal-executable source/normal-executable.c)
85+
target_link_libraries(normal-executable ${CMAKE_DL_LIBS} metacall)
86+
add_test(NAME normal-executable
87+
COMMAND $<TARGET_FILE:normal-executable>
88+
)
89+
set_property(TEST normal-executable PROPERTY ENVIRONMENT "NORMAL_EXECUTABLE=1")
90+
91+
# NodeJS Loader is linked weakly to libnode2
92+
add_library(node_loader SHARED source/node_loader.c)
93+
target_link_libraries(node_loader
94+
PRIVATE
95+
96+
# Delay load for MSVC
97+
$<$<CXX_COMPILER_ID:MSVC>:libnode2>
98+
$<$<CXX_COMPILER_ID:MSVC>:delayimp>
99+
)
100+
target_link_options(node_loader
101+
PRIVATE
102+
# TODO: Add flags for gcc?
103+
$<$<AND:$<BOOL:${APPLE}>,$<CXX_COMPILER_ID:AppleClang,Clang>>:-Wl,-undefined,dynamic_lookup>
104+
$<$<CXX_COMPILER_ID:MSVC>:/DELAYLOAD:$<TARGET_FILE_BASE_NAME:libnode2>.dll>
105+
)
106+
107+
# NodeJS library of NodeJS Loader
108+
add_library(libnode2 SHARED source/libnode2.c source/libnode2.cpp)
109+
110+
# Additional dlinfo test (unrelated to the PoC)
111+
if(NOT WIN32 AND NOT APPLE)
112+
add_executable(dlinfo-test source/dlinfo-test.c)
113+
target_link_libraries(dlinfo-test ${CMAKE_DL_LIBS})
114+
endif()

README.md

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,42 @@
1-
# plthook-poc
1+
# MetaCall PLT/IAT Hooking Proof of Concept
22
Proof of Concept for implementing PLT/IAT hooking for MetaCall.
3+
This will be used in order to allow MetaCall to be loaded in places where a runtime is already started.
4+
5+
The feature once implemented will solve the following isuses:
6+
- https://github.com/metacall/core/issues/231
7+
- https://github.com/metacall/core/issues/460
8+
- https://github.com/metacall/core/issues/493
9+
- https://github.com/metacall/core/issues/31
10+
11+
## How it works
12+
13+
First of all we have the following preconditions:
14+
- `libmetacall` which loads `libnode_loader`.
15+
- `libnode_loader` is not linked to anything but we are going to weakly link it to `libnode`, this means that in Windows it must be linked with `/DELAYLOAD`, in Linux and MacOS it must not be linked.
16+
17+
There are two possible cases, this happens before loading libnode_loader:
18+
- MetaCall is not being executed by `node.exe`, then:
19+
1) Windows:
20+
- `libmetacall` loads dynamically all the dependencies of `libnode_loader` (aka `libnode`).
21+
- We list all the symbols of each dependency (aka `libnode`) so we construct a hashmap of symbol string to symbol function pointer.
22+
- We list all the unresolved symbols of `libnode_loader` and we link them to `libnode`.
23+
24+
2) MacOS & Linux:
25+
- `libmetacall` loads dynamically all the dependencies of `libnode_loader` (aka `libnode`).
26+
- Linking is resolved by the linker automatically.
27+
28+
- MetaCall is being executed by node.exe, then we have two possible cases:
29+
1) `node.exe` compiled statically (without `libnode`):
30+
- We get all the library dependencies from `node.exe` and we do not find `libnode`, so we get the handle of the currrent process.
31+
- We list all symbols of `node.exe` and we construct a hash map a hashmap of symbol string to symbol function pointer.
32+
- We list all the unresolved symbols of `libnode_loader` and we link them to `node.exe`.
33+
34+
2) `node.exe` compiled dynamically (with `libnode`):
35+
- We get all the library dependencies from `node.exe` and we find `libnode` so we get the handle from it.
36+
- We list all the symbols of each dependency (aka `libnode`) so we construct a hashmap of symbol string to symbol function pointer of those dependencies (`libnode`).
37+
- We list all the unresolved symbols of `libnode_loader` and we link them to `libnode` of `node.exe`.
38+
39+
## Outcome
40+
41+
With this methodology we prevent loading a library that contains a runtime. This is very dangerous because numerous runtimes rely on constructors (C++ constructors of static class delacarations or C compiler dependant constructor mechanisms like GNU or Clang `__attribute__((destructor))`) that are mutually exclusive between them. So if we only load the library but we do not call method of the library, it can still cause errors.
42+
The loaders will be redirected to the proper runtime, reusing the functions and instance of the already running runtime.

source/dlinfo-test.c

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#include <stdio.h>
2+
3+
#ifndef __USE_GNU
4+
#define __USE_GNU
5+
#endif
6+
#include <dlfcn.h>
7+
#include <link.h>
8+
9+
int main() {
10+
// Open a shared library
11+
const char *lib_name = "./liblibnode.so"; // For example, the math library
12+
void *handle = dlopen(lib_name, RTLD_NOW);
13+
14+
if (!handle) {
15+
fprintf(stderr, "Error opening library %s: %s\n", lib_name, dlerror());
16+
return 1;
17+
}
18+
19+
// Retrieve the dynamic link map of the shared object using dlinfo
20+
struct link_map *map;
21+
if (dlinfo(handle, RTLD_DI_LINKMAP, &map) == 0) {
22+
printf("Link map of %s:\n", lib_name);
23+
while (map) {
24+
printf(" Name: %s, Address: %ld\n", map->l_name, map->l_addr);
25+
map = map->l_next;
26+
}
27+
} else {
28+
fprintf(stderr, "dlinfo failed: %s\n", dlerror());
29+
}
30+
31+
// Use dlsym to find a symbol in the library (e.g., "sin" in libm.so)
32+
void *symbol = dlsym(handle, "sin");
33+
if (symbol) {
34+
printf("Found symbol 'sin' at address: %p\n", symbol);
35+
} else {
36+
printf("Symbol 'sin' not found: %s\n", dlerror());
37+
}
38+
39+
// Close the library handle
40+
dlclose(handle);
41+
return 0;
42+
}

source/dynlib.h

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#ifndef DYNLYB
2+
#define DYNLYB 1
3+
4+
#include <stdio.h>
5+
#include <assert.h>
6+
7+
#if defined(WIN32) || defined(_WIN32)
8+
#include <windows.h>
9+
typedef HMODULE dyn_handle_t;
10+
#define DYN_LIBRARY(lib) lib ".dll"
11+
#define RTLD_LAZY 0 // Use this as workaround
12+
#define RTLD_GLOBAL 0
13+
#else
14+
#ifndef __USE_GNU
15+
#define __USE_GNU // This is required for dlinfo and dladdr
16+
#endif
17+
#include <dlfcn.h>
18+
typedef void* dyn_handle_t;
19+
#ifdef __APPLE__
20+
#define DYN_LIBRARY(lib) "lib" lib ".dylib"
21+
#else
22+
#define DYN_LIBRARY(lib) "lib" lib ".so"
23+
#include <link.h>
24+
#endif
25+
#endif
26+
27+
#define DYN_LIBRARY_PATH(path, lib) path DYN_LIBRARY(lib)
28+
29+
static inline int dyn_open(char *lib, int flags, dyn_handle_t *handle) {
30+
#if defined(WIN32) || defined(_WIN32)
31+
*handle = LoadLibrary(lib);
32+
if (*handle == NULL) {
33+
printf("Error: %d\n", GetLastError());
34+
return 1;
35+
}
36+
#else
37+
*handle = dlopen(lib, flags);
38+
if (*handle == NULL) {
39+
printf("Error: %s\n", dlerror());
40+
return 1;
41+
}
42+
#endif
43+
44+
return 0;
45+
}
46+
47+
static inline int dyn_sym(dyn_handle_t handle, char *sym, void (**fp)(void)) {
48+
#if defined(WIN32) || defined(_WIN32)
49+
*fp = (void (*)(void))GetProcAddress(handle, sym);
50+
if (*fp == NULL) {
51+
printf("Error: %d\n", GetLastError());
52+
FreeLibrary(handle);
53+
return 1;
54+
}
55+
#else
56+
union {
57+
void *ptr;
58+
void (*fp)(void);
59+
} cast;
60+
61+
cast.ptr = dlsym(handle, sym);
62+
63+
if (cast.ptr == NULL) {
64+
printf("Error: %s\n", dlerror());
65+
dlclose(handle);
66+
return 1;
67+
}
68+
69+
*fp = cast.fp;
70+
#endif
71+
72+
return 0;
73+
}
74+
75+
static inline void dyn_close(dyn_handle_t handle) {
76+
#if defined(WIN32) || defined(_WIN32)
77+
FreeLibrary(handle);
78+
#else
79+
dlclose(handle);
80+
#endif
81+
}
82+
83+
static inline void dyn_symbol_info(void* symbol) {
84+
#if !defined(WIN32) && !defined(_WIN32) && !defined(__APPLE__)
85+
Dl_info info;
86+
if (dladdr(symbol, &info)) {
87+
printf("Symbol: %s\n", info.dli_sname);
88+
printf("Address: %p\n", info.dli_saddr);
89+
printf("Library: %s\n", (info.dli_fname ? info.dli_fname : "unknown"));
90+
} else {
91+
printf("Error: Failed to get symbol info.\n");
92+
}
93+
#endif
94+
}
95+
96+
static inline void dyn_handle_info(dyn_handle_t handle) {
97+
#if !defined(WIN32) && !defined(_WIN32) && !defined(__APPLE__)
98+
const struct link_map *link_map = 0;
99+
const int ret = dlinfo(handle, RTLD_DI_LINKMAP, &link_map);
100+
assert(link_map != 0);
101+
printf("Libraries:\n");
102+
while (link_map->l_prev) {
103+
printf("%s\n", link_map->l_name);
104+
link_map = link_map->l_prev;
105+
}
106+
#endif
107+
}
108+
109+
110+
static inline dyn_handle_t dyn_current_process_handle(void) {
111+
#if defined(WIN32) || defined(_WIN32)
112+
return GetModuleHandle(NULL);
113+
#else
114+
return dlopen(NULL, RTLD_LAZY);
115+
#endif
116+
}
117+
118+
#endif

source/libnode.c

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#include "libnode.h"
2+
3+
char *string_function(void) {
4+
#ifdef NODE_STATIC
5+
return "node-static";
6+
#else
7+
return "node-dynamic";
8+
#endif
9+
}

source/libnode.h

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#ifndef LIBNODE
2+
#define LIBNODE 1
3+
4+
#if defined(WIN32) || defined(_WIN32)
5+
#define EXPORT __declspec(dllexport)
6+
#else
7+
#define EXPORT __attribute__((visibility("default")))
8+
#endif
9+
10+
EXPORT char *string_function(void);
11+
12+
#endif

source/libnode2.c

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#include "libnode2.h"
2+
3+
char *string_function(void) {
4+
return "libnode2";
5+
}
6+
7+
char *string_function2(void) {
8+
return "libnode2";
9+
}

0 commit comments

Comments
 (0)