From d0f0d14c996cee8aa2a3d21a8a22742d4b2749dc Mon Sep 17 00:00:00 2001 From: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:37:34 -0500 Subject: [PATCH] dc-upkeep-7: cap command-file line reads --- apps/deckcraft-cli/src/main.rs | 38 ++++++++++++++++++++++++++---- apps/deckcraft-cli/tests/it/cli.rs | 14 +++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/deckcraft-cli/src/main.rs b/apps/deckcraft-cli/src/main.rs index 76a4c244..be99ee07 100644 --- a/apps/deckcraft-cli/src/main.rs +++ b/apps/deckcraft-cli/src/main.rs @@ -455,6 +455,28 @@ enum CommandSource { File(String), } +const MAX_COMMAND_FILE_LINE_BYTES: usize = 1024 * 1024; + +fn read_command_file_line(reader: &mut impl std::io::BufRead, line: &mut Vec) -> std::io::Result { + line.clear(); + loop { + let available = reader.fill_buf()?; + if available.is_empty() { + return Ok(!line.is_empty()); + } + let newline = available.iter().position(|byte| *byte == b'\n'); + let count = newline.unwrap_or(available.len()); + line.len().checked_add(count).filter(|length| *length <= MAX_COMMAND_FILE_LINE_BYTES).ok_or_else(|| { + std::io::Error::new(std::io::ErrorKind::InvalidData, format!("command line exceeds {MAX_COMMAND_FILE_LINE_BYTES} byte limit")) + })?; + line.extend_from_slice(&available[..count]); + reader.consume(count + usize::from(newline.is_some())); + if newline.is_some() { + return Ok(true); + } + } +} + /// The same command syntax and execution path for --cmd and each command-file line. fn run_command(s: &mut Session, command: &str, print: bool) -> Result<(), String> { let (id, params) = match command.split_once('=') { @@ -469,13 +491,19 @@ fn run_command(s: &mut Session, command: &str, print: bool) -> Result<(), String } fn run_command_file(s: &mut Session, path: &str, print: bool) -> Result<(), String> { - use std::io::{BufRead, BufReader}; + use std::io::BufReader; let file = std::fs::File::open(path).map_err(|e| format!("{path}: {e}"))?; - for (index, line) in BufReader::new(file).lines().enumerate() { - let line_number = index + 1; - let line = line.map_err(|e| format!("{path}:{line_number}: {e}"))?; - let line = if index == 0 { line.trim_start_matches('\u{feff}') } else { &line }; + let mut reader = BufReader::new(file); + let mut bytes = Vec::new(); + let mut line_number = 0usize; + loop { + line_number = line_number.checked_add(1).ok_or_else(|| format!("{path}: too many lines"))?; + if !read_command_file_line(&mut reader, &mut bytes).map_err(|e| format!("{path}:{line_number}: {e}"))? { + break; + } + let line = std::str::from_utf8(&bytes).map_err(|e| format!("{path}:{line_number}: invalid UTF-8: {e}"))?; + let line = if line_number == 1 { line.trim_start_matches('\u{feff}') } else { line }; let command = line.trim(); if command.is_empty() || command.starts_with('#') { continue; diff --git a/apps/deckcraft-cli/tests/it/cli.rs b/apps/deckcraft-cli/tests/it/cli.rs index dabcfc81..fef870a8 100644 --- a/apps/deckcraft-cli/tests/it/cli.rs +++ b/apps/deckcraft-cli/tests/it/cli.rs @@ -142,6 +142,20 @@ fn command_file_read_errors_name_the_path_and_utf8_line() { std::fs::remove_dir_all(dir).unwrap(); } +#[test] +fn command_file_rejects_an_oversized_line_before_running_it() { + let dir = command_file_dir("line-limit"); + let file = dir.join("commands.txt"); + let command = format!("slide.new={{\"title\":\"{}\"}}", "x".repeat(1024 * 1024)); + std::fs::write(&file, format!("{command}\nslide.new\n")).unwrap(); + + let o = cli(&["run", "--print", "--cmd-file", &file.to_string_lossy()]); + let err = stderr(&o); + assert!(err.contains(&format!("{}:1:", file.display())) && err.contains("command line exceeds 1048576 byte limit"), "{err}"); + assert!(o.stdout.is_empty(), "the oversized command must not run"); + std::fs::remove_dir_all(dir).unwrap(); +} + #[test] fn inline_commands_keep_their_existing_syntax_and_errors() { let o = cli(&["run", "--cmd", "slide.new", "--cmd", "slide.new={\"title\":\"Inline = #\"}", "--cmd", "document.inspect", "--print"]);