diff --git a/crates/ide-host/src/plugins/api.rs b/crates/ide-host/src/plugins/api.rs index 1f04d0bd1..6aed8866a 100644 --- a/crates/ide-host/src/plugins/api.rs +++ b/crates/ide-host/src/plugins/api.rs @@ -180,7 +180,7 @@ impl SecretStore for FileSecretStore { /// Largest response body a plugin receives. const MAX_RESPONSE: usize = 8 << 20; /// Largest stdout / stderr kept from a process. -const MAX_OUTPUT: usize = 4 << 20; +const MAX_OUTPUT: usize = 16 << 20; /// Most processes one plugin keeps running at once. const MAX_PROCESSES: usize = 8; /// Most output one stream of a handle holds until the plugin reads it. @@ -2443,9 +2443,14 @@ impl HostApi for PluginApi { let read = |pipe: Option>| { std::thread::spawn(move || { let mut out = Vec::new(); + let mut overflow = false; if let Some(p) = pipe { let mut p = p; - let _ = p.by_ref().take(MAX_OUTPUT as u64).read_to_end(&mut out); + let _ = p.by_ref().take((MAX_OUTPUT + 1) as u64).read_to_end(&mut out); + if out.len() > MAX_OUTPUT { + overflow = true; + out.truncate(MAX_OUTPUT); + } let _ = std::io::copy(&mut p, &mut std::io::sink()); } let mut text = String::from_utf8_lossy(&out).into_owned(); @@ -2454,7 +2459,7 @@ impl HostApi for PluginApi { end -= 1; } text.truncate(end); - text + (text, overflow) }) }; let stdout = read(child.stdout.take().map(|p| Box::new(p) as Box)); @@ -2474,11 +2479,18 @@ impl HostApi for PluginApi { } std::thread::sleep(Duration::from_millis(10)); }; - Ok(ProcessOutput { - status: status.code(), - stdout: stdout.join().unwrap_or_default(), - stderr: stderr.join().unwrap_or_default(), - }) + let (stdout, stdout_overflow) = stdout.join().unwrap_or_default(); + let (stderr, _) = stderr.join().unwrap_or_default(); + if stdout_overflow { + tracing::warn!( + plugin = %self.id(), + program = %cmd.program, + limit_mib = MAX_OUTPUT >> 20, + "process stdout exceeded limit" + ); + return Err(HostError::Failed(format!("{} stdout exceeded {} MiB limit", cmd.program, MAX_OUTPUT >> 20))); + } + Ok(ProcessOutput { status: status.code(), stdout, stderr }) } fn subscribe(&self, kind: EventKind) { diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index bac7a2496..842849f8c 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -6683,6 +6683,38 @@ fn process_timeout_returns_guest_error_without_disabling_plugin() { assert_eq!(info.stats.crashes, 0); } +#[cfg(unix)] +#[test] +fn process_stdout_accepts_large_payloads_and_reports_overflow_explicitly() { + use std::os::unix::fs::PermissionsExt; + let f = fixture(Limits { timeout: Duration::from_secs(10), ..Limits::default() }); + let package = f._tmp.path().join("api14-large-proc"); + copy_dir(&sample("api14"), &package); + let manifest = package.join("plugin.toml"); + let text = + std::fs::read_to_string(&manifest).unwrap().replace("optional = [", "optional = [\"process:api14-tool\", "); + std::fs::write(manifest, text).unwrap(); + f.install(&package); + + let medium_bytes = 5 * 1024 * 1024; + let medium = f._tmp.path().join("medium-tool"); + std::fs::write(&medium, "#!/bin/sh\ndd if=/dev/zero bs=1048576 count=5 2>/dev/null | tr '\\0' 'x'\n").unwrap(); + std::fs::set_permissions(&medium, std::fs::Permissions::from_mode(0o755)).unwrap(); + let medium = medium.canonicalize().unwrap(); + f.host.choose_program("api14", "process:api14-tool", Some(&f.project), Some(&medium)).unwrap(); + let out = f.run("api14", "process-run", None).unwrap().unwrap(); + assert_eq!(out.len(), medium_bytes); + + let huge = f._tmp.path().join("huge-tool"); + std::fs::write(&huge, "#!/bin/sh\ndd if=/dev/zero bs=1048576 count=17 2>/dev/null | tr '\\0' 'x'\n").unwrap(); + std::fs::set_permissions(&huge, std::fs::Permissions::from_mode(0o755)).unwrap(); + let huge = huge.canonicalize().unwrap(); + f.host.choose_program("api14", "process:api14-tool", Some(&f.project), Some(&huge)).unwrap(); + let err = f.run("api14", "process-run", None).unwrap_err(); + assert!(err.contains("stdout exceeded 16 MiB limit"), "{err}"); + assert_eq!(f.host.info_of("api14").unwrap().stats.crashes, 0); +} + /// A link already in the project is written through, so it is judged by where it points. #[cfg(unix)] #[test] diff --git a/plugins/README.md b/plugins/README.md index dce5b21b1..b85d652c0 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -622,7 +622,7 @@ then run `openidea-plugin check`. | `Project::symbols(query, limit)` | `workspace.symbols` | New in 1.3; `project.read`. Fuzzy in-memory search (no LSP): name, camelCase kind, container and UTF-16 location. Queries ≤ 256 UTF-8 bytes; limits capped at 1000. Snapshot notes below. | | `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. | | `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. | -| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; 4 MiB of output; killed when the call is cancelled or times out. | +| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; up to 16 MiB of stdout (fails if exceeded); killed when the call is cancelled or times out. | | `host::run_command(cmd, project, stdin)` → `{code, stdout, stderr}` | `process.run-command` | API 1.3: same limits as `run_process`, with append-only environment variables; needs the pinned `process:` grant. | | `host::start(cmd, project)` → `Child` | `process.start` | API 1.3: `poll(max_bytes)`, `write(text)`, `close_stdin()`, `wait(timeout_ms)`, `kill()`; at most 8 handles; dropping one kills its process. | | `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. |