Skip to content

Commit f05d9da

Browse files
committed
feat(runtime): add intrinsic resolution for TLS native calls
Wire vmpilot_tls_read64/write64/read32/write32 into the NATIVE_CALL dispatch pipeline via a sentinel-based intrinsic resolution mechanism. The blob stores a reserved target_offset (INTRINSIC_BASE + id) for runtime-provided functions. VmEngine::create() resolves these to actual function pointers before any instruction executes, so the NATIVE_CALL handler works unchanged. Backend contract documented in vm_intrinsics.hpp: the backend must emit NATIVE_CALL + TransitionEntry with the sentinel target_offset when it encounters segment-prefixed memory accesses (fs:/gs:/TPIDR_EL0).
1 parent 4f9d182 commit f05d9da

5 files changed

Lines changed: 326 additions & 0 deletions

File tree

runtime/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ add_library(VMPilot_Runtime STATIC
5555
src/oram_strategies.cpp
5656
src/classify_args.cpp
5757
src/tls_helpers.cpp
58+
src/vm_intrinsics.cpp
5859
src/blob_builder.cpp
5960
src/program_builder.cpp
6061
src/handler_detail.cpp
@@ -138,6 +139,7 @@ if(ENABLE_TESTS)
138139
test_cfg_patterns
139140
test_native_call
140141
test_policy_matrix
142+
test_tls_intrinsic
141143
)
142144
vmpilot_add_runtime_test(${_t} SOURCES test/integration/${_t}.cpp)
143145
endforeach()

runtime/include/vm_intrinsics.hpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#pragma once
2+
#ifndef __RUNTIME_VM_INTRINSICS_HPP__
3+
#define __RUNTIME_VM_INTRINSICS_HPP__
4+
5+
/// @file vm_intrinsics.hpp
6+
/// @brief Intrinsic native call resolution for runtime-provided functions.
7+
///
8+
/// The NATIVE_CALL handler resolves targets via
9+
/// target_offset + load_base_delta
10+
/// which works for functions in the protected binary. Runtime-internal
11+
/// functions (e.g. TLS helpers) live in the runtime library and their
12+
/// addresses are unknown at blob-build time.
13+
///
14+
/// Convention: the blob stores a sentinel value in target_offset to
15+
/// indicate an intrinsic. The runtime resolves it to the actual
16+
/// function pointer at engine creation (VmEngine::create), before any
17+
/// instruction executes.
18+
///
19+
/// ─── Backend Contract ────────────────────────────────────────────────
20+
///
21+
/// When the backend (SimpleBackend / LLVM backend) encounters a
22+
/// segment-prefixed memory access in the protected region:
23+
///
24+
/// x86-64: fs:[offset] → TLS_READ64 / TLS_WRITE64
25+
/// x86-32: gs:[offset] → TLS_READ32 / TLS_WRITE32
26+
/// ARM64: mrs TPIDR_EL0 + ldr/str → TLS_READ64 / TLS_WRITE64
27+
///
28+
/// It must emit:
29+
///
30+
/// 1. Load the TLS offset into r0.
31+
/// For writes: offset in r0, value in r1.
32+
///
33+
/// 2. A NATIVE_CALL instruction with insn.aux = transition entry index.
34+
///
35+
/// 3. A TransitionEntry in the blob with:
36+
/// target_offset = intrinsic_target(IntrinsicId::TLS_READ64)
37+
/// = INTRINSIC_BASE + id
38+
/// arg_count = te_pack_arg_count(1, 0, false, false, false)
39+
/// (2 for write variants)
40+
/// call_site_ip = instruction index of the NATIVE_CALL
41+
///
42+
/// The runtime resolves the sentinel to the actual function pointer at
43+
/// engine creation time. The result (for reads) is returned in r0.
44+
/// ─────────────────────────────────────────────────────────────────────
45+
46+
#include <cstdint>
47+
48+
namespace VMPilot::Runtime {
49+
50+
/// Sentinel base for intrinsic target_offset values.
51+
/// Top of the 64-bit address space — never a real function pointer.
52+
inline constexpr uint64_t INTRINSIC_BASE = 0xFFFF'FFFF'FFFF'FF00ULL;
53+
54+
/// Well-known intrinsic IDs.
55+
enum class IntrinsicId : uint8_t {
56+
TLS_READ64 = 0, ///< vmpilot_tls_read64(offset) -> uint64_t
57+
TLS_WRITE64 = 1, ///< vmpilot_tls_write64(offset, value) -> void
58+
TLS_READ32 = 2, ///< vmpilot_tls_read32(offset) -> uint64_t
59+
TLS_WRITE32 = 3, ///< vmpilot_tls_write32(offset, value) -> void
60+
61+
COUNT ///< must be last
62+
};
63+
64+
/// Produce the sentinel target_offset for blob builders.
65+
constexpr uint64_t intrinsic_target(IntrinsicId id) noexcept {
66+
return INTRINSIC_BASE + static_cast<uint8_t>(id);
67+
}
68+
69+
/// Check whether a target_offset is an intrinsic sentinel.
70+
constexpr bool is_intrinsic_target(uint64_t target_offset) noexcept {
71+
return target_offset >= INTRINSIC_BASE
72+
&& target_offset < INTRINSIC_BASE + static_cast<uint8_t>(IntrinsicId::COUNT);
73+
}
74+
75+
/// Resolve an intrinsic ID to the actual function pointer.
76+
/// Returns nullptr for unknown IDs.
77+
void* resolve_intrinsic(IntrinsicId id) noexcept;
78+
79+
} // namespace VMPilot::Runtime
80+
81+
#endif // __RUNTIME_VM_INTRINSICS_HPP__

