Skip to content

Commit 79f3486

Browse files
Attempt to repro on linux
1 parent 3c57c66 commit 79f3486

1 file changed

Lines changed: 214 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* Copyright 2026 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/// @file test_room_process_invalidation.cpp
18+
/// @brief Linux integration repro for server-driven room invalidation at process exit.
19+
///
20+
/// Reproduces the user-reported shutdown path: a C++ client joins a room, the server
21+
/// invalidates the session (duplicate identity), and the client process exits without
22+
/// calling Room::disconnect() or livekit::shutdown(). On glibc/Linux, heap corruption
23+
/// during thread teardown can surface as:
24+
/// SIGABRT: tcache_thread_shutdown(): unaligned tcache chunk detected
25+
26+
#include <gtest/gtest.h>
27+
#include <livekit/livekit.h>
28+
29+
#include <cstdlib>
30+
#include <string>
31+
32+
#if defined(__linux__)
33+
34+
#include <sys/types.h>
35+
#include <sys/wait.h>
36+
#include <unistd.h>
37+
38+
#include <cerrno>
39+
#include <chrono>
40+
#include <condition_variable>
41+
#include <csignal>
42+
#include <cstring>
43+
#include <mutex>
44+
#include <thread>
45+
#endif
46+
47+
namespace livekit::test {
48+
49+
#if defined(__linux__)
50+
51+
namespace {
52+
53+
using namespace std::chrono_literals;
54+
55+
constexpr char kChildReadyByte = 1;
56+
constexpr auto kChildConnectTimeout = 30s;
57+
constexpr auto kInvalidationTimeout = 30s;
58+
constexpr auto kChildExitTimeout = 30s;
59+
60+
struct ProcessInvalidationConfig {
61+
std::string url;
62+
std::string token;
63+
bool available = false;
64+
65+
static ProcessInvalidationConfig fromEnv() {
66+
ProcessInvalidationConfig config;
67+
const char* url = std::getenv("LIVEKIT_URL");
68+
const char* token = std::getenv("LIVEKIT_TOKEN_A");
69+
if (url != nullptr && token != nullptr && url[0] != '\0' && token[0] != '\0') {
70+
config.url = url;
71+
config.token = token;
72+
config.available = true;
73+
}
74+
return config;
75+
}
76+
};
77+
78+
class DuplicateIdentityWaiter : public RoomDelegate {
79+
public:
80+
void onDisconnected(Room&, const DisconnectedEvent& event) override {
81+
{
82+
const std::scoped_lock<std::mutex> lock(mutex_);
83+
reason_ = event.reason;
84+
disconnected_ = true;
85+
}
86+
cv_.notify_all();
87+
}
88+
89+
bool waitForDuplicateIdentity(std::chrono::milliseconds timeout) {
90+
std::unique_lock<std::mutex> lock(mutex_);
91+
return cv_.wait_for(lock, timeout,
92+
[this]() { return disconnected_ && reason_ == DisconnectReason::DuplicateIdentity; });
93+
}
94+
95+
private:
96+
mutable std::mutex mutex_;
97+
std::condition_variable cv_;
98+
bool disconnected_ = false;
99+
DisconnectReason reason_ = DisconnectReason::Unknown;
100+
};
101+
102+
bool writeByte(int fd, char value) { return ::write(fd, &value, 1) == 1; }
103+
104+
bool readByte(int fd, char* value) { return ::read(fd, value, 1) == 1; }
105+
106+
void runVictimChild(int ready_pipe_write, const std::string& url, const std::string& token) {
107+
if (!livekit::initialize(livekit::LogLevel::Info)) {
108+
_exit(2);
109+
}
110+
111+
Room room;
112+
DuplicateIdentityWaiter delegate;
113+
room.setDelegate(&delegate);
114+
115+
RoomOptions options;
116+
if (!room.connect(url, token, options)) {
117+
_exit(3);
118+
}
119+
120+
if (!writeByte(ready_pipe_write, kChildReadyByte)) {
121+
_exit(4);
122+
}
123+
124+
if (!delegate.waitForDuplicateIdentity(kInvalidationTimeout)) {
125+
_exit(5);
126+
}
127+
128+
// Deliberately skip Room::disconnect() and livekit::shutdown() to mirror the
129+
// user report: process exit tears down Rust/WebRTC threads and glibc allocators.
130+
std::exit(0);
131+
}
132+
133+
} // namespace
134+
135+
class RoomInvalidationProcessExitTest : public ::testing::Test {};
136+
137+
TEST_F(RoomInvalidationProcessExitTest, DuplicateIdentityInvalidationThenClientProcessExit) {
138+
const ProcessInvalidationConfig config = ProcessInvalidationConfig::fromEnv();
139+
if (!config.available) {
140+
GTEST_SKIP() << "LIVEKIT_URL and LIVEKIT_TOKEN_A must be set";
141+
}
142+
143+
int ready_pipe[2] = {-1, -1};
144+
ASSERT_EQ(::pipe(ready_pipe), 0) << "pipe() failed: " << std::strerror(errno);
145+
146+
const pid_t child_pid = ::fork();
147+
ASSERT_NE(child_pid, -1) << "fork() failed: " << std::strerror(errno);
148+
149+
if (child_pid == 0) {
150+
::close(ready_pipe[0]);
151+
runVictimChild(ready_pipe[1], config.url, config.token);
152+
::close(ready_pipe[1]);
153+
_exit(1);
154+
}
155+
156+
::close(ready_pipe[1]);
157+
158+
char ready_byte = 0;
159+
ASSERT_TRUE(readByte(ready_pipe[0], &ready_byte)) << "timed out waiting for child connect signal";
160+
::close(ready_pipe[0]);
161+
ASSERT_EQ(ready_byte, kChildReadyByte);
162+
163+
ASSERT_TRUE(livekit::initialize(livekit::LogLevel::Info)) << "parent initialize failed";
164+
165+
Room usurper;
166+
RoomOptions options;
167+
ASSERT_TRUE(usurper.connect(config.url, config.token, options))
168+
<< "parent failed to connect with duplicate identity token";
169+
170+
int status = 0;
171+
const auto wait_deadline = std::chrono::steady_clock::now() + kChildExitTimeout;
172+
while (true) {
173+
const pid_t waited = ::waitpid(child_pid, &status, WNOHANG);
174+
if (waited == child_pid) {
175+
break;
176+
}
177+
if (waited == -1) {
178+
FAIL() << "waitpid() failed: " << std::strerror(errno);
179+
}
180+
if (std::chrono::steady_clock::now() >= wait_deadline) {
181+
::kill(child_pid, SIGKILL);
182+
(void)::waitpid(child_pid, &status, 0);
183+
FAIL() << "timed out waiting for victim child process to exit";
184+
}
185+
std::this_thread::sleep_for(50ms);
186+
}
187+
188+
usurper.disconnect();
189+
livekit::shutdown();
190+
191+
if (WIFSIGNALED(status)) {
192+
const int signal = WTERMSIG(status);
193+
FAIL() << "victim child terminated by signal " << signal
194+
<< " (expected clean exit 0; SIGABRT=6 often indicates glibc tcache corruption on shutdown)";
195+
}
196+
197+
if (!WIFEXITED(status)) {
198+
FAIL() << "victim child did not exit normally";
199+
}
200+
201+
EXPECT_EQ(WEXITSTATUS(status), 0) << "victim child should exit 0 after duplicate-identity invalidation";
202+
}
203+
204+
#else
205+
206+
class RoomInvalidationProcessExitTest : public ::testing::Test {};
207+
208+
TEST_F(RoomInvalidationProcessExitTest, DuplicateIdentityInvalidationThenClientProcessExit) {
209+
GTEST_SKIP() << "Linux-only repro: glibc tcache_thread_shutdown() abort is not observable on this platform";
210+
}
211+
212+
#endif
213+
214+
} // namespace livekit::test

0 commit comments

Comments
 (0)