ice/turn transport: make sure PJSIP is initialized before it's used
Using PJSIP functions without calling pj_init first can lead to subtle
non-deterministic bugs.
GitLab: #18
Change-Id: I9364fd247165c0ce19a8d0d42575fb66651b54a3
diff --git a/include/ice_transport_factory.h b/include/ice_transport_factory.h
index df3367c..3e4c931 100644
--- a/include/ice_transport_factory.h
+++ b/include/ice_transport_factory.h
@@ -19,6 +19,7 @@
#include "ice_options.h"
#include "ice_transport.h"
#include "ip_utils.h"
+#include "pj_init_lock.h"
#include <functional>
#include <memory>
@@ -50,6 +51,9 @@
std::shared_ptr<pj_caching_pool> getPoolCaching() { return cp_; }
private:
+ // Declaring pjInitLock_ before cp_ because its constructor needs to be called
+ // first (see constructor implementation for a comment with more information).
+ PjInitLock pjInitLock_;
std::shared_ptr<pj_caching_pool> cp_;
pj_ice_strans_cfg ice_cfg_;
std::shared_ptr<Logger> logger_ {};
diff --git a/include/pj_init_lock.h b/include/pj_init_lock.h
new file mode 100644
index 0000000..c521a8a
--- /dev/null
+++ b/include/pj_init_lock.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 Savoir-faire Linux Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <fmt/core.h>
+#include <mutex>
+#include <pj/errno.h>
+#include <pj/types.h>
+
+namespace dhtnet {
+
+// PJSIP expects the number of calls to pj_shutdown to match the number of calls
+// to pj_init (https://docs.pjsip.org/en/latest/specific-guides/develop/init_shutdown_thread.html).
+// The intended behavior seems to be the following:
+// - The first call to pj_init actually initializes the library; subsequent calls do nothing.
+// - All calls to pj_shutdown do nothing, except the last one which actually performs the shutdown.
+// Unfortunately, the way this logic is implemented in PJSIP is not thread-safe, so we're
+// responsible for making sure that these functions can't be called by two threads at the same time.
+class PjInitLock
+{
+private:
+ inline static std::mutex mutex_;
+
+public:
+ PjInitLock()
+ {
+ std::lock_guard lk(mutex_);
+ pj_status_t status = pj_init();
+
+ if (status != PJ_SUCCESS) {
+ char errorMessage[PJ_ERR_MSG_SIZE];
+ pj_strerror(status, errorMessage, sizeof(errorMessage));
+ throw std::runtime_error(
+ fmt::format("pj_init failed: {}", errorMessage));
+ }
+ }
+
+ ~PjInitLock()
+ {
+ std::lock_guard lk(mutex_);
+ pj_shutdown();
+ }
+};
+
+}