diff --git a/.github/workflows/pr-pull.yaml b/.github/workflows/pr-pull.yaml index b2ffa34c3..d46f93704 100644 --- a/.github/workflows/pr-pull.yaml +++ b/.github/workflows/pr-pull.yaml @@ -5,7 +5,7 @@ on: pull_request: paths-ignore: - '**.md' - branches: [master] + branches: [master, streamlabs] types: [ opened, synchronize, reopened ] permissions: contents: read diff --git a/browser-app.cpp b/browser-app.cpp index 650900861..6169ece16 100644 --- a/browser-app.cpp +++ b/browser-app.cpp @@ -80,16 +80,14 @@ void BrowserApp::OnBeforeChildProcessLaunch(CefRefPtr command_li std::lock_guard guard(flag_mutex); if (this->media_flag != -1) { if (this->media_flag) { - command_line->AppendSwitchWithValue( - "enable-media-stream", "1"); + command_line->AppendSwitchWithValue("enable-media-stream", "1"); } this->media_flag = -1; } else if (this->media_flags.size()) { bool flag = media_flags.front(); media_flags.pop(); if (flag) { - command_line->AppendSwitchWithValue( - "enable-media-stream", "1"); + command_line->AppendSwitchWithValue("enable-media-stream", "1"); } } } @@ -131,16 +129,14 @@ void BrowserApp::OnBeforeCommandLineProcessing(const CefString &, CefRefPtr guard(flag_mutex); if (this->media_flag != -1) { if (this->media_flag) { - command_line->AppendSwitchWithValue( - "enable-media-stream", "1"); + command_line->AppendSwitchWithValue("enable-media-stream", "1"); } this->media_flag = -1; } else if (this->media_flags.size()) { bool flag = media_flags.front(); media_flags.pop(); if (flag) { - command_line->AppendSwitchWithValue( - "enable-media-stream", "1"); + command_line->AppendSwitchWithValue("enable-media-stream", "1"); } } command_line->AppendSwitchWithValue("remote-debugging-port", "9222"); @@ -152,13 +148,13 @@ void BrowserApp::OnBeforeCommandLineProcessing(const CefString &, CefRefPtr exposedFunctions = {"getControlLevel", "getCurrentScene", "getStatus", - "startRecording", "stopRecording", "startStreaming", - "stopStreaming", "pauseRecording", "unpauseRecording", - "startReplayBuffer", "stopReplayBuffer", "saveReplayBuffer", - "startVirtualcam", "stopVirtualcam", "getScenes", - "setCurrentScene", "getTransitions", "getCurrentTransition", - "setCurrentTransition","messageFromApp", "messageToApp"}; +std::vector exposedFunctions = {"getControlLevel", "getCurrentScene", "getStatus", + "startRecording", "stopRecording", "startStreaming", + "stopStreaming", "pauseRecording", "unpauseRecording", + "startReplayBuffer", "stopReplayBuffer", "saveReplayBuffer", + "startVirtualcam", "stopVirtualcam", "getScenes", + "setCurrentScene", "getTransitions", "getCurrentTransition", + "setCurrentTransition", "messageFromApp", "messageToApp"}; void BrowserApp::OnContextCreated(CefRefPtr browser, CefRefPtr, CefRefPtr context) { diff --git a/browser-app.hpp b/browser-app.hpp index 51f81348d..cfe5fcbac 100644 --- a/browser-app.hpp +++ b/browser-app.hpp @@ -75,7 +75,9 @@ class BrowserApp : public CefApp, public CefRenderProcessHandler, public CefBrow public: #if defined(__APPLE__) || defined(_WIN32) - inline BrowserApp(bool shared_texture_available_ = false) : shared_texture_available(shared_texture_available_), media_flag(-1) + inline BrowserApp(bool shared_texture_available_ = false) + : shared_texture_available(shared_texture_available_), + media_flag(-1) #else inline BrowserApp(bool shared_texture_available_ = false, bool wayland_ = false) : shared_texture_available(shared_texture_available_), diff --git a/browser-client.cpp b/browser-client.cpp index e617498ed..7360f5371 100644 --- a/browser-client.cpp +++ b/browser-client.cpp @@ -290,7 +290,8 @@ bool BrowserClient::OnProcessMessageReceived(CefRefPtr browser, CefR json = (int)webpage_control_level; } else if (name == "messageToApp") { const std::string message = input_args->GetString(1).ToString(); - blog(LOG_INFO, "[BrowserMessage] messageToApp called: arguments %zu, arg2 %s", input_args->GetSize(), message.c_str()); + blog(LOG_INFO, "[BrowserMessage] messageToApp called: arguments %zu, arg2 %s", + input_args->GetSize(), message.c_str()); std::lock_guard lock(bs->messagesToAppMutex); bs->messagesToApp.push_back(message); } diff --git a/cmake/os-windows.cmake b/cmake/os-windows.cmake index f568dcba9..c759292f6 100644 --- a/cmake/os-windows.cmake +++ b/cmake/os-windows.cmake @@ -1,5 +1,9 @@ -target_compile_options(obs-browser PRIVATE $,/MTd,/MT>) -target_compile_definitions(obs-browser PRIVATE ENABLE_BROWSER_SHARED_TEXTURE) +set_property(TARGET obs-browser PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") +target_compile_definitions( + obs-browser + PRIVATE ENABLE_BROWSER_SHARED_TEXTURE $<$:NDEBUG> + $<$:_HAS_ITERATOR_DEBUGGING=0>) +target_compile_options(obs-browser PRIVATE $<$:/U_DEBUG>) target_link_libraries(obs-browser PRIVATE CEF::Wrapper CEF::Library d3d11 dxgi) target_link_options(obs-browser PRIVATE /IGNORE:4099) @@ -10,14 +14,24 @@ add_executable(OBS::browser-helper ALIAS obs-browser-helper) target_sources( obs-browser-helper PRIVATE # cmake-format: sortable - browser-app.cpp browser-app.hpp cef-headers.hpp obs-browser-page.manifest + browser-app.cpp browser-app.hpp cef-headers.hpp obs-browser-page.manifest obs-browser-sandbox.h + obs-browser-subprocess-win.cpp obs-browser-subprocess-win.hpp obs-browser-page/obs-browser-page-main.cpp) +target_sources(obs-browser PRIVATE obs-browser-sandbox.h obs-browser-subprocess-win.cpp + obs-browser-subprocess-win.hpp obs-browser-sandbox-selection.cpp + obs-browser-sandbox-selection.hpp) + target_include_directories(obs-browser-helper PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/deps" "${CMAKE_CURRENT_SOURCE_DIR}/obs-browser-page") -target_compile_options(obs-browser-helper PRIVATE $,/MTd,/MT>) -target_compile_definitions(obs-browser-helper PRIVATE ENABLE_BROWSER_SHARED_TEXTURE) +target_compile_features(obs-browser-helper PRIVATE cxx_std_17) +set_property(TARGET obs-browser-helper PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") +target_compile_definitions( + obs-browser-helper + PRIVATE ENABLE_BROWSER_SHARED_TEXTURE $<$:NDEBUG> + $<$:_HAS_ITERATOR_DEBUGGING=0>) +target_compile_options(obs-browser-helper PRIVATE $<$:/U_DEBUG>) target_link_libraries(obs-browser-helper PRIVATE CEF::Wrapper CEF::Library nlohmann_json::nlohmann_json) target_link_options(obs-browser-helper PRIVATE /IGNORE:4099 /SUBSYSTEM:WINDOWS) @@ -34,3 +48,21 @@ set_target_properties_obs( #make it dependence for obs-browser add_dependencies(obs-browser obs-browser-helper) + +if(BUILD_TESTING) + include(CTest) + + add_executable(obs-browser-sandbox-selection-test) + target_sources( + obs-browser-sandbox-selection-test + PRIVATE obs-browser-sandbox-selection.cpp obs-browser-sandbox-selection.hpp + tests/obs-browser-sandbox-selection-test.cpp) + target_include_directories(obs-browser-sandbox-selection-test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") + target_compile_features(obs-browser-sandbox-selection-test PRIVATE cxx_std_17) + set_property(TARGET obs-browser-sandbox-selection-test PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") + target_compile_definitions( + obs-browser-sandbox-selection-test + PRIVATE $<$:NDEBUG> $<$:_HAS_ITERATOR_DEBUGGING=0>) + target_compile_options(obs-browser-sandbox-selection-test PRIVATE $<$:/U_DEBUG>) + add_test(NAME obs-browser-sandbox-selection-test COMMAND obs-browser-sandbox-selection-test) +endif() diff --git a/obs-browser-page/obs-browser-page-main.cpp b/obs-browser-page/obs-browser-page-main.cpp index d679bb0c4..801b79b30 100644 --- a/obs-browser-page/obs-browser-page-main.cpp +++ b/obs-browser-page/obs-browser-page-main.cpp @@ -18,11 +18,10 @@ #include "cef-headers.hpp" #include "browser-app.hpp" +#include "obs-browser-subprocess-win.hpp" #ifdef _WIN32 #include -#include -#include // GPU hint exports for AMD/NVIDIA laptops #ifdef _MSC_VER @@ -30,9 +29,6 @@ extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; #endif -static HANDLE shutdown_event = nullptr; -static bool thread_initialized = false; - DECLARE_HANDLE(OBS_DPI_AWARENESS_CONTEXT); #define OBS_DPI_AWARENESS_CONTEXT_UNAWARE ((OBS_DPI_AWARENESS_CONTEXT)-1) #define OBS_DPI_AWARENESS_CONTEXT_SYSTEM_AWARE ((OBS_DPI_AWARENESS_CONTEXT)-2) @@ -51,55 +47,15 @@ static bool SetHighDPIv2Scaling() return !!func(OBS_DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); } -static void shutdown_check_thread(DWORD parent_pid, DWORD main_thread_id) -{ - HANDLE parent = OpenProcess(SYNCHRONIZE, false, parent_pid); - if (!parent) { - return; - } - - HANDLE handles[2] = {parent, shutdown_event}; - - DWORD ret = WaitForMultipleObjects(2, handles, false, INFINITE); - if (ret == WAIT_OBJECT_0) { - PostThreadMessage(main_thread_id, WM_QUIT, 0, 0); - ret = WaitForSingleObject(shutdown_event, 5000); - if (ret != WAIT_OBJECT_0) { - TerminateProcess(GetCurrentProcess(), (UINT)-1); - } - } - - CloseHandle(parent); -} - int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { - PROCESS_POWER_THROTTLING_STATE PowerThrottling; - PowerThrottling.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; - PowerThrottling.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION; - PowerThrottling.StateMask = 0; - - SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &PowerThrottling, sizeof(PowerThrottling)); - - std::thread shutdown_check; - - CefMainArgs mainArgs(nullptr); #if CHROME_VERSION_BUILD < 5615 if (!SetHighDPIv2Scaling()) CefEnableHighDPISupport(); #endif - CefRefPtr command_line = CefCommandLine::CreateCommandLine(); - command_line->InitFromString(::GetCommandLineW()); - - std::string parent_pid_str = command_line->GetSwitchValue("parent_pid"); - std::string current_pid = std::to_string(GetCurrentProcessId()); - if (!parent_pid_str.empty()) { - shutdown_event = CreateEvent(nullptr, true, false, nullptr); - DWORD parent_pid = (DWORD)std::stoi(parent_pid_str); - shutdown_check = std::thread(shutdown_check_thread, parent_pid, GetCurrentThreadId()); - thread_initialized = true; - } + return ExecuteBrowserSubprocess(nullptr); +} #else #if defined(NO_STACK_PROTECTOR) @@ -113,22 +69,9 @@ int main(int argc, char *argv[]) return 1; #endif CefMainArgs mainArgs(argc, argv); -#endif CefRefPtr mainApp(new BrowserApp()); int ret = CefExecuteProcess(mainArgs, mainApp.get(), NULL); - -#ifdef _WIN32 - /* chromium browser subprocesses actually have TerminateProcess called - * on them for whatever reason, so it's unlikely this code will ever - * get called, but better to be safe than sorry */ - if (thread_initialized) { - SetEvent(shutdown_event); - shutdown_check.join(); - } - if (shutdown_event) { - CloseHandle(shutdown_event); - } -#endif return ret; } +#endif diff --git a/obs-browser-plugin.cpp b/obs-browser-plugin.cpp index 3c788fd10..7c31aac3b 100644 --- a/obs-browser-plugin.cpp +++ b/obs-browser-plugin.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,13 @@ #include #include #include +#include + +#define OBS_BROWSER_SANDBOX_PLUGIN_IMPLEMENTATION +#include "obs-browser-sandbox.h" +#undef OBS_BROWSER_SANDBOX_PLUGIN_IMPLEMENTATION +#include "obs-browser-sandbox-selection.hpp" +#include "obs-browser-subprocess-win.hpp" #else #include "signal-restore.hpp" #endif @@ -79,6 +87,8 @@ using namespace std; static thread manager_thread; static bool manager_initialized = false; +static std::atomic_bool cef_initialized{false}; +static std::atomic_bool browser_shutdown_requested{false}; os_event_t *cef_started_event = nullptr; #if defined(_WIN32) @@ -107,8 +117,7 @@ class BrowserTask : public CefTask { * call otherwise the CEF message pump may stop functioning * correctly, it's only supposed to take 10ms max */ #ifdef WIN32 - QMetaObject::invokeMethod(&messageObject, "ExecuteTask", - Qt::QueuedConnection, + QMetaObject::invokeMethod(&messageObject, "ExecuteTask", Qt::QueuedConnection, Q_ARG(MessageTask, task)); #elif __APPLE__ ExecuteTask(task); @@ -170,8 +179,7 @@ static bool is_local_file_modified(obs_properties_t *props, obs_property_t *, ob return true; } -static bool is_mediaflag_modified(obs_properties_t *props, obs_property_t *, - obs_data_t *settings) +static bool is_mediaflag_modified(obs_properties_t *props, obs_property_t *, obs_data_t *settings) { UNUSED_PARAMETER(props); bool enabled = obs_data_get_bool(settings, "is_media_flag"); @@ -179,8 +187,7 @@ static bool is_mediaflag_modified(obs_properties_t *props, obs_property_t *, return true; } -static bool is_fps_custom(obs_properties_t *props, obs_property_t *, - obs_data_t *settings) +static bool is_fps_custom(obs_properties_t *props, obs_property_t *, obs_data_t *settings) { bool enabled = obs_data_get_bool(settings, "fps_custom"); obs_property_t *fps = obs_properties_get(props, "fps"); @@ -196,10 +203,8 @@ static obs_properties_t *browser_source_get_properties(void *data) DStr path; obs_properties_set_flags(props, OBS_PROPERTIES_DEFER_UPDATE); - obs_property_t *prop = obs_properties_add_bool( - props, "is_local_file", obs_module_text("LocalFile")); - obs_property_t *is_media_flag_prop = - obs_properties_add_bool(props, "is_media_flag", "IsMediaFlag"); + obs_property_t *prop = obs_properties_add_bool(props, "is_local_file", obs_module_text("LocalFile")); + obs_property_t *is_media_flag_prop = obs_properties_add_bool(props, "is_media_flag", "IsMediaFlag"); obs_property_set_visible(is_media_flag_prop, false); if (bs && !bs->url.empty()) { @@ -213,19 +218,14 @@ static obs_properties_t *browser_source_get_properties(void *data) } obs_property_set_modified_callback(prop, is_local_file_modified); - obs_property_set_modified_callback(is_media_flag_prop, - is_mediaflag_modified); - obs_properties_add_path(props, "local_file", - obs_module_text("LocalFile"), OBS_PATH_FILE, - "*.*", path->array); - obs_properties_add_text(props, "url", obs_module_text("URL"), - OBS_TEXT_DEFAULT); + obs_property_set_modified_callback(is_media_flag_prop, is_mediaflag_modified); + obs_properties_add_path(props, "local_file", obs_module_text("LocalFile"), OBS_PATH_FILE, "*.*", path->array); + obs_properties_add_text(props, "url", obs_module_text("URL"), OBS_TEXT_DEFAULT); obs_properties_add_int(props, "width", obs_module_text("Width"), 1, 8192, 1); obs_properties_add_int(props, "height", obs_module_text("Height"), 1, 8192, 1); - obs_properties_add_bool(props, "reroute_audio", - obs_module_text("RerouteAudioStreamlabs")); + obs_properties_add_bool(props, "reroute_audio", obs_module_text("RerouteAudioStreamlabs")); obs_property_t *fps_set = obs_properties_add_bool(props, "fps_custom", obs_module_text("CustomFrameRate")); obs_property_set_modified_callback(fps_set, is_fps_custom); @@ -241,10 +241,9 @@ static obs_properties_t *browser_source_get_properties(void *data) obs_properties_add_bool(props, "shutdown", obs_module_text("ShutdownSourceNotVisible")); obs_properties_add_bool(props, "restart_when_active", obs_module_text("RefreshBrowserActive")); - obs_property_t *controlLevel = obs_properties_add_list( - props, "webpage_control_level", - obs_module_text("WebpageControlLevel"), OBS_COMBO_TYPE_LIST, - OBS_COMBO_FORMAT_INT); + obs_property_t *controlLevel = obs_properties_add_list(props, "webpage_control_level", + obs_module_text("WebpageControlLevel"), + OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_visible(controlLevel, false); obs_property_list_add_int(controlLevel, obs_module_text("WebpageControlLevel.Level.None"), @@ -320,8 +319,7 @@ static obs_data_array_t *browser_source_get_messages(void *data) messages = obs_data_array_create(); for (const auto &message : bs->messagesToApp) { obs_data_t *msg_data = obs_data_create(); - obs_data_set_string(msg_data, "message", - message.c_str()); + obs_data_set_string(msg_data, "message", message.c_str()); obs_data_array_push_back(messages, msg_data); obs_data_release(msg_data); } @@ -334,18 +332,81 @@ static obs_data_array_t *browser_source_get_messages(void *data) static CefRefPtr app; -static void BrowserInit(obs_data_t *settings_obs) +static std::mutex &CefLifecycleMutex() { - UNUSED_PARAMETER(settings_obs); -#if defined(__APPLE__) && defined(USE_UI_LOOP) - ExecuteTask([settings_obs]() { + /* This module is loaded into CEF child processes to dispatch their entry + * point. Keep browser-process-only state lazy. */ + static std::mutex mutex; + return mutex; +} + +static void SignalBrowserInitializationComplete() +{ + if (cef_started_event) + os_event_signal(cef_started_event); +} + +bool obs_browser_initialized(void) +{ + return cef_initialized.load(); +} + +#ifdef _WIN32 +extern "C" __declspec(dllexport) int __cdecl obs_browser_execute_subprocess(void *sandbox_info) +{ + return ExecuteBrowserSubprocess(sandbox_info); +} + +static BrowserSandboxMode GetBrowserSandboxMode(BrowserSandboxExports &exports) +{ + HMODULE executable = GetModuleHandleW(nullptr); + exports.abi_version = reinterpret_cast( + GetProcAddress(executable, "obs_browser_sandbox_abi_version")); + exports.create = reinterpret_cast( + GetProcAddress(executable, "obs_browser_sandbox_info_create")); + exports.destroy = reinterpret_cast( + GetProcAddress(executable, "obs_browser_sandbox_info_destroy")); + + const BrowserSandboxMode mode = SelectBrowserSandboxMode(exports); + if (mode == BrowserSandboxMode::Legacy) { + blog(LOG_WARNING, "[obs-browser]: OBS does not expose the Windows sandbox ABI; " + "using the legacy unsandboxed browser helper."); + return mode; + } + + if (mode == BrowserSandboxMode::AbiVersionMismatch) { + blog(LOG_ERROR, "[obs-browser]: OBS Windows sandbox ABI version mismatch (expected %u).", + OBS_BROWSER_SANDBOX_ABI_VERSION); + return mode; + } + + if (mode == BrowserSandboxMode::MissingLifecycleExports) { + blog(LOG_ERROR, "[obs-browser]: OBS advertises Windows sandbox support without " + "all required lifecycle exports."); + return mode; + } + + return mode; +} #endif - string path = obs_get_module_binary_path(obs_current_module()); - path = path.substr(0, path.find_last_of('/') + 1); - path += "//obs-browser-page"; + +static bool BrowserInit(obs_data_t *settings_obs) +{ + /* Serializing initialization with shutdown keeps an unload from missing a + * successful CefInitialize call on UI-loop hosts. */ + std::lock_guard lifecycle_lock(CefLifecycleMutex()); + if (browser_shutdown_requested.load()) { + SignalBrowserInitializationComplete(); + return false; + } + + UNUSED_PARAMETER(settings_obs); + string path = obs_get_module_binary_path(obs_current_module()); + path = path.substr(0, path.find_last_of('/') + 1); + path += "//obs-browser-page"; #ifdef _WIN32 - path += ".exe"; - CefMainArgs args; + path += ".exe"; + CefMainArgs args(GetModuleHandleW(nullptr)); #else /* On non-windows platforms, ie macOS, we'll want to pass thru flags to * CEF */ @@ -353,97 +414,115 @@ static void BrowserInit(obs_data_t *settings_obs) CefMainArgs args(cmdline_args.argc, cmdline_args.argv); #endif - BPtr conf_path = obs_module_config_path(""); - os_mkdir(conf_path); + BPtr conf_path = obs_module_config_path(""); + os_mkdir(conf_path); + + CefSettings settings; + settings.log_severity = LOGSEVERITY_FATAL; + BPtr log_path = obs_module_config_path("debug.log"); + BPtr log_path_abs = os_get_abs_path_ptr(log_path); + CefString(&settings.log_file) = log_path_abs; + settings.windowless_rendering_enabled = true; + +#ifdef _WIN32 + BrowserSandboxExports sandbox_exports; + const BrowserSandboxMode sandbox_mode = GetBrowserSandboxMode(sandbox_exports); + if (sandbox_mode != BrowserSandboxMode::Legacy && sandbox_mode != BrowserSandboxMode::Enabled) { + SignalBrowserInitializationComplete(); + return false; + } - CefSettings settings; - settings.log_severity = LOGSEVERITY_FATAL; - BPtr log_path = obs_module_config_path("debug.log"); - BPtr log_path_abs = os_get_abs_path_ptr(log_path); - CefString(&settings.log_file) = log_path_abs; - settings.windowless_rendering_enabled = true; + const bool use_sandbox = sandbox_mode == BrowserSandboxMode::Enabled; + if (use_sandbox) { + settings.no_sandbox = false; + CefString(&settings.browser_subprocess_path) = ""; + } else { settings.no_sandbox = true; + } +#else + settings.no_sandbox = true; +#endif - uint32_t obs_ver = obs_get_version(); - uint32_t obs_maj = obs_ver >> 24; - uint32_t obs_min = (obs_ver >> 16) & 0xFF; - uint32_t obs_pat = obs_ver & 0xFFFF; + uint32_t obs_ver = obs_get_version(); + uint32_t obs_maj = obs_ver >> 24; + uint32_t obs_min = (obs_ver >> 16) & 0xFF; + uint32_t obs_pat = obs_ver & 0xFFFF; - /* This allows servers the ability to determine that browser panels and + /* This allows servers the ability to determine that browser panels and * browser sources are coming from OBS. */ - std::stringstream prod_ver; - prod_ver << "Chrome/"; - prod_ver << std::to_string(cef_version_info(4)) << "." - << std::to_string(cef_version_info(5)) << "." - << std::to_string(cef_version_info(6)) << "." - << std::to_string(cef_version_info(7)); - prod_ver << " OBS/"; - prod_ver << std::to_string(obs_maj) << "." - << std::to_string(obs_min) << "." - << std::to_string(obs_pat); - prod_ver << " SLD"; + std::stringstream prod_ver; + prod_ver << "Chrome/"; + prod_ver << std::to_string(cef_version_info(4)) << "." << std::to_string(cef_version_info(5)) << "." + << std::to_string(cef_version_info(6)) << "." << std::to_string(cef_version_info(7)); + prod_ver << " OBS/"; + prod_ver << std::to_string(obs_maj) << "." << std::to_string(obs_min) << "." << std::to_string(obs_pat); + prod_ver << " SLD"; CefString(&settings.user_agent_product) = prod_ver.str(); #ifdef USE_UI_LOOP - settings.external_message_pump = true; - settings.multi_threaded_message_loop = false; + settings.external_message_pump = true; + settings.multi_threaded_message_loop = false; #endif #if !defined(_WIN32) && !defined(__APPLE__) - // Override locale path from OBS binary path to plugin binary path - string locales = - obs_get_module_binary_path(obs_current_module()); - locales = locales.substr(0, locales.find_last_of('/') + 1); - locales += "locales"; - BPtr abs_locales = os_get_abs_path_ptr(locales.c_str()); - CefString(&settings.locales_dir_path) = abs_locales; + // Override locale path from OBS binary path to plugin binary path + string locales = obs_get_module_binary_path(obs_current_module()); + locales = locales.substr(0, locales.find_last_of('/') + 1); + locales += "locales"; + BPtr abs_locales = os_get_abs_path_ptr(locales.c_str()); + CefString(&settings.locales_dir_path) = abs_locales; #endif #if defined(__APPLE__) - std::string binPath = getExecutablePath(); - binPath = binPath.substr(0, - binPath.size() - strlen("/bin/obs64")); - std::string browserSourcePath = binPath; - binPath += "/Frameworks/Chromium Embedded Framework.framework"; - CefString(&settings.framework_dir_path) = binPath; - - // Set the browser-source-path. Streamlabs desktop (if not run within an app) - // will not spin up all the helper apps but at least we will not crash. - // Streamlabs desktop.app and OBS.app backend will work properly with browser - // sources because they can both spin up the helper processes. - browserSourcePath += - "/Frameworks/obs64 Helper.app/Contents/MacOS/obs64 Helper"; - CefString(&settings.browser_subprocess_path) = - browserSourcePath; - blog(LOG_INFO, - "Set browser_subprocess_path for obs64 (app bundle): [%s]", - browserSourcePath.c_str()); -#endif - std::string obs_locale = obs_get_locale(); - std::string accepted_languages; - if (obs_locale != "en-US") { - accepted_languages = obs_locale; - accepted_languages += ","; - accepted_languages += "en-US,en"; - } else { - accepted_languages = "en-US,en"; - } + std::string binPath = getExecutablePath(); + binPath = binPath.substr(0, binPath.size() - strlen("/bin/obs64")); + std::string browserSourcePath = binPath; + binPath += "/Frameworks/Chromium Embedded Framework.framework"; + CefString(&settings.framework_dir_path) = binPath; + + // Set the browser-source-path. Streamlabs desktop (if not run within an app) + // will not spin up all the helper apps but at least we will not crash. + // Streamlabs desktop.app and OBS.app backend will work properly with browser + // sources because they can both spin up the helper processes. + browserSourcePath += "/Frameworks/obs64 Helper.app/Contents/MacOS/obs64 Helper"; + CefString(&settings.browser_subprocess_path) = browserSourcePath; + blog(LOG_INFO, "Set browser_subprocess_path for obs64 (app bundle): [%s]", browserSourcePath.c_str()); +#endif + std::string obs_locale = obs_get_locale(); + std::string accepted_languages; + if (obs_locale != "en-US") { + accepted_languages = obs_locale; + accepted_languages += ","; + accepted_languages += "en-US,en"; + } else { + accepted_languages = "en-US,en"; + } - BPtr conf_path_abs = os_get_abs_path_ptr(conf_path); - CefString(&settings.locale) = obs_get_locale(); - CefString(&settings.accept_language_list) = accepted_languages; + BPtr conf_path_abs = os_get_abs_path_ptr(conf_path); + CefString(&settings.locale) = obs_get_locale(); + CefString(&settings.accept_language_list) = accepted_languages; #if CHROME_VERSION_BUILD <= 6533 - settings.persist_user_preferences = 1; + settings.persist_user_preferences = 1; #endif - CefString(&settings.cache_path) = conf_path_abs; + CefString(&settings.cache_path) = conf_path_abs; #if !defined(__APPLE__) || defined(ENABLE_BROWSER_LEGACY) + /* A sandboxed Windows process must use the main executable as its + * subprocess, so leave this path empty. */ +#ifdef _WIN32 + if (!use_sandbox) { char *abs_path = os_get_abs_path_ptr(path.c_str()); CefString(&settings.browser_subprocess_path) = abs_path; bfree(abs_path); + } +#else + char *abs_path = os_get_abs_path_ptr(path.c_str()); + CefString(&settings.browser_subprocess_path) = abs_path; + bfree(abs_path); +#endif #endif - bool tex_sharing_avail = false; + bool tex_sharing_avail = false; #ifdef ENABLE_BROWSER_SHARED_TEXTURE if (hwaccel) { @@ -464,7 +543,28 @@ static void BrowserInit(obs_data_t *settings_obs) #endif #ifdef _WIN32 - CefExecuteProcess(args, app, nullptr); + void *sandbox_info = nullptr; + if (use_sandbox) { + if (!CreateBrowserSandboxInfo(sandbox_exports, &sandbox_info)) { + blog(LOG_ERROR, "[obs-browser]: OBS failed to create Windows sandbox information."); + app = nullptr; + SignalBrowserInitializationComplete(); + return false; + } + } + + const int execute_result = CefExecuteProcess(args, app, sandbox_info); + if (!BrowserSandboxExecuteProcessSucceeded(execute_result)) { + if (sandbox_info) + sandbox_exports.destroy(sandbox_info); + blog(LOG_ERROR, + "[obs-browser]: CefExecuteProcess failed before initialization " + "(result %d).", + execute_result); + app = nullptr; + SignalBrowserInitializationComplete(); + return false; + } #endif #if !defined(_WIN32) @@ -472,7 +572,9 @@ static void BrowserInit(obs_data_t *settings_obs) bool success = CefInitialize(args, settings, app, nullptr); RestoreSignalHandlers(); #else - bool success = CefInitialize(args, settings, app, nullptr); + bool success = CefInitialize(args, settings, app, sandbox_info); + if (sandbox_info) + sandbox_exports.destroy(sandbox_info); #endif if (!success) { @@ -481,16 +583,17 @@ static void BrowserInit(obs_data_t *settings_obs) #else blog(LOG_ERROR, "[obs-browser]: CEF failed to initialize."); #endif - return; + app = nullptr; + SignalBrowserInitializationComplete(); + return false; } // Register custom scheme handler for local browser sources CefRegisterSchemeHandlerFactory("http", "absolute", new BrowserSchemeHandlerFactory()); - os_event_signal(cef_started_event); -#if defined(__APPLE__) && defined(USE_UI_LOOP) - }); -#endif + cef_initialized.store(true); + SignalBrowserInitializationComplete(); + return true; } #if defined(USE_UI_LOOP) && defined(__APPLE) @@ -499,6 +602,10 @@ extern BrowserCppInt *message; static void BrowserShutdown(void) { + std::lock_guard lifecycle_lock(CefLifecycleMutex()); + if (!cef_initialized.exchange(false)) + return; + CefClearSchemeHandlerFactories(); #ifdef ENABLE_BROWSER_QT_LOOP @@ -518,7 +625,14 @@ static void BrowserShutdown(void) #ifndef ENABLE_BROWSER_QT_LOOP static void BrowserManagerThread(obs_data_t *settings) { - BrowserInit(settings); + if (!BrowserInit(settings)) + return; + + if (browser_shutdown_requested.load()) { + BrowserShutdown(); + return; + } + CefRunMessageLoop(); BrowserShutdown(); } @@ -531,10 +645,13 @@ extern "C" EXPORT void obs_browser_initialize(obs_data_t *settings) BrowserInit(settings); #elif __APPLE__ - ExecuteTask([&settings]() { - BrowserInit(settings); - const float interval = 0.01f; // 10ms, for ex. 16.6ms = 60fps. We want a bit more than 60fps to keep CEF happy? - DoCefMessageLoopTimer(interval); // Do not block the main queue so we avoid calling CefRunMessageLoop() + ExecuteTask([settings]() { + if (!BrowserInit(settings)) + return; + const float interval = + 0.01f; // 10ms, for ex. 16.6ms = 60fps. We want a bit more than 60fps to keep CEF happy? + DoCefMessageLoopTimer( + interval); // Do not block the main queue so we avoid calling CefRunMessageLoop() }); #else auto binded_fn = bind(BrowserManagerThread, settings); @@ -560,20 +677,16 @@ void RegisterBrowserSource() return obs_module_text("BrowserSource"); }; info.create = [](obs_data_t *settings, obs_source_t *source) -> void * { - blog(LOG_INFO, - "Browser Source, INIT via info.create , settings %p source %p", - settings, source); + blog(LOG_INFO, "Browser Source, INIT via info.create , settings %p source %p", settings, source); obs_browser_initialize(settings); if (manager_initialized && app) { - bool enabled = - obs_data_get_bool(settings, "is_media_flag"); + bool enabled = obs_data_get_bool(settings, "is_media_flag"); app->AddFlag(enabled); } obs_source_set_audio_mixers(source, 0xFF); - obs_source_set_monitoring_type( - source, OBS_MONITORING_TYPE_MONITOR_ONLY); + obs_source_set_monitoring_type(source, OBS_MONITORING_TYPE_MONITOR_ONLY); BrowserSource *bs = new BrowserSource(settings, source); blog(LOG_INFO, "Browserapp pointer: %p", app.get()); @@ -586,8 +699,7 @@ void RegisterBrowserSource() info.update = [](void *data, obs_data_t *settings) { BrowserSource *bs = static_cast(data); if (app) { - bool enabled = - obs_data_get_bool(settings, "is_media_flag"); + bool enabled = obs_data_get_bool(settings, "is_media_flag"); app->media_flag = enabled ? 1 : 0; } bs->Update(settings); @@ -933,6 +1045,7 @@ void obs_module_post_load(void) void obs_module_unload(void) { + browser_shutdown_requested.store(true); #if defined(USE_UI_LOOP) || defined(ENABLE_BROWSER_QT_LOOP) BrowserShutdown(); #elif __APPLE__ @@ -942,7 +1055,7 @@ void obs_module_unload(void) }); #else if (manager_thread.joinable()) { - if (!QueueCEFTask([]() { CefQuitMessageLoop(); })) + if (cef_initialized.load() && !QueueCEFTask([]() { CefQuitMessageLoop(); })) blog(LOG_DEBUG, "[obs-browser]: Failed to post CefQuit task to loop"); manager_thread.join(); diff --git a/obs-browser-sandbox-selection.cpp b/obs-browser-sandbox-selection.cpp new file mode 100644 index 000000000..9c02542d8 --- /dev/null +++ b/obs-browser-sandbox-selection.cpp @@ -0,0 +1,46 @@ +/****************************************************************************** + Copyright (C) 2026 by Streamlabs + + 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 2 of the License, or + (at your option) any later version. + ******************************************************************************/ + +#include "obs-browser-sandbox-selection.hpp" + +#ifdef _WIN32 + +BrowserSandboxMode SelectBrowserSandboxMode(const BrowserSandboxExports &exports) +{ + if (!exports.abi_version) + return BrowserSandboxMode::Legacy; + + if (exports.abi_version() != OBS_BROWSER_SANDBOX_ABI_VERSION) + return BrowserSandboxMode::AbiVersionMismatch; + + if (!exports.create || !exports.destroy) + return BrowserSandboxMode::MissingLifecycleExports; + + return BrowserSandboxMode::Enabled; +} + +bool CreateBrowserSandboxInfo(const BrowserSandboxExports &exports, void **sandbox_info) +{ + if (!sandbox_info) + return false; + + *sandbox_info = nullptr; + if (!exports.create) + return false; + + *sandbox_info = exports.create(); + return *sandbox_info != nullptr; +} + +bool BrowserSandboxExecuteProcessSucceeded(int execute_result) +{ + return execute_result == -1; +} + +#endif diff --git a/obs-browser-sandbox-selection.hpp b/obs-browser-sandbox-selection.hpp new file mode 100644 index 000000000..571542b91 --- /dev/null +++ b/obs-browser-sandbox-selection.hpp @@ -0,0 +1,33 @@ +/****************************************************************************** + Copyright (C) 2026 by Streamlabs + + 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 2 of the License, or + (at your option) any later version. + ******************************************************************************/ + +#pragma once + +#ifdef _WIN32 + +#include "obs-browser-sandbox.h" + +enum class BrowserSandboxMode { + Legacy, + Enabled, + AbiVersionMismatch, + MissingLifecycleExports, +}; + +struct BrowserSandboxExports { + obs_browser_sandbox_abi_version_proc abi_version = nullptr; + obs_browser_sandbox_info_create_proc create = nullptr; + obs_browser_sandbox_info_destroy_proc destroy = nullptr; +}; + +BrowserSandboxMode SelectBrowserSandboxMode(const BrowserSandboxExports &exports); +bool CreateBrowserSandboxInfo(const BrowserSandboxExports &exports, void **sandbox_info); +bool BrowserSandboxExecuteProcessSucceeded(int execute_result); + +#endif diff --git a/obs-browser-sandbox.h b/obs-browser-sandbox.h new file mode 100644 index 000000000..cf53389a8 --- /dev/null +++ b/obs-browser-sandbox.h @@ -0,0 +1,57 @@ +/****************************************************************************** + Copyright (C) 2026 by Streamlabs + + 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 2 of the License, or + (at your option) any later version. + ******************************************************************************/ + +#pragma once + +/* + * Windows sandbox ABI shared by the OBS executable and obs-browser. Keep this + * C-only surface stable: the main executable owns CEF's sandbox library, and + * the plugin discovers its lifecycle functions dynamically. + */ +#ifdef _WIN32 + +#include + +#define OBS_BROWSER_SANDBOX_ABI_VERSION 1u + +#if defined(OBS_BROWSER_SANDBOX_HOST_IMPLEMENTATION) +#define OBS_BROWSER_SANDBOX_HOST_API __declspec(dllexport) +#else +#define OBS_BROWSER_SANDBOX_HOST_API +#endif + +#if defined(OBS_BROWSER_SANDBOX_PLUGIN_IMPLEMENTATION) +#define OBS_BROWSER_SANDBOX_PLUGIN_API __declspec(dllexport) +#else +#define OBS_BROWSER_SANDBOX_PLUGIN_API +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +OBS_BROWSER_SANDBOX_HOST_API uint32_t __cdecl obs_browser_sandbox_abi_version(void); +OBS_BROWSER_SANDBOX_HOST_API void *__cdecl obs_browser_sandbox_info_create(void); +OBS_BROWSER_SANDBOX_HOST_API void __cdecl obs_browser_sandbox_info_destroy(void *sandbox_info); + +OBS_BROWSER_SANDBOX_PLUGIN_API int __cdecl obs_browser_execute_subprocess(void *sandbox_info); + +#ifdef __cplusplus +} +#endif + +typedef uint32_t(__cdecl *obs_browser_sandbox_abi_version_proc)(void); +typedef void *(__cdecl *obs_browser_sandbox_info_create_proc)(void); +typedef void(__cdecl *obs_browser_sandbox_info_destroy_proc)(void *sandbox_info); +typedef int(__cdecl *obs_browser_execute_subprocess_proc)(void *sandbox_info); + +#undef OBS_BROWSER_SANDBOX_HOST_API +#undef OBS_BROWSER_SANDBOX_PLUGIN_API + +#endif diff --git a/obs-browser-subprocess-win.cpp b/obs-browser-subprocess-win.cpp new file mode 100644 index 000000000..011df8795 --- /dev/null +++ b/obs-browser-subprocess-win.cpp @@ -0,0 +1,90 @@ +/****************************************************************************** + Copyright (C) 2026 by Streamlabs + + 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 2 of the License, or + (at your option) any later version. + ******************************************************************************/ + +#include "obs-browser-subprocess-win.hpp" + +#ifdef _WIN32 + +#include "browser-app.hpp" +#include "cef-headers.hpp" + +#include + +#include +#include +#include +#include + +namespace { + +void shutdown_check_thread(DWORD parent_pid, DWORD main_thread_id, HANDLE shutdown_event) +{ + HANDLE parent = OpenProcess(SYNCHRONIZE, false, parent_pid); + if (!parent) + return; + + HANDLE handles[2] = {parent, shutdown_event}; + + DWORD ret = WaitForMultipleObjects(2, handles, false, INFINITE); + if (ret == WAIT_OBJECT_0) { + PostThreadMessage(main_thread_id, WM_QUIT, 0, 0); + ret = WaitForSingleObject(shutdown_event, 5000); + if (ret != WAIT_OBJECT_0) + TerminateProcess(GetCurrentProcess(), (UINT)-1); + } + + CloseHandle(parent); +} + +} // namespace + +int ExecuteBrowserSubprocess(void *sandbox_info) +{ + PROCESS_POWER_THROTTLING_STATE power_throttling{}; + power_throttling.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION; + power_throttling.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION; + SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &power_throttling, sizeof(power_throttling)); + + std::thread shutdown_check; + HANDLE shutdown_event = nullptr; + CefMainArgs main_args(GetModuleHandleW(nullptr)); + CefRefPtr command_line = CefCommandLine::CreateCommandLine(); + command_line->InitFromString(::GetCommandLineW()); + + std::string parent_pid_str = command_line->GetSwitchValue("parent_pid"); + if (!parent_pid_str.empty()) { + DWORD parent_pid = 0; + const auto parse_result = std::from_chars(parent_pid_str.data(), + parent_pid_str.data() + parent_pid_str.size(), parent_pid); + if (parse_result.ec == std::errc() && + parse_result.ptr == parent_pid_str.data() + parent_pid_str.size() && parent_pid != 0) { + shutdown_event = CreateEvent(nullptr, true, false, nullptr); + if (shutdown_event) { + shutdown_check = std::thread(shutdown_check_thread, parent_pid, GetCurrentThreadId(), + shutdown_event); + } + } + } + + CefRefPtr main_app(new BrowserApp()); + int ret = CefExecuteProcess(main_args, main_app.get(), sandbox_info); + + /* Chromium browser subprocesses are generally terminated directly, but + * keep the parent watcher cleanup for normal process exits. */ + if (shutdown_check.joinable()) { + SetEvent(shutdown_event); + shutdown_check.join(); + } + if (shutdown_event) + CloseHandle(shutdown_event); + + return ret; +} + +#endif diff --git a/obs-browser-subprocess-win.hpp b/obs-browser-subprocess-win.hpp new file mode 100644 index 000000000..e0552eaee --- /dev/null +++ b/obs-browser-subprocess-win.hpp @@ -0,0 +1,14 @@ +/****************************************************************************** + Copyright (C) 2026 by Streamlabs + + 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 2 of the License, or + (at your option) any later version. + ******************************************************************************/ + +#pragma once + +#ifdef _WIN32 +int ExecuteBrowserSubprocess(void *sandbox_info); +#endif diff --git a/panel/browser-panel.cpp b/panel/browser-panel.cpp index c335b2e8c..07e444ac4 100644 --- a/panel/browser-panel.cpp +++ b/panel/browser-panel.cpp @@ -27,6 +27,7 @@ extern bool QueueCEFTask(std::function task); extern "C" void obs_browser_initialize(void); +extern bool obs_browser_initialized(void); extern os_event_t *cef_started_event; std::mutex popup_whitelist_mutex; @@ -102,7 +103,7 @@ struct QCefCookieManagerInternal : QCefCookieManager { QCefCookieManagerInternal(const std::string &storage_path, bool persist_session_cookies) { - if (os_event_try(cef_started_event) != 0) + if (!obs_browser_initialized()) throw "Browser thread not initialized"; BPtr rpath = obs_module_config_path(storage_path.c_str()); @@ -526,21 +527,21 @@ struct QCefInternal : QCef { bool QCefInternal::init_browser(void) { - if (os_event_try(cef_started_event) == 0) + if (obs_browser_initialized()) return true; obs_browser_initialize(); - return false; + return obs_browser_initialized(); } bool QCefInternal::initialized(void) { - return os_event_try(cef_started_event) == 0; + return obs_browser_initialized(); } bool QCefInternal::wait_for_browser_init(void) { - return os_event_wait(cef_started_event) == 0; + return cef_started_event && os_event_wait(cef_started_event) == 0 && obs_browser_initialized(); } QCefWidget *QCefInternal::create_widget(QWidget *parent, const std::string &url, QCefCookieManager *cm) diff --git a/tests/obs-browser-sandbox-selection-test.cpp b/tests/obs-browser-sandbox-selection-test.cpp new file mode 100644 index 000000000..2f8b9a40c --- /dev/null +++ b/tests/obs-browser-sandbox-selection-test.cpp @@ -0,0 +1,81 @@ +/****************************************************************************** + Copyright (C) 2026 by Streamlabs + + 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 2 of the License, or + (at your option) any later version. + ******************************************************************************/ + +#include "obs-browser-sandbox-selection.hpp" + +#include + +namespace { + +uint32_t __cdecl expected_abi_version() +{ + return OBS_BROWSER_SANDBOX_ABI_VERSION; +} + +uint32_t __cdecl incompatible_abi_version() +{ + return OBS_BROWSER_SANDBOX_ABI_VERSION + 1; +} + +void *__cdecl create_sandbox_info() +{ + return reinterpret_cast(1); +} + +void *__cdecl create_null_sandbox_info() +{ + return nullptr; +} + +void __cdecl destroy_sandbox_info(void *) {} + +bool expect(bool condition, const char *message) +{ + if (condition) + return true; + + std::fprintf(stderr, "FAILED: %s\n", message); + return false; +} + +} // namespace + +int main() +{ + bool passed = true; + BrowserSandboxExports exports; + + passed &= expect(SelectBrowserSandboxMode(exports) == BrowserSandboxMode::Legacy, + "missing ABI export uses the legacy helper path"); + + exports.abi_version = incompatible_abi_version; + passed &= expect(SelectBrowserSandboxMode(exports) == BrowserSandboxMode::AbiVersionMismatch, + "an advertised incompatible ABI fails closed"); + + exports.abi_version = expected_abi_version; + exports.destroy = destroy_sandbox_info; + passed &= expect(SelectBrowserSandboxMode(exports) == BrowserSandboxMode::MissingLifecycleExports, + "an advertised ABI without a create lifecycle export fails closed"); + + exports.create = create_null_sandbox_info; + void *sandbox_info = reinterpret_cast(1); + passed &= expect(SelectBrowserSandboxMode(exports) == BrowserSandboxMode::Enabled, + "complete matching exports select the sandbox path"); + passed &= expect(!CreateBrowserSandboxInfo(exports, &sandbox_info) && !sandbox_info, + "a null sandbox-info creation fails closed"); + + exports.create = create_sandbox_info; + passed &= expect(CreateBrowserSandboxInfo(exports, &sandbox_info) && sandbox_info, + "a non-null sandbox-info creation succeeds"); + passed &= + expect(BrowserSandboxExecuteProcessSucceeded(-1), "the browser-process CefExecuteProcess result is -1"); + passed &= expect(!BrowserSandboxExecuteProcessSucceeded(0), "a non--1 CefExecuteProcess result fails closed"); + + return passed ? 0 : 1; +}