Skip to content

Commit 96ab3fd

Browse files
committed
Fix GH issue #104 guest-side wrfsbase 0
1 parent a7ce794 commit 96ab3fd

3 files changed

Lines changed: 128 additions & 4 deletions

File tree

‎lib/tinykvm/machine.hpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,11 @@ struct Machine
437437
mutable std::unique_ptr<FileDescriptors> m_fds = nullptr;
438438

439439
Machine* m_remote = nullptr;
440+
/* True only while a remote connection is live (between activate and
441+
disconnect). Must not be derived from guest-visible state: the guest
442+
can write FSBASE (CR4.FSGSBASE is enabled), so using its TLS base as
443+
the connection token lets a guest skip the disconnect cleanup. */
444+
bool m_remote_active = false;
440445
uint32_t m_remote_connections = 0;
441446

442447
std::unique_ptr<MachineProfiling> m_profiling = nullptr;

‎lib/tinykvm/remote.cpp‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,26 @@ void Machine::ipre_remote_resume_now(bool save_all, std::function<void(Machine&)
178178
remote_vm.cpu().get_special_registers().fs.base =
179179
this->get_special_registers().fs.base;
180180
// After disconnect, access is no longer serialized (don't touch remote anymore)
181-
const auto our_fsbase = this->remote_disconnect();
182-
if (our_fsbase == 0)
181+
// NOTE: The restored FSBASE is guest-writable and may be zero, so the
182+
// connection state itself decides whether the disconnect succeeded.
183+
if (!this->is_remote_connected())
183184
throw std::runtime_error("ipre_resume_storage: Remote disconnect failed");
185+
const auto our_fsbase = this->remote_disconnect();
184186

185187
// 6. When returning, restore original register state
186188
copy_callee_saved_registers(save_all, this->registers(), saved_gprs);
187189
this->registers().rax = this->registers().rsi; // RAX is return value
188190
this->registers().rip += 2; // Skip over OUT instruction
189191
if (save_all)
190192
this->set_fpu_registers(saved_fprs);
193+
if (our_fsbase == 0) {
194+
// The resume stub skips wrfsbase when the pushed value is zero, so
195+
// restore a zeroed guest FSBASE here instead of leaving the remote
196+
// VM's FSBASE loaded after the remote memory is unmapped again.
197+
auto& local_sprs = vcpu.get_special_registers();
198+
local_sprs.fs.base = 0;
199+
this->set_special_registers(local_sprs);
200+
}
191201
this->prepare_vmresume(our_fsbase, true);
192202
vcpu.stopped = false;
193203
}
@@ -312,8 +322,10 @@ Machine::address_t Machine::remote_activate_now()
312322

313323
this->remote_connect(*this->m_remote, true);
314324
this->m_remote_connections++;
325+
this->m_remote_active = true;
315326

316-
// Set current FSBASE to remote original FSBASE
327+
// Remember our own FSBASE, so that it can be restored on disconnect.
328+
// NOTE: This is guest-writable and may legitimately be zero.
317329
vcpu.remote_original_tls_base = get_fsgs().first;
318330

319331
auto& remote = *this->m_remote;
@@ -356,6 +368,7 @@ Machine::address_t Machine::remote_disconnect()
356368
{
357369
if (!this->is_remote_connected())
358370
return 0;
371+
this->m_remote_active = false;
359372

360373
auto& remote = *this->m_remote;
361374
remote.m_remote = nullptr; // Clear halfway state
@@ -397,7 +410,7 @@ Machine::address_t Machine::remote_disconnect()
397410
}
398411
bool Machine::is_remote_connected() const noexcept
399412
{
400-
return this->m_remote != nullptr && this->vcpu.remote_original_tls_base != 0;
413+
return this->m_remote != nullptr && this->m_remote_active;
401414
}
402415

403416
bool Machine::is_foreign_address(address_t addr) const noexcept

‎tests/unit/remote.cpp‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,112 @@ int main() {
7676
REQUIRE(machine.remote_connection_count() == 1);
7777
}
7878

