assay/lua/builtins/
readonly.rs1use mlua::{Lua, MultiValue, Table, Value};
10
11const BLOCKED_TABLES: &[&str] = &["shell", "process", "machinectl"];
13
14const BLOCKED_FUNCTIONS: &[&str] = &[
16 "http.post",
17 "http.put",
18 "http.patch",
19 "http.delete",
20 "http.serve",
21 "http.serve_with_extra",
22 "http.download",
23 "ws.connect",
24 "fs.write",
25 "fs.write_bytes",
26 "fs.remove",
27 "fs.rename",
28 "fs.copy",
29 "fs.chmod",
30 "fs.mkdir",
31 "fs.tempdir",
32 "fs.sub_in_file",
33 "env.set",
34 "db.execute",
35 "oci.copy",
36 "oci.tag",
37 "oci.mutate",
38 "systemd.start",
39 "systemd.stop",
40 "systemd.restart",
41 "systemd.reload",
42 "systemd.unit_action",
43 "systemd.machine_start",
44 "systemd.machine_poweroff",
45 "systemd.machine_reboot",
46 "systemd.machine_terminate",
47 "systemd.machine_exec",
48 "apt.update",
49 "apt.install",
50 "apt.remove",
51 "apt.add_source",
52 "compress.untar",
53 "tar.create",
54 "tar.extract",
55 "io.popen",
56];
57
58pub fn apply(lua: &Lua) -> mlua::Result<()> {
59 for path in BLOCKED_FUNCTIONS {
60 block_function(lua, path)?;
61 }
62 for name in BLOCKED_TABLES {
63 block_table(lua, name)?;
64 }
65 guard_http_client_request(lua)?;
66 guard_io_open(lua)?;
67 guard_io_output(lua)?;
68 Ok(())
69}
70
71fn blocked(name: &str) -> mlua::Error {
72 mlua::Error::runtime(format!(
73 "readonly: {name} blocked (write operations are disabled in read-only mode)"
74 ))
75}
76
77fn blocked_stub(lua: &Lua, name: &str) -> mlua::Result<mlua::Function> {
78 let name = name.to_string();
79 lua.create_function(move |_, _args: MultiValue| -> mlua::Result<Value> { Err(blocked(&name)) })
80}
81
82fn block_function(lua: &Lua, path: &str) -> mlua::Result<()> {
86 let Some((table_name, fn_name)) = path.split_once('.') else {
87 return Ok(());
88 };
89 let Some(table) = lua.globals().get::<Option<Table>>(table_name)? else {
90 return Ok(());
91 };
92 if table.get::<Value>(fn_name)?.is_function() {
93 table.set(fn_name, blocked_stub(lua, path)?)?;
94 }
95 Ok(())
96}
97
98fn block_table(lua: &Lua, name: &str) -> mlua::Result<()> {
100 let Some(table) = lua.globals().get::<Option<Table>>(name)? else {
101 return Ok(());
102 };
103 for pair in table.clone().pairs::<Value, Value>() {
104 let (key, value) = pair?;
105 if let (Value::String(key_str), true) = (&key, value.is_function()) {
106 let label = format!("{name}.{}", key_str.to_str()?);
107 table.set(key, blocked_stub(lua, &label)?)?;
108 }
109 }
110 Ok(())
111}
112
113fn guard_http_client_request(lua: &Lua) -> mlua::Result<()> {
118 let Some(http) = lua.globals().get::<Option<Table>>("http")? else {
119 return Ok(());
120 };
121 let Some(inner) = http.get::<Option<mlua::Function>>("_client_request")? else {
122 return Ok(());
123 };
124 let wrapper = lua.create_async_function(move |_, args: MultiValue| {
125 let inner = inner.clone();
126 async move {
127 let method = match args.iter().nth(1) {
128 Some(Value::String(s)) => Some(s.to_str()?.to_string()),
129 _ => None,
130 };
131 if let Some(method) = method
132 && method != "get"
133 {
134 return Err(blocked(&format!("http.{method}")));
135 }
136 inner.call_async::<MultiValue>(args).await
137 }
138 })?;
139 http.set("_client_request", wrapper)?;
140 Ok(())
141}
142
143fn guard_io_open(lua: &Lua) -> mlua::Result<()> {
145 let Some(io_table) = lua.globals().get::<Option<Table>>("io")? else {
146 return Ok(());
147 };
148 let Some(inner) = io_table.get::<Option<mlua::Function>>("open")? else {
149 return Ok(());
150 };
151 let wrapper = lua.create_function(move |_, args: MultiValue| {
152 let mode = match args.iter().nth(1) {
153 Some(Value::String(s)) => s.to_str()?.to_string(),
154 _ => "r".to_string(),
155 };
156 if mode.contains('w') || mode.contains('a') || mode.contains('+') {
157 return Err(blocked("io.open"));
158 }
159 inner.call::<MultiValue>(args)
160 })?;
161 io_table.set("open", wrapper)?;
162 Ok(())
163}
164
165fn guard_io_output(lua: &Lua) -> mlua::Result<()> {
168 let Some(io_table) = lua.globals().get::<Option<Table>>("io")? else {
169 return Ok(());
170 };
171 let Some(inner) = io_table.get::<Option<mlua::Function>>("output")? else {
172 return Ok(());
173 };
174 let wrapper = lua.create_function(move |_, args: MultiValue| {
175 if !args.is_empty() {
176 return Err(blocked("io.output"));
177 }
178 inner.call::<MultiValue>(args)
179 })?;
180 io_table.set("output", wrapper)?;
181 Ok(())
182}