Merge pull request #155 from EmDecay/fix/147-input-channel-caps
fix: cap control requests and refuse file reads that never end
This commit is contained in:
15 files changed
+251
-55
No files matched your search
@@ -51,11 +51,7 @@ fn path_arg<'a>(p: &'a Value, cmd: &str) -> Result<&'a str> {
|
||||
|
||||
/// Read a STEP file as an Import feature.
|
||||
fn read_step(path: &str) -> Result<solvecraft_io::StepFeature> {
|
||||
let meta = solvecraft_io::vfs::len(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
if meta > solvecraft_io::MAX_STEP_BYTES as u64 {
|
||||
return Err(EngineError::Other(format!("{path}: file too large")));
|
||||
}
|
||||
let bytes = solvecraft_io::vfs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let bytes = solvecraft_io::vfs::read(path, solvecraft_io::MAX_STEP_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
solvecraft_io::step_import_feature(&bytes, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
@@ -80,11 +76,7 @@ fn add_step(s: &mut Session, path: &str, name: Option<&str>) -> Result<Value> {
|
||||
|
||||
/// Add a MeshImport feature for a 3MF or STL file.
|
||||
fn add_mesh(s: &mut Session, path: &str, name: Option<&str>) -> Result<Value> {
|
||||
let meta = solvecraft_io::vfs::len(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
if meta > solvecraft_io::MAX_3MF_BYTES as u64 {
|
||||
return Err(EngineError::Other(format!("{path}: file too large")));
|
||||
}
|
||||
let bytes = solvecraft_io::vfs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let bytes = solvecraft_io::vfs::read(path, solvecraft_io::MAX_3MF_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let mf = solvecraft_io::mesh_import_feature(&bytes, path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let fname = name.filter(|n| !n.trim().is_empty()).unwrap_or(&mf.name).to_string();
|
||||
let id = s.doc_mut().add_feature(mf.kind, Some(&fname))?;
|
||||
@@ -132,11 +124,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let errors = s.model.results.iter().filter(|r| r.error.is_some()).count();
|
||||
return Ok(json!({"path": path, "features": s.doc.features.len(), "errors": errors, "bodies": r["bodies"], "warnings": r["warnings"]}));
|
||||
}
|
||||
let meta = solvecraft_io::vfs::len(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
if meta as usize > solvecraft_io::MAX_DESIGN_BYTES {
|
||||
return Err(EngineError::Other(format!("{path}: file too large")));
|
||||
}
|
||||
let bytes = solvecraft_io::vfs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let bytes = solvecraft_io::vfs::read(path, solvecraft_io::MAX_DESIGN_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let doc = solvecraft_io::read_design(&bytes)?;
|
||||
*s = Session::new(doc);
|
||||
s.path = Some(path.to_string());
|
||||
|
||||
@@ -226,16 +226,15 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"format": format, "path": path, "text": text}))
|
||||
}
|
||||
|
||||
/// Largest parameter table we read from a file.
|
||||
const MAX_PARAMETER_BYTES: usize = 16 << 20;
|
||||
|
||||
fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "parameters.import";
|
||||
let text = match (str_(p, "text"), str_(p, "path")) {
|
||||
(Some(t), _) => t.to_string(),
|
||||
(None, Some(path)) => {
|
||||
let meta = solvecraft_io::vfs::len(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
if meta > 16 << 20 {
|
||||
return Err(bad(cmd, "file too large"));
|
||||
}
|
||||
solvecraft_io::vfs::read_to_string(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
|
||||
solvecraft_io::vfs::read_to_string(path, MAX_PARAMETER_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
|
||||
}
|
||||
_ => return Err(bad(cmd, "give `text` or `path`")),
|
||||
};
|
||||
|
||||
@@ -31,11 +31,7 @@ fn image_bytes(p: &Value, cmd: &str) -> Result<Vec<u8>> {
|
||||
base64_decode(d).ok_or_else(|| bad(cmd, "`data` is not base64"))?
|
||||
} else {
|
||||
let path = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "give `path` or `data`"))?;
|
||||
let meta = solvecraft_io::vfs::len(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
if meta as usize > MAX_CANVAS_BYTES {
|
||||
return Err(bad(cmd, "the image is too large"));
|
||||
}
|
||||
solvecraft_io::vfs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
|
||||
solvecraft_io::vfs::read(path, MAX_CANVAS_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?
|
||||
};
|
||||
if b.len() > MAX_CANVAS_BYTES {
|
||||
return Err(bad(cmd, "the image is too large"));
|
||||
|
||||
@@ -34,11 +34,7 @@ fn source_text(p: &Value, cmd: &str) -> Result<String> {
|
||||
return Ok(t.to_string());
|
||||
}
|
||||
let path = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "give `path` or `text`"))?;
|
||||
let meta = solvecraft_io::vfs::len(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
if meta as usize > MAX_DRAWING_BYTES {
|
||||
return Err(EngineError::Other(format!("{path}: file too large")));
|
||||
}
|
||||
let bytes = solvecraft_io::vfs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
let bytes = solvecraft_io::vfs::read(path, MAX_DRAWING_BYTES).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
Ok(String::from_utf8_lossy(&bytes).to_string())
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ pub use solvecraft_kernel as kernel;
|
||||
pub use solvecraft_render as render;
|
||||
pub use solvecraft_sketch as sketch;
|
||||
|
||||
/// Longest accepted request line on the JSON-lines channels (MCP over stdio and the control
|
||||
/// server over loopback), so both refuse runaway input at the same size.
|
||||
pub const MAX_LINE: usize = 16 << 20;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EngineError {
|
||||
#[error("unknown command `{0}`")]
|
||||
|
||||
@@ -100,7 +100,9 @@ pub fn load(dir: &Path, id: &str) -> Result<(Document, Entry)> {
|
||||
.ok()
|
||||
.and_then(|b| serde_json::from_slice(&b).ok())
|
||||
.ok_or_else(|| EngineError::Other(format!("no recovered design `{id}`")))?;
|
||||
let bytes = fs::read(file(dir, id, "solvecraft")).map_err(|e| EngineError::Other(format!("recovered design `{id}`: {e}")))?;
|
||||
let path = file(dir, id, "solvecraft");
|
||||
let bytes = solvecraft_io::vfs::read(&path.to_string_lossy(), solvecraft_io::MAX_DESIGN_BYTES)
|
||||
.map_err(|e| EngineError::Other(format!("recovered design `{id}`: {e}")))?;
|
||||
Ok((solvecraft_io::read_design(&bytes)?, entry))
|
||||
}
|
||||
|
||||
|
||||
@@ -2299,3 +2299,29 @@ fn adopting_a_computed_component_keeps_its_warm_world_geometry() {
|
||||
s.invalidate_view(100);
|
||||
assert!(std::sync::Arc::ptr_eq(&prepared, &s.world_state()));
|
||||
}
|
||||
|
||||
/// Every file command checked the size with the metadata and then read to the end. A character
|
||||
/// device reports a length of 0, so the cap passed and the read never finished; a FIFO blocked
|
||||
/// the calling thread, which over the control channel is the thread drawing the window.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn special_files_are_refused_by_every_file_command() {
|
||||
let mut s = Session::default();
|
||||
for cmd in ["doc.open", "parameters.import"] {
|
||||
let r = s.execute(cmd, &json!({"path": "/dev/zero"}));
|
||||
assert!(r.as_ref().is_err_and(|e| e.to_string().contains("not a regular file")), "{cmd}: {r:?}");
|
||||
}
|
||||
// The commands that check the extension first need a special file with the right name.
|
||||
let dir = std::env::temp_dir().join(format!("solvecraft-special-{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
for (cmd, name) in [("file.insert_step", "x.step"), ("file.insert_mesh", "x.3mf"), ("sketch.insert_dxf", "x.dxf")] {
|
||||
let p = dir.join(name);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
if !std::process::Command::new("mkfifo").arg(&p).status().map(|st| st.success()).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let r = s.execute(cmd, &json!({"path": p.to_string_lossy()}));
|
||||
assert!(r.as_ref().is_err_and(|e| e.to_string().contains("not a regular file")), "{cmd}: {r:?}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in new issue
Block a user