79+
TEST_CASE("Remote call with guest-zeroed FSBASE must not wedge fork", "[Remote]")
80+
{
81+
const auto storage_binary = build_and_load(R"M(
82+
extern long write(int, const void*, unsigned long);
83+
int main() {
84+
return 1234;
85+
}
86+
extern void remote_hello_world() {
87+
write(1, "Hello Remote World!", 19);
88+
}
89+
)M", "-Wl,-Ttext-segment=0x40400000");
90+
91+
// Extract storage remote symbols
92+
const std::string command = "objcopy -w --extract-symbol --strip-symbol=!remote* --strip-symbol=* " + storage_binary.first + " storage.syms";
93+
FILE* f = popen(command.c_str(), "r");
94+
if (f == nullptr) {
95+
throw std::runtime_error("Unable to extract remote symbols");
96+
}
97+
pclose(f);
98+
99+
const auto main_binary = build_and_load(R"M(
100+
extern void remote_hello_world();
101+
int main() {
102+
return 2345;
103+
}
104+
extern long test_remote_call() {
105+
remote_hello_world();
106+
return 1;
107+
}
108+
extern long test_zero_fsbase() {
109+
unsigned long fsbase;
110+
__asm__ volatile("rdfsbase %0" : "=r"(fsbase));
111+
/* CR4.FSGSBASE is enabled for guests, so a guest can zero its own
112+
FSBASE right before a remote call. That must not affect the
113+
connection state kept by the host. */
114+
__asm__ volatile("wrfsbase %0" :: "r"(0UL));
115+
remote_hello_world();
116+
__asm__ volatile("wrfsbase %0" :: "r"(fsbase));
117+
return 1;
118+
}
119+
)M", "-Wl,--just-symbols=storage.syms");
120+
121+
tinykvm::Machine storage { storage_binary.second, {
122+
.max_mem = 16ULL << 20, // MB
123+
.vmem_base_address = 1ULL << 30, // 1GB
124+
} };
125+
storage.setup_linux({"storage"}, env);
126+
storage.run(4.0f);
127+
REQUIRE(storage.return_value() == 1234);
128+
129+
unsigned remote_writes = 0;
130+
storage.set_printer([&] (const char* data, size_t size) {
131+
if (std::string_view{data, size} == "Hello Remote World!")
132+
remote_writes ++;
133+
});
134+
135+
tinykvm::Machine machine { main_binary.second, {
136+
.max_mem = MAX_MEMORY
137+
} };
138+
machine.setup_linux({"main"}, env);
139+
machine.remote_connect(storage);
140+
machine.set_remote_allow_page_faults(true);
141+
machine.run(4.0f);
142+
REQUIRE(machine.return_value() == 2345);
143+
REQUIRE(!machine.is_remote_connected());
144+
145+
machine.prepare_copy_on_write(1UL << 20);
146+
const tinykvm::MachineOptions fork_options {
147+
.max_mem = MAX_MEMORY,
148+
.max_cow_mem = MAX_COWMEM,
149+
.split_hugepages = true
150+
};
151+
152+
// A guest that zeroes its FSBASE still performs a real remote call, and
153+
// the disconnect afterwards must still happen.
154+
tinykvm::Machine fork(machine, fork_options);
155+
fork.set_remote_allow_page_faults(true);
156+
157+
fork.vmcall("test_zero_fsbase");
158+
REQUIRE(fork.return_value() == 1);
159+
REQUIRE(remote_writes == 1);
160+
REQUIRE(!fork.is_remote_connected());
161+
REQUIRE(fork.remote_connection_count() == 1);
162+
163+
// The fork must be recyclable: an ordinary remote call after reset works.
164+
fork.reset_to(machine, fork_options);
165+
fork.vmcall("test_remote_call");
166+
REQUIRE(fork.return_value() == 1);
167+
REQUIRE(remote_writes == 2);
168+
REQUIRE(!fork.is_remote_connected());
169+
REQUIRE(fork.remote_connection_count() == 2);
170+
171+
// Control: an identical sequence without wrfsbase.
172+
tinykvm::Machine fork2(machine, fork_options);
173+
fork2.set_remote_allow_page_faults(true);
174+
175+
fork2.vmcall("test_remote_call");
176+
REQUIRE(fork2.return_value() == 1);
177+
fork2.reset_to(machine, fork_options);
178+
fork2.vmcall("test_remote_call");
179+
REQUIRE(fork2.return_value() == 1);
180+
REQUIRE(remote_writes == 4);
181+
REQUIRE(!fork2.is_remote_connected());
182+
REQUIRE(fork2.remote_connection_count() == 2);
183+
}
184+
79185
TEST_CASE("Fail accessing remote VM directly", "[Remote]")
80186
{
81187
const auto storage_binary = build_and_load(R"M(

0 commit comments

Comments
 (0)