runtime/src/vm_engine.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/// ensure link-time availability without exposing implementation in the header.
88

99
#include "vm_engine.hpp"
10+
#include "vm_intrinsics.hpp"
1011

1112
#include <vm/vm_encoding.hpp>
1213
#include <vm/hardware_rng.hpp>
@@ -122,6 +123,24 @@ VmEngine<Policy, Oram>::create(
122123
auto raw_trans = m->blob.native_calls();
123124
m->native_calls.assign(raw_trans.begin(), raw_trans.end());
124125

126+
// 5b. Resolve intrinsic native calls (runtime-provided functions).
127+
//
128+
// The blob stores a sentinel target_offset (INTRINSIC_BASE + id) for
129+
// built-in intrinsics whose addresses are unknown at blob-build time.
130+
// Patch them here so the handler's (target_offset + load_base_delta)
131+
// yields the correct function pointer.
132+
for (auto& te : m->native_calls) {
133+
if (is_intrinsic_target(te.target_offset)) {
134+
auto id = static_cast<IntrinsicId>(
135+
te.target_offset - INTRINSIC_BASE);
136+
auto* fn = resolve_intrinsic(id);
137+
if (!fn)
138+
return tl::make_unexpected(DiagnosticCode::NativeCallBridgeFailed);
139+
te.target_offset = reinterpret_cast<uint64_t>(fn)
140+
- static_cast<uint64_t>(load_base_delta);
141+
}
142+
}
143+
125144
// 6. Derive global memory encoding (LUT-based, unchanged from doc 15)
126145
derive_memory_tables(stored_seed, m->mem.encode, m->mem.decode);
127146

runtime/src/vm_intrinsics.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// @file vm_intrinsics.cpp
2+
/// @brief Intrinsic ID to function pointer resolution.
3+
4+
#include "vm_intrinsics.hpp"
5+
#include <tls_helpers.hpp>
6+
7+
namespace VMPilot::Runtime {
8+
9+
void* resolve_intrinsic(IntrinsicId id) noexcept {
10+
switch (id) {
11+
case IntrinsicId::TLS_READ64:
12+
return reinterpret_cast<void*>(&vmpilot_tls_read64);
13+
case IntrinsicId::TLS_WRITE64:
14+
return reinterpret_cast<void*>(&vmpilot_tls_write64);
15+
case IntrinsicId::TLS_READ32:
16+
return reinterpret_cast<void*>(&vmpilot_tls_read32);
17+
case IntrinsicId::TLS_WRITE32:
18+
return reinterpret_cast<void*>(&vmpilot_tls_write32);
19+
default:
20+
return nullptr;
21+
}
22+
}
23+
24+
} // namespace VMPilot::Runtime
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/// @file test_tls_intrinsic.cpp
2+
/// @brief Integration test: TLS intrinsic resolution via NATIVE_CALL.
3+
///
4+
/// Verifies that a blob containing an intrinsic sentinel target_offset
5+
/// (e.g. TLS_READ64) is correctly resolved at engine creation, and the
6+
/// NATIVE_CALL handler dispatches to vmpilot_tls_read64.
7+
8+
#include "test_blob_builder.hpp"
9+
10+
#include "vm_engine.hpp"
11+
#include "vm_intrinsics.hpp"
12+
13+
#include <tls_helpers.hpp>
14+
#include <vm/vm_opcode.hpp>
15+
#include <vm/vm_blob.hpp>
16+
17+
#include <gtest/gtest.h>
18+
19+
#include <cstdint>
20+
21+
using namespace VMPilot::Runtime;
22+
using namespace VMPilot::Common::VM;
23+
using namespace VMPilot::Test;
24+
25+
static uint8_t flags_none() { return 0; }
26+
27+
// ============================================================================
28+
// Test: TLS_READ64 intrinsic through NATIVE_CALL
29+
// ============================================================================
30+
31+
TEST(TlsIntrinsic, Read64ViaIntrinsic) {
32+
uint8_t seed[32];
33+
fill_seed(seed);
34+
35+
// Build a single-BB program: NATIVE_CALL(aux=0) → HALT
36+
TestBB bb{};
37+
bb.bb_id = 1;
38+
bb.epoch = 0;
39+
bb.live_regs_bitmap = 0xFFFF;
40+
bb.flags = 0;
41+
fill_epoch(bb.epoch_seed, 0xA0);
42+
43+
bb.instructions = {
44+
{VmOpcode::NATIVE_CALL, flags_none(), 0, 0, 0}, // aux=0 → first transition entry
45+
{VmOpcode::HALT, flags_none(), 0, 0, 0},
46+
};
47+
48+
// Transition entry: intrinsic sentinel for TLS_READ64, 1 arg
49+
TestNativeCall tc{};
50+
tc.call_site_ip = 0;
51+
tc.arg_count = te_pack_arg_count(1, 0, false, false, false);
52+
tc.target_addr = intrinsic_target(IntrinsicId::TLS_READ64);
53+
54+
auto blob = build_test_blob(seed, {bb}, {}, false, {tc});
55+
56+
// r0 = TLS offset. On most platforms, offset 0 of the thread pointer
57+
// contains a self-pointer or well-known value. We read it directly
58+
// for the expected result.
59+
uint64_t tls_offset = 0;
60+
uint64_t expected = vmpilot_tls_read64(tls_offset);
61+
62+
uint64_t initial_regs[16] = {};
63+
initial_regs[0] = tls_offset;
64+
65+
auto engine = VmEngine<DebugPolicy, DirectOram>::create(
66+
blob.data(), blob.size(), seed, 0, initial_regs, 1);
67+
ASSERT_TRUE(engine.has_value()) << "Engine creation should succeed";
68+
69+
auto r = engine->execute();
70+
ASSERT_TRUE(r.has_value()) << "Execution should succeed";
71+
EXPECT_EQ(r->return_value, expected)
72+
<< "TLS_READ64 intrinsic should return the same value as vmpilot_tls_read64(0)";
73+
}
74+
75+
// ============================================================================
76+
// Test: TLS_READ32 intrinsic through NATIVE_CALL
77+
// ============================================================================
78+
79+
TEST(TlsIntrinsic, Read32ViaIntrinsic) {
80+
uint8_t seed[32];
81+
fill_seed(seed);
82+
83+
TestBB bb{};
84+
bb.bb_id = 1;
85+
bb.epoch = 0;
86+
bb.live_regs_bitmap = 0xFFFF;
87+
bb.flags = 0;
88+
fill_epoch(bb.epoch_seed, 0xA1);
89+
90+
bb.instructions = {
91+
{VmOpcode::NATIVE_CALL, flags_none(), 0, 0, 0},
92+
{VmOpcode::HALT, flags_none(), 0, 0, 0},
93+
};
94+
95+
TestNativeCall tc{};
96+
tc.call_site_ip = 0;
97+
tc.arg_count = te_pack_arg_count(1, 0, false, false, false);
98+
tc.target_addr = intrinsic_target(IntrinsicId::TLS_READ32);
99+
100+
auto blob = build_test_blob(seed, {bb}, {}, false, {tc});
101+
102+
uint64_t tls_offset = 0;
103+
uint64_t expected = vmpilot_tls_read32(tls_offset);
104+
105+
uint64_t initial_regs[16] = {};
106+
initial_regs[0] = tls_offset;
107+
108+
auto engine = VmEngine<DebugPolicy, DirectOram>::create(
109+
blob.data(), blob.size(), seed, 0, initial_regs, 1);
110+
ASSERT_TRUE(engine.has_value());
111+
112+
auto r = engine->execute();
113+
ASSERT_TRUE(r.has_value());
114+
EXPECT_EQ(r->return_value, expected)
115+
<< "TLS_READ32 intrinsic should return the same value as vmpilot_tls_read32(0)";
116+
}
117+
118+
// ============================================================================
119+
// Test: intrinsic resolution with non-zero load_base_delta
120+
// ============================================================================
121+
122+
TEST(TlsIntrinsic, ResolutionWithNonZeroDelta) {
123+
uint8_t seed[32];
124+
fill_seed(seed);
125+
126+
TestBB bb{};
127+
bb.bb_id = 1;
128+
bb.epoch = 0;
129+
bb.live_regs_bitmap = 0xFFFF;
130+
bb.flags = 0;
131+
fill_epoch(bb.epoch_seed, 0xA2);
132+
133+
bb.instructions = {
134+
{VmOpcode::NATIVE_CALL, flags_none(), 0, 0, 0},
135+
{VmOpcode::HALT, flags_none(), 0, 0, 0},
136+
};
137+
138+
TestNativeCall tc{};
139+
tc.call_site_ip = 0;
140+
tc.arg_count = te_pack_arg_count(1, 0, false, false, false);
141+
tc.target_addr = intrinsic_target(IntrinsicId::TLS_READ64);
142+
143+
auto blob = build_test_blob(seed, {bb}, {}, false, {tc});
144+
145+
uint64_t tls_offset = 0;
146+
uint64_t expected = vmpilot_tls_read64(tls_offset);
147+
148+
uint64_t initial_regs[16] = {};
149+
initial_regs[0] = tls_offset;
150+
151+
// Non-zero delta: intrinsic resolution must compensate correctly
152+
int64_t delta = 0x1000;
153+
auto engine = VmEngine<DebugPolicy, DirectOram>::create(
154+
blob.data(), blob.size(), seed, delta, initial_regs, 1);
155+
ASSERT_TRUE(engine.has_value())
156+
<< "Engine creation should succeed with non-zero load_base_delta";
157+
158+
auto r = engine->execute();
159+
ASSERT_TRUE(r.has_value());
160+
EXPECT_EQ(r->return_value, expected)
161+
<< "Intrinsic should produce correct result regardless of load_base_delta";
162+
}
163+
164+
// ============================================================================
165+
// Test: sentinel outside intrinsic range is NOT resolved as intrinsic
166+
// ============================================================================
167+
168+
TEST(TlsIntrinsic, OutOfRangeSentinelNotResolvedAsIntrinsic) {
169+
// INTRINSIC_BASE + COUNT falls outside is_intrinsic_target() range,
170+
// so it's treated as a regular (non-intrinsic) native call target.
171+
// Creation succeeds, but execution fails because the address is garbage.
172+
uint8_t seed[32];
173+
fill_seed(seed);
174+
175+
TestBB bb{};
176+
bb.bb_id = 1;
177+
bb.epoch = 0;
178+
bb.live_regs_bitmap = 0xFFFF;
179+
bb.flags = 0;
180+
fill_epoch(bb.epoch_seed, 0xA3);
181+
182+
bb.instructions = {
183+
{VmOpcode::NATIVE_CALL, flags_none(), 0, 0, 0},
184+
{VmOpcode::HALT, flags_none(), 0, 0, 0},
185+
};
186+
187+
TestNativeCall tc{};
188+
tc.call_site_ip = 0;
189+
tc.arg_count = te_pack_arg_count(1, 0, false, false, false);
190+
tc.target_addr = INTRINSIC_BASE + static_cast<uint8_t>(IntrinsicId::COUNT);
191+
192+
auto blob = build_test_blob(seed, {bb}, {}, false, {tc});
193+
194+
// Creation succeeds — the out-of-range sentinel is not recognized
195+
auto engine = VmEngine<DebugPolicy, DirectOram>::create(
196+
blob.data(), blob.size(), seed);
197+
EXPECT_TRUE(engine.has_value())
198+
<< "Out-of-range sentinel should not be treated as intrinsic; "
199+
"creation succeeds (it looks like a regular native call target)";
200+
}

0 commit comments

Comments
 (0)