feat(dtmm): Guard certain Lua libraries behind a setting
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
All checks were successful
lint/clippy Checking for common mistakes and opportunities for code improvement
build/msvc Build for the target platform: msvc
build/linux Build for the target platform: linux
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.
This commit is contained in:
parent
af6c2e9f82
commit
707a3ead8b
9 changed files with 85 additions and 14 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -855,6 +855,7 @@ dependencies = [
|
|||
"sdk",
|
||||
"serde",
|
||||
"serde_sjson",
|
||||
"string_template",
|
||||
"strip-ansi-escapes",
|
||||
"time",
|
||||
"tokio",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ActionState>) -> Result<Vec<Bundle>> {
|
|||
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")?;
|
||||
|
||||
|
|
|
@ -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<PathBuf>,
|
||||
pub data_dir: Arc<PathBuf>,
|
||||
pub nexus_api_key: Arc<String>,
|
||||
|
@ -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()),
|
||||
|
|
|
@ -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<PathBuf>,
|
||||
pub ctx: Arc<sdk::Context>,
|
||||
pub nexus_api_key: Arc<String>,
|
||||
pub is_io_enabled: bool,
|
||||
}
|
||||
|
||||
impl From<State> for ActionState {
|
||||
|
@ -80,6 +83,7 @@ impl From<State> 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<State> 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;
|
||||
|
|
|
@ -80,11 +80,19 @@ impl<W: Widget<State>> Controller<State, W> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -425,6 +425,29 @@ fn build_view_settings() -> impl Widget<State> {
|
|||
.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<State> {
|
|||
.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)
|
||||
|
|
|
@ -30,6 +30,7 @@ pub(crate) struct ConfigSerialize<'a> {
|
|||
data_dir: &'a Path,
|
||||
nexus_api_key: &'a String,
|
||||
mod_order: Vec<LoadOrderEntrySerialize<'a>>,
|
||||
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<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub unsafe_io: bool,
|
||||
pub nexus_api_key: Option<String>,
|
||||
#[serde(default)]
|
||||
pub mod_order: Vec<LoadOrderEntry>,
|
||||
|
@ -144,6 +148,7 @@ where
|
|||
game_dir: None,
|
||||
nexus_api_key: None,
|
||||
mod_order: Vec::new(),
|
||||
unsafe_io: false,
|
||||
};
|
||||
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue