confium_sandbox_process/
process_sandbox.rs1use std::collections::HashSet;
20use std::io::Read;
21use std::io::Write;
22use std::process::Child;
23use std::process::Command;
24use std::process::Stdio;
25use std::str;
26
27use snafu::Backtrace;
28use snafu::GenerateImplicitData;
29
30use crate::Error;
31use crate::Result;
32use crate::protocol::LEN_PREFIX_BYTES;
33use crate::protocol::MAX_FRAME_BYTES;
34use crate::protocol::Request;
35use crate::protocol::Response;
36use crate::protocol::parse_len;
37use crate::protocol::value_to_json;
38use crate::sandbox::Capability;
39use crate::sandbox::Sandbox;
40use crate::sandbox::SandboxInstance;
41use crate::sandbox::Value;
42
43#[derive(Debug, Default, Clone)]
49pub struct ProcessSandbox;
50
51impl ProcessSandbox {
52 pub fn new() -> Self {
54 Self
55 }
56}
57
58impl Sandbox for ProcessSandbox {
59 fn load_module(&self, bytes: &[u8]) -> Result<Box<dyn SandboxInstance>> {
60 let path = str::from_utf8(bytes).map_err(|e| Error::InvalidPath {
61 source: e,
62 backtrace: Backtrace::generate(),
63 })?;
64 let path = path.trim();
69
70 let mut child = Command::new(path)
71 .stdin(Stdio::piped())
72 .stdout(Stdio::piped())
73 .stderr(Stdio::inherit())
76 .spawn()
77 .map_err(|e| Error::Spawn {
78 source: e,
79 backtrace: Backtrace::generate(),
80 })?;
81
82 let stdin = child.stdin.take().ok_or_else(|| Error::Spawn {
85 source: std::io::Error::other("plugin stdin pipe was not captured"),
86 backtrace: Backtrace::generate(),
87 })?;
88 let stdout = child.stdout.take().ok_or_else(|| Error::Spawn {
89 source: std::io::Error::other("plugin stdout pipe was not captured"),
90 backtrace: Backtrace::generate(),
91 })?;
92
93 Ok(Box::new(ProcessInstance {
94 child: Some(child),
95 stdin,
96 stdout,
97 caps: CapabilitySet::new(),
98 }))
99 }
100
101 fn name(&self) -> &'static str {
102 "process"
103 }
104}
105
106pub struct ProcessInstance {
112 child: Option<Child>,
113 stdin: std::process::ChildStdin,
114 stdout: std::process::ChildStdout,
115 caps: CapabilitySet,
116}
117
118impl Drop for ProcessInstance {
119 fn drop(&mut self) {
120 let _ = self.stdin.flush();
123 if let Some(child) = self.child.as_mut() {
124 let _ = child.kill();
125 let _ = child.wait();
126 }
127 }
128}
129
130impl ProcessInstance {
131 fn round_trip(&mut self, req: &Request) -> Result<Response> {
137 let frame = req.to_frame()?;
138 self.stdin
139 .write_all(&frame)
140 .map_err(|e| Error::WriteRequest {
141 source: e,
142 backtrace: Backtrace::generate(),
143 })?;
144 self.stdin.flush().map_err(|e| Error::WriteRequest {
145 source: e,
146 backtrace: Backtrace::generate(),
147 })?;
148
149 let payload = read_frame(&mut self.stdout)?;
150 Response::from_json_bytes(&payload)
151 }
152}
153
154impl SandboxInstance for ProcessInstance {
155 fn call(&mut self, function: &str, args: &[Value]) -> Result<Vec<Value>> {
156 let json_args: Vec<_> = args.iter().map(value_to_json).collect();
157 let req = Request::new(function, json_args);
158 let resp = self.round_trip(&req)?;
159 resp.into_result(function)
160 }
161
162 fn grant_capability(&mut self, cap: Capability) -> Result<()> {
163 self.caps.grant(cap);
164 Ok(())
165 }
166
167 fn revoke_capability(&mut self, cap: &Capability) -> Result<()> {
168 self.caps.revoke(cap);
169 Ok(())
170 }
171}
172
173#[derive(Debug, Default)]
184struct CapabilitySet {
185 caps: HashSet<Capability>,
186}
187
188impl CapabilitySet {
189 fn new() -> Self {
190 Self::default()
191 }
192
193 fn grant(&mut self, cap: Capability) {
194 self.caps.insert(cap);
195 }
196
197 fn revoke(&mut self, cap: &Capability) {
198 self.caps.remove(cap);
199 }
200
201 #[allow(dead_code)]
202 fn has(&self, cap: &Capability) -> bool {
203 self.caps.contains(cap)
204 }
205}
206
207fn read_frame<R: Read>(reader: &mut R) -> Result<Vec<u8>> {
216 let mut header = [0u8; LEN_PREFIX_BYTES];
217 read_exact_or_eof(reader, &mut header)?;
218 let len = parse_len(&header)?;
219 let mut payload = vec![0u8; len];
221 if len > 0 {
222 read_exact_or_eof(reader, &mut payload)?;
223 }
224 Ok(payload)
225}
226
227fn read_exact_or_eof<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
231 let mut filled = 0;
232 while filled < buf.len() {
233 let n = reader
234 .read(&mut buf[filled..])
235 .map_err(|e| Error::ReadResponse {
236 source: e,
237 backtrace: Backtrace::generate(),
238 })?;
239 if n == 0 {
240 return Err(Error::ReadResponse {
241 source: std::io::Error::new(
242 std::io::ErrorKind::UnexpectedEof,
243 format!(
244 "plugin stdout closed after {filled}/{} bytes of frame",
245 buf.len()
246 ),
247 ),
248 backtrace: Backtrace::generate(),
249 });
250 }
251 filled += n;
252 }
253 let _ = MAX_FRAME_BYTES; Ok(())
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use std::io::Cursor;
261
262 #[test]
263 fn sandbox_name_is_process() {
264 let sb = ProcessSandbox::new();
265 assert_eq!(sb.name(), "process");
266 }
267
268 #[test]
269 fn sandbox_default_works() {
270 let sb = ProcessSandbox;
271 assert_eq!(sb.name(), "process");
272 }
273
274 #[test]
275 fn read_frame_decodes_simple_payload() {
276 let mut bytes = vec![0, 0, 0, 5];
278 bytes.extend_from_slice(b"hello");
279 let mut cur = Cursor::new(bytes);
280 let payload = read_frame(&mut cur).expect("frame reads");
281 assert_eq!(&payload, b"hello");
282 }
283
284 #[test]
285 fn read_frame_empty_payload() {
286 let bytes = vec![0, 0, 0, 0];
287 let mut cur = Cursor::new(bytes);
288 let payload = read_frame(&mut cur).expect("frame reads");
289 assert!(payload.is_empty());
290 }
291
292 #[test]
293 fn read_frame_eof_on_truncated_header() {
294 let bytes = vec![0, 0];
295 let mut cur = Cursor::new(bytes);
296 let err = read_frame(&mut cur).expect_err("must fail");
297 assert_eq!(err.code(), 0x2103);
299 }
300
301 #[test]
302 fn read_frame_eof_on_truncated_payload() {
303 let mut bytes = vec![0, 0, 0, 10];
304 bytes.extend_from_slice(b"short");
305 let mut cur = Cursor::new(bytes);
306 let err = read_frame(&mut cur).expect_err("must fail");
307 assert_eq!(err.code(), 0x2103);
308 }
309
310 #[test]
311 fn capability_set_grant_revoke() {
312 let mut caps = CapabilitySet::new();
313 let cap = Capability::InterfaceAccess {
314 name: "hash".into(),
315 };
316 assert!(!caps.has(&cap));
317 caps.grant(cap.clone());
318 assert!(caps.has(&cap));
319 caps.revoke(&cap);
320 assert!(!caps.has(&cap));
321 }
322}