Merge pull request #8455 from derrod/safe-mode

Add Safe Mode
This commit is contained in:
Lain
2023-07-22 16:56:40 -07:00
committed by GitHub
13 changed files with 257 additions and 12 deletions

View File

@@ -108,6 +108,10 @@ foreach(graphics_library IN ITEMS opengl metal d3d11)
endif()
endforeach()
get_property(obs_module_list GLOBAL PROPERTY OBS_MODULES_ENABLED)
list(JOIN obs_module_list "|" SAFE_MODULES)
target_compile_definitions(obs-studio PRIVATE "SAFE_MODULES=\"${SAFE_MODULES}\"")
# cmake-format: off
set_target_properties_obs(obs-studio PROPERTIES FOLDER frontend OUTPUT_NAME "$<IF:$<PLATFORM_ID:Windows>,obs64,obs>")
# cmake-format: on

View File

@@ -486,6 +486,10 @@ source_group(
unset(_SOURCES)
unset(_UI)
get_property(OBS_MODULE_LIST GLOBAL PROPERTY OBS_MODULE_LIST)
list(JOIN OBS_MODULE_LIST "|" SAFE_MODULES)
target_compile_definitions(obs PRIVATE "SAFE_MODULES=\"${SAFE_MODULES}\"")
define_graphic_modules(obs)
setup_obs_app(obs)
setup_target_resources(obs obs-studio)

View File

@@ -125,6 +125,15 @@ AlreadyRunning.Title="OBS is already running"
AlreadyRunning.Text="OBS is already running! Unless you meant to do this, please shut down any existing instances of OBS before trying to run a new instance. If you have OBS set to minimize to the system tray, please check to see if it's still running there."
AlreadyRunning.LaunchAnyway="Launch Anyway"
# warning if auto Safe Mode has engaged
AutoSafeMode.Title="Safe Mode"
AutoSafeMode.Text="OBS did not shut down properly during your last session.\n\nWould you like to start in Safe Mode (third-party plugins, scripting, and websockets disabled)?"
AutoSafeMode.LaunchSafe="Run in Safe Mode"
AutoSafeMode.LaunchNormal="Run Normally"
## Restart Option
SafeMode.Restart="Do you want to restart OBS in Safe Mode (third-party plugins, scripting, and websockets disabled)?"
SafeMode.RestartNormal="Do you want to restart OBS in Normal Mode?"
ChromeOS.Title="Unsupported Platform"
ChromeOS.Text="OBS appears to be running inside a ChromeOS container. This platform is unsupported."
@@ -834,6 +843,8 @@ Basic.MainMenu.Help.Logs.UploadLastLog="Upload &Previous Log File"
Basic.MainMenu.Help.Logs.ViewCurrentLog="&View Current Log"
Basic.MainMenu.Help.CheckForUpdates="Check For Updates"
Basic.MainMenu.Help.Repair="Check File Integrity"
Basic.MainMenu.Help.RestartSafeMode="Restart in Safe Mode"
Basic.MainMenu.Help.RestartNormal="Restart in Normal Mode"
Basic.MainMenu.Help.CrashLogs="Crash &Reports"
Basic.MainMenu.Help.CrashLogs.ShowLogs="&Show Crash Reports"
Basic.MainMenu.Help.CrashLogs.UploadLastLog="Upload &Previous Crash Report"

View File

@@ -532,9 +532,11 @@
<addaction name="separator"/>
<addaction name="actionRepair"/>
<addaction name="actionCheckForUpdates"/>
<addaction name="actionRestartSafe"/>
<addaction name="actionShowMacPermissions"/>
<addaction name="separator"/>
<addaction name="actionShowWhatsNew"/>
<addaction name="actionShowAbout"/>
<addaction name="actionShowMacPermissions"/>
<addaction name="separator"/>
</widget>
<widget class="QMenu" name="menuBasic_MainMenu_Edit">
@@ -2004,6 +2006,11 @@
<string>Basic.MainMenu.Help.Repair</string>
</property>
</action>
<action name="actionRestartSafe">
<property name="text">
<string>Basic.MainMenu.Help.RestartSafeMode</string>
</property>
</action>
<action name="actionInteract">
<property name="text">
<string>Interact</string>

View File

@@ -87,6 +87,10 @@ static string lastCrashLogFile;
bool portable_mode = false;
bool steam = false;
bool safe_mode = false;
bool disable_3p_plugins = false;
bool unclean_shutdown = false;
bool disable_shutdown_check = false;
static bool multi = false;
static bool log_verbose = false;
static bool unfiltered_log = false;
@@ -105,6 +109,7 @@ string opt_starting_profile;
string opt_starting_scene;
bool restart = false;
bool restart_safe = false;
QPointer<OBSLogViewer> obsLogViewer;
@@ -1715,6 +1720,12 @@ bool OBSApp::OBSInit()
QT_VERSION_STR);
blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false");
if (safe_mode) {
blog(LOG_WARNING, "Safe Mode enabled.");
} else if (disable_3p_plugins) {
blog(LOG_WARNING, "Third-party plugins disabled.");
}
setQuitOnLastWindowClosed(false);
mainWindow = new OBSBasic();
@@ -2429,6 +2440,10 @@ static int run_program(fstream &logFile, int argc, char *argv[])
blog(LOG_WARNING, "================================");
blog(LOG_WARNING, "User is now running multiple "
"instances of OBS!");
/* Clear unclean_shutdown flag as multiple instances
* running from the same config will lead to a
* false-positive detection.*/
unclean_shutdown = false;
}
/* --------------------------------------- */
@@ -2453,6 +2468,34 @@ static int run_program(fstream &logFile, int argc, char *argv[])
if (!created_log)
create_log_file(logFile);
if (unclean_shutdown) {
blog(LOG_WARNING,
"[Safe Mode] Unclean shutdown detected!");
}
if (unclean_shutdown && !safe_mode) {
QMessageBox mb(QMessageBox::Warning,
QTStr("AutoSafeMode.Title"),
QTStr("AutoSafeMode.Text"));
QPushButton *launchSafeButton =
mb.addButton(QTStr("AutoSafeMode.LaunchSafe"),
QMessageBox::AcceptRole);
QPushButton *launchNormalButton =
mb.addButton(QTStr("AutoSafeMode.LaunchNormal"),
QMessageBox::RejectRole);
mb.setDefaultButton(launchNormalButton);
mb.exec();
safe_mode = mb.clickedButton() == launchSafeButton;
if (safe_mode) {
blog(LOG_INFO,
"[Safe Mode] User has launched in Safe Mode.");
} else {
blog(LOG_WARNING,
"[Safe Mode] User elected to launch normally.");
}
}
qInstallMessageHandler([](QtMsgType type,
const QMessageLogContext &,
const QString &message) {
@@ -2540,9 +2583,18 @@ static int run_program(fstream &logFile, int argc, char *argv[])
OBSErrorBox(nullptr, "%s", error);
}
if (restart)
QProcess::startDetached(qApp->arguments()[0],
qApp->arguments());
if (restart || restart_safe) {
auto args = qApp->arguments();
auto executable = args[0];
if (restart_safe) {
args.append("--safe-mode");
} else {
args.removeAll("--safe-mode");
}
QProcess::startDetached(executable, args);
}
return ret;
}
@@ -3194,6 +3246,32 @@ static void upgrade_settings(void)
os_closedir(dir);
}
static void check_safe_mode_sentinel(void)
{
#ifndef NDEBUG
/* Safe Mode detection is disabled in Debug builds to keep developers
* somewhat sane. */
return;
#else
if (disable_shutdown_check)
return;
BPtr sentinelPath = GetConfigPathPtr("obs-studio/safe_mode");
if (os_file_exists(sentinelPath)) {
unclean_shutdown = true;
return;
}
os_quick_write_utf8_file(sentinelPath, nullptr, 0, false);
#endif
}
static void delete_safe_mode_sentinel(void)
{
BPtr sentinelPath = GetConfigPathPtr("obs-studio/safe_mode");
os_unlink(sentinelPath);
}
#ifndef _WIN32
void OBSApp::SigIntSignalHandler(int s)
{
@@ -3281,6 +3359,17 @@ int main(int argc, char *argv[])
} else if (arg_is(argv[i], "--verbose", nullptr)) {
log_verbose = true;
} else if (arg_is(argv[i], "--safe-mode", nullptr)) {
safe_mode = true;
} else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) {
disable_3p_plugins = true;
} else if (arg_is(argv[i], "--disable-shutdown-check",
nullptr)) {
/* This exists mostly to bypass the dialog during development. */
disable_shutdown_check = true;
} else if (arg_is(argv[i], "--always-on-top", nullptr)) {
opt_always_on_top = true;
@@ -3347,6 +3436,9 @@ int main(int argc, char *argv[])
"--portable, -p: Use portable mode.\n"
#endif
"--multi, -m: Don't warn when launching multiple instances.\n\n"
"--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and websockets).\n"
"--only-bundled-plugins: Only load included (first-party) plugins\n"
"--disable-shutdown-check: Disable unclean shutdown detection.\n"
"--verbose: Make log more verbose.\n"
"--always-on-top: Start in 'always on top' mode.\n\n"
"--unfiltered_log: Make log unfiltered.\n\n"
@@ -3393,6 +3485,7 @@ int main(int argc, char *argv[])
}
#endif
check_safe_mode_sentinel();
upgrade_settings();
fstream logFile;
@@ -3412,6 +3505,7 @@ int main(int argc, char *argv[])
log_blocked_dlls();
#endif
delete_safe_mode_sentinel();
blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs());
base_set_log_handler(nullptr, nullptr);
return ret;

View File

@@ -267,6 +267,8 @@ static inline int GetProfilePath(char *path, size_t size, const char *file)
extern bool portable_mode;
extern bool steam;
extern bool safe_mode;
extern bool disable_3p_plugins;
extern bool opt_start_streaming;
extern bool opt_start_recording;
@@ -278,6 +280,7 @@ extern bool opt_allow_opengl;
extern bool opt_always_on_top;
extern std::string opt_starting_scene;
extern bool restart;
extern bool restart_safe;
#ifdef _WIN32
extern "C" void install_dll_blocklist_hook(void);

View File

@@ -21,6 +21,7 @@
#include <cstddef>
#include <ctime>
#include <functional>
#include <unordered_set>
#include <obs-data.h>
#include <obs.h>
#include <obs.hpp>
@@ -232,6 +233,30 @@ static void AddExtraModulePaths()
#endif
}
/* First-party modules considered to be potentially unsafe to load in Safe Mode
* due to them allowing external code (e.g. scripts) to modify OBS's state. */
static const unordered_set<string> unsafe_modules = {
"frontend-tools", // Scripting
"obs-websocket", // Allows outside modifications
};
static void SetSafeModuleNames()
{
#ifndef SAFE_MODULES
return;
#else
string module;
stringstream modules(SAFE_MODULES);
while (getline(modules, module, '|')) {
/* When only disallowing third-party plugins, still add
* "unsafe" bundled modules to the safe list. */
if (disable_3p_plugins || !unsafe_modules.count(module))
obs_add_safe_module(module.c_str());
}
#endif
}
extern obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main);
void assignDockToggle(QDockWidget *dock, QAction *action)
@@ -801,9 +826,18 @@ void OBSBasic::Save(const char *file)
}
if (api) {
OBSDataAutoRelease moduleObj = obs_data_create();
api->on_save(moduleObj);
obs_data_set_obj(saveData, "modules", moduleObj);
if (safeModeModuleData) {
/* If we're in Safe Mode and have retained unloaded
* plugin data, update the existing data object instead
* of creating a new one. */
api->on_save(safeModeModuleData);
obs_data_set_obj(saveData, "modules",
safeModeModuleData);
} else {
OBSDataAutoRelease moduleObj = obs_data_create();
api->on_save(moduleObj);
obs_data_set_obj(saveData, "modules", moduleObj);
}
}
if (!obs_data_save_json_safe(saveData, file, "tmp", "bak"))
@@ -1084,6 +1118,12 @@ void OBSBasic::LoadData(obs_data_t *data, const char *file)
if (api)
api->on_preload(modulesObj);
if (safe_mode || disable_3p_plugins) {
/* Keep a reference to "modules" data so plugins that are not
* loaded do not have their collection specific data lost. */
safeModeModuleData = obs_data_get_obj(data, "modules");
}
OBSDataArrayAutoRelease sceneOrder =
obs_data_get_array(data, "scene_order");
OBSDataArrayAutoRelease sources = obs_data_get_array(data, "sources");
@@ -1259,14 +1299,14 @@ retryScene:
if (!opt_starting_scene.empty())
opt_starting_scene.clear();
if (opt_start_streaming) {
if (opt_start_streaming && !safe_mode) {
blog(LOG_INFO, "Starting stream due to command line parameter");
QMetaObject::invokeMethod(this, "StartStreaming",
Qt::QueuedConnection);
opt_start_streaming = false;
}
if (opt_start_recording) {
if (opt_start_recording && !safe_mode) {
blog(LOG_INFO,
"Starting recording due to command line parameter");
QMetaObject::invokeMethod(this, "StartRecording",
@@ -1274,13 +1314,13 @@ retryScene:
opt_start_recording = false;
}
if (opt_start_replaybuffer) {
if (opt_start_replaybuffer && !safe_mode) {
QMetaObject::invokeMethod(this, "StartReplayBuffer",
Qt::QueuedConnection);
opt_start_replaybuffer = false;
}
if (opt_start_virtualcam) {
if (opt_start_virtualcam && !safe_mode) {
QMetaObject::invokeMethod(this, "StartVirtualCam",
Qt::QueuedConnection);
opt_start_virtualcam = false;
@@ -1961,7 +2001,14 @@ void OBSBasic::OBSInit()
#endif
struct obs_module_failure_info mfi;
AddExtraModulePaths();
/* Safe Mode disables third-party plugins so we don't need to add earch
* paths outside the OBS bundle/installation. */
if (safe_mode || disable_3p_plugins) {
SetSafeModuleNames();
} else {
AddExtraModulePaths();
}
blog(LOG_INFO, "---------------------------------");
obs_load_all_modules2(&mfi);
blog(LOG_INFO, "---------------------------------");
@@ -2274,6 +2321,11 @@ void OBSBasic::OBSInit()
ui->actionShowWhatsNew = nullptr;
#endif
if (safe_mode) {
ui->actionRestartSafe->setText(
QTStr("Basic.MainMenu.Help.RestartNormal"));
}
UpdatePreviewProgramIndicators();
OnFirstLoad();
@@ -4904,6 +4956,7 @@ void OBSBasic::ClearSceneData()
outputHandler->UpdateVirtualCamOutputSource();
}
safeModeModuleData = nullptr;
lastScene = nullptr;
swapScene = nullptr;
programScene = nullptr;
@@ -6563,6 +6616,20 @@ void OBSBasic::on_actionRepair_triggered()
#endif
}
void OBSBasic::on_actionRestartSafe_triggered()
{
QMessageBox::StandardButton button = OBSMessageBox::question(
this, QTStr("Restart"),
safe_mode ? QTStr("SafeMode.RestartNormal")
: QTStr("SafeMode.Restart"));
if (button == QMessageBox::Yes) {
restart = safe_mode;
restart_safe = !safe_mode;
close();
}
}
void OBSBasic::logUploadFinished(const QString &text, const QString &error)
{
ui->menuLogFiles->setEnabled(true);
@@ -9270,6 +9337,8 @@ void OBSBasic::UpdateTitleBar()
name << "Studio ";
name << App()->GetVersionString(false);
if (safe_mode)
name << " (SAFE MODE)";
if (App()->IsPortableMode())
name << " - " << Str("TitleBar.PortableMode");

View File

@@ -231,6 +231,8 @@ private:
QList<QPointer<QDockWidget>> oldExtraDocks;
QStringList oldExtraDockNames;
OBSDataAutoRelease safeModeModuleData;
bool loaded = false;
long disableSaving = 1;
bool projectChanged = false;
@@ -1044,6 +1046,7 @@ private slots:
void on_actionCheckForUpdates_triggered();
void on_actionRepair_triggered();
void on_actionShowWhatsNew_triggered();
void on_actionRestartSafe_triggered();
void on_actionShowCrashLogs_triggered();
void on_actionUploadLastCrashLog_triggered();

View File

@@ -271,6 +271,15 @@ plugin modules.
---------------------
.. function:: void *obs_add_safe_module(const char *name)
Adds a *name* to the list of modules allowed to load in Safe Mode.
If the list is empty, all modules are allowed.
:param name: The name of the module (filename sans extension).
---------------------
.. function:: void obs_module_failure_info_free(struct obs_module_failure_info *mfi)
Frees data allocated data used in the *mfi* parameter (calls

View File

@@ -466,6 +466,7 @@ typedef DARRAY(struct obs_source_info) obs_source_info_array_t;
struct obs_core {
struct obs_module *first_module;
DARRAY(struct obs_module_path) module_paths;
DARRAY(char *) safe_modules;
obs_source_info_array_t source_types;
obs_source_info_array_t input_types;

View File

@@ -274,6 +274,15 @@ void obs_add_module_path(const char *bin, const char *data)
da_push_back(obs->module_paths, &omp);
}
void obs_add_safe_module(const char *name)
{
if (!obs || !name)
return;
char *item = bstrdup(name);
da_push_back(obs->safe_modules, &item);
}
extern void get_plugin_info(const char *path, bool *is_obs_plugin,
bool *can_load);
@@ -282,6 +291,19 @@ struct fail_info {
size_t fail_count;
};
static bool is_safe_module(const char *name)
{
if (!obs->safe_modules.num)
return true;
for (size_t i = 0; i < obs->safe_modules.num; i++) {
if (strcmp(name, obs->safe_modules.array[i]) == 0)
return true;
}
return false;
}
static void load_all_callback(void *param, const struct obs_module_info2 *info)
{
struct fail_info *fail_info = param;
@@ -298,6 +320,12 @@ static void load_all_callback(void *param, const struct obs_module_info2 *info)
return;
}
if (!is_safe_module(info->name)) {
blog(LOG_WARNING, "Skipping module '%s', not on safe list",
info->name);
return;
}
if (!can_load_obs_plugin) {
blog(LOG_WARNING,
"Skipping module '%s' due to possible "

View File

@@ -1441,6 +1441,10 @@ void obs_shutdown(void)
free_module_path(obs->module_paths.array + i);
da_free(obs->module_paths);
for (size_t i = 0; i < obs->safe_modules.num; i++)
bfree(obs->safe_modules.array[i]);
da_free(obs->safe_modules);
if (obs->name_store_owned)
profiler_name_store_free(obs->name_store);

View File

@@ -517,6 +517,14 @@ EXPORT const char *obs_get_module_data_path(obs_module_t *module);
*/
EXPORT void obs_add_module_path(const char *bin, const char *data);
/**
* Adds a module to the list of modules allowed to load in Safe Mode.
* If the list is empty, all modules are allowed.
*
* @param name Specifies the module's name (filename sans extension).
*/
EXPORT void obs_add_safe_module(const char *name);
/** Automatically loads all modules from module paths (convenience function) */
EXPORT void obs_load_all_modules(void);