fix(plugins): raise process stdout limit to 16 MiB and surface truncation (#2838)

process.run kept at most 4 MiB of stdout and silently dropped the rest while reporting success, so large structured output (kubectl -o json, cargo metadata) reached plugins cut off mid-document. The limit is now 16 MiB, as for file reads, and stdout beyond it fails the call with an explicit error instead of returning truncated output. stderr is still truncated without failing.

Co-authored-by: dengliu <5108773+dengliu@users.noreply.github.com>
This commit is contained in:
Deng Liu
2026-11-12 09:31:52 +08:00
committed by GitHub
co-authored by dengliu
parent a7af19d6b9
commit f4ff2feefa
3 changed files with 53 additions and 9 deletions
+20 -8
View File
@@ -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<Box<dyn Read + Send>>| {
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<dyn Read + Send>));
@@ -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) {
+32
View File
@@ -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]
+1 -1
View File
@@ -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:<program>` 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. |