From 707a3ead8b0bedaad952a2d8d2fb934aa6bc8789 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 24 Apr 2023 16:45:49 +0200 Subject: [PATCH] feat(dtmm): Guard certain Lua libraries behind a setting Libraries like `io`, `os` and `ffi` allow practically unrestricted access to the system's files and running arbitrary operations. The base game removes them for this reason, and while we don't want to disable them permanently, very few mods should ever have a need for them. So we hide them behind a setting, worded so that people only enable it when absolutely needed. Closes #112. --- Cargo.lock | 1 + crates/dtmm/Cargo.toml | 1 + crates/dtmm/assets/mod_main.lua | 34 ++++++++++++++++++------- crates/dtmm/src/controller/game.rs | 10 +++++++- crates/dtmm/src/state/data.rs | 2 ++ crates/dtmm/src/state/delegate.rs | 9 +++++-- crates/dtmm/src/ui/widget/controller.rs | 12 +++++++-- crates/dtmm/src/ui/window/main.rs | 25 ++++++++++++++++++ crates/dtmm/src/util/config.rs | 5 ++++ 9 files changed, 85 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba688e1..bd49702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,6 +855,7 @@ dependencies = [ "sdk", "serde", "serde_sjson", + "string_template", "strip-ansi-escapes", "time", "tokio", diff --git a/crates/dtmm/Cargo.toml b/crates/dtmm/Cargo.toml index 18ba7b1..86444be 100644 --- a/crates/dtmm/Cargo.toml +++ b/crates/dtmm/Cargo.toml @@ -32,3 +32,4 @@ colors-transform = "0.2.11" usvg = "0.25.0" druid-widget-nursery = "0.1" ansi-parser = "0.8.0" +string_template = "0.2.1" diff --git a/crates/dtmm/assets/mod_main.lua b/crates/dtmm/assets/mod_main.lua index 715397f..f811c5f 100644 --- a/crates/dtmm/assets/mod_main.lua +++ b/crates/dtmm/assets/mod_main.lua @@ -107,19 +107,35 @@ log("mod_main", "Initializing mods...") local require_store = {} +-- This token is treated as a string template and filled by DTMM during deployment. +-- This allows hiding unsafe I/O functions behind a setting. +-- It's also a valid table definition, thereby degrading gracefully when not replaced. +local is_io_enabled = {{is_io_enabled}} -- luacheck: ignore 113 +local lua_libs = { + debug = debug, + os = { + date = os.date, + time = os.time, + clock = os.clock, + getenv = os.getenv, + difftime = os.difftime, + }, + load = load, + loadfile = loadfile, + loadstring = loadstring, +} + +if is_io_enabled then + lua_libs.io = io + lua_libs.os = os + lua_libs.ffi = ffi +end + Mods = { -- Keep a backup of certain system libraries before -- Fatshark's code scrubs them. -- The loader can then decide to pass them on to mods, or ignore them - lua = setmetatable({}, { - io = io, - debug = debug, - ffi = ffi, - os = os, - load = load, - loadfile = loadfile, - loadstring = loadstring, - }), + lua = setmetatable({}, { __index = lua_libs }), require_store = require_store } diff --git a/crates/dtmm/src/controller/game.rs b/crates/dtmm/src/controller/game.rs index 4f5633e..a5f6fa8 100644 --- a/crates/dtmm/src/controller/game.rs +++ b/crates/dtmm/src/controller/game.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::io::{self, Cursor, ErrorKind}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -15,6 +16,7 @@ use sdk::{ Bundle, BundleDatabase, BundleFile, BundleFileType, BundleFileVariant, FromBinary, ToBinary, }; use serde::{Deserialize, Serialize}; +use string_template::Template; use time::OffsetDateTime; use tokio::fs; use tokio::io::AsyncWriteExt; @@ -452,7 +454,13 @@ async fn patch_boot_bundle(state: Arc) -> Result> { let span = tracing::debug_span!("Importing mod main script"); let _enter = span.enter(); - let lua = include_str!("../../assets/mod_main.lua"); + let is_io_enabled = format!("{}", state.is_io_enabled); + let mut data = HashMap::new(); + data.insert("is_io_enabled", is_io_enabled.as_str()); + + let tmpl = include_str!("../../assets/mod_main.lua"); + let lua = Template::new(tmpl).render(&data); + tracing::trace!("Main script rendered:\n===========\n{}\n=============", lua); let file = lua::compile(MOD_BOOT_SCRIPT, lua).wrap_err("Failed to compile mod main Lua file")?; diff --git a/crates/dtmm/src/state/data.rs b/crates/dtmm/src/state/data.rs index b1adcb6..55774aa 100644 --- a/crates/dtmm/src/state/data.rs +++ b/crates/dtmm/src/state/data.rs @@ -153,6 +153,7 @@ pub(crate) struct State { pub is_save_in_progress: bool, pub is_next_save_pending: bool, pub is_update_in_progress: bool, + pub is_io_enabled: bool, pub game_dir: Arc, pub data_dir: Arc, pub nexus_api_key: Arc, @@ -189,6 +190,7 @@ impl State { is_save_in_progress: false, is_next_save_pending: false, is_update_in_progress: false, + is_io_enabled: false, config_path: Arc::new(PathBuf::new()), game_dir: Arc::new(PathBuf::new()), data_dir: Arc::new(PathBuf::new()), diff --git a/crates/dtmm/src/state/delegate.rs b/crates/dtmm/src/state/delegate.rs index 2e12bd0..97c6e6b 100644 --- a/crates/dtmm/src/state/delegate.rs +++ b/crates/dtmm/src/state/delegate.rs @@ -1,4 +1,5 @@ -use std::{path::PathBuf, sync::Arc}; +use std::path::PathBuf; +use std::sync::Arc; use color_eyre::Report; use druid::im::Vector; @@ -8,8 +9,9 @@ use druid::{ }; use tokio::sync::mpsc::UnboundedSender; +use crate::ui::window; use crate::util::ansi::ansi_to_rich_text; -use crate::{ui::window, util::config::Config}; +use crate::util::config::Config; use super::{ModInfo, State}; @@ -68,6 +70,7 @@ pub(crate) struct ActionState { pub config_path: Arc, pub ctx: Arc, pub nexus_api_key: Arc, + pub is_io_enabled: bool, } impl From for ActionState { @@ -80,6 +83,7 @@ impl From for ActionState { config_path: state.config_path, ctx: state.ctx, nexus_api_key: state.nexus_api_key, + is_io_enabled: state.is_io_enabled, } } } @@ -407,6 +411,7 @@ impl AppDelegate for Delegate { state.config_path = Arc::new(config.path); state.data_dir = Arc::new(config.data_dir); state.game_dir = Arc::new(config.game_dir.unwrap_or_default()); + state.is_io_enabled = config.unsafe_io; } state.loading = false; diff --git a/crates/dtmm/src/ui/widget/controller.rs b/crates/dtmm/src/ui/widget/controller.rs index b6d3806..f789b5a 100644 --- a/crates/dtmm/src/ui/widget/controller.rs +++ b/crates/dtmm/src/ui/widget/controller.rs @@ -80,11 +80,19 @@ impl> Controller for DirtyStateController { ) { // Only start tracking changes after the initial load has finished if old_data.loading == data.loading { - if compare_state_fields!(old_data, data, mods, game_dir, data_dir, nexus_api_key) { + if compare_state_fields!( + old_data, + data, + mods, + game_dir, + data_dir, + nexus_api_key, + is_io_enabled + ) { ctx.submit_command(ACTION_START_SAVE_SETTINGS); } - if compare_state_fields!(old_data, data, mods, game_dir) { + if compare_state_fields!(old_data, data, mods, game_dir, is_io_enabled) { ctx.submit_command(ACTION_SET_DIRTY); } } diff --git a/crates/dtmm/src/ui/window/main.rs b/crates/dtmm/src/ui/window/main.rs index fbedb8f..7aa2819 100644 --- a/crates/dtmm/src/ui/window/main.rs +++ b/crates/dtmm/src/ui/window/main.rs @@ -425,6 +425,29 @@ fn build_view_settings() -> impl Widget { .with_flex_child(TextBox::new().expand_width().lens(State::nexus_api_key), 1.) .expand_width(); + let io_setting = Flex::row() + .must_fill_main_axis(true) + .main_axis_alignment(MainAxisAlignment::Start) + .with_child(Label::new("Enable unsafe I/O:")) + .with_default_spacer() + .with_child(Checkbox::from_label(Label::dynamic( + |enabled: &bool, _: &Env| { + if *enabled { + "Enabled".into() + } else { + "Disabled".into() + } + }, + ))) + .lens(State::is_io_enabled) + .tooltip(|_: &State, _: &Env| { + "Enabling this gives ANY mod full access to your files \ + and the ability to load arbitrary software libraries.\n\ + Only enable this if it is crucial for a mod's functionality, \ + and you are sure none of the ones you have installed are malicious." + }) + .expand_width(); + let content = Flex::column() .must_fill_main_axis(true) .cross_axis_alignment(CrossAxisAlignment::Start) @@ -432,6 +455,8 @@ fn build_view_settings() -> impl Widget { .with_default_spacer() .with_child(game_dir_setting) .with_default_spacer() + .with_child(io_setting) + .with_default_spacer() .with_child(nexus_apy_key_setting); SizedBox::new(content) diff --git a/crates/dtmm/src/util/config.rs b/crates/dtmm/src/util/config.rs index 1cafeae..3a0d0b2 100644 --- a/crates/dtmm/src/util/config.rs +++ b/crates/dtmm/src/util/config.rs @@ -30,6 +30,7 @@ pub(crate) struct ConfigSerialize<'a> { data_dir: &'a Path, nexus_api_key: &'a String, mod_order: Vec>, + unsafe_io: bool, } impl<'a> From<&'a ActionState> for ConfigSerialize<'a> { @@ -38,6 +39,7 @@ impl<'a> From<&'a ActionState> for ConfigSerialize<'a> { game_dir: &state.game_dir, data_dir: &state.data_dir, nexus_api_key: &state.nexus_api_key, + unsafe_io: state.is_io_enabled, mod_order: state .mods .iter() @@ -61,6 +63,8 @@ pub(crate) struct Config { #[serde(default = "get_default_data_dir")] pub data_dir: PathBuf, pub game_dir: Option, + #[serde(default)] + pub unsafe_io: bool, pub nexus_api_key: Option, #[serde(default)] pub mod_order: Vec, @@ -144,6 +148,7 @@ where game_dir: None, nexus_api_key: None, mod_order: Vec::new(), + unsafe_io: false, }; { -- 2.45.3