Skip to content

fix(server): allow newlines in exec command arguments#1965

Open
zanetworker wants to merge 3 commits into
NVIDIA:mainfrom
zanetworker:fix/exec-multiline-scripts
Open

fix(server): allow newlines in exec command arguments#1965
zanetworker wants to merge 3 commits into
NVIDIA:mainfrom
zanetworker:fix/exec-multiline-scripts

Conversation

@zanetworker

Copy link
Copy Markdown
Contributor

Summary

Allow newline characters in sandbox exec command arguments so multi-line scripts (python3 -c "def f():\n ...", bash -c "echo 'line1\nline2'") work without base64 workarounds.

Related Issue

Fixes #1963

Changes

  • shell_escape() (sandbox.rs): Remove newline rejection. Newlines inside POSIX single quotes are literal characters, not command separators. Null byte rejection preserved.
  • validate_exec_request_fields() (validation.rs): Add reject_null_bytes() for exec command arguments instead of reject_control_chars(). Newline rejection preserved for workdir and environment values.

Testing

Verified end-to-end against a live gateway with podman driver on macOS:

$ openshell sandbox exec -n multiline-test -- python3 -c "
def hello():
    print('multi-line scripts work!')
hello()
"
multi-line scripts work!
  • cargo test --package openshell-server — 857 tests pass, 0 failures
  • Multi-line Python script via sandbox exec — works
  • Multi-line bash script via sandbox exec — works
  • Single-line commands — no regression
  • Null bytes in args — still rejected
  • Newlines in workdir — still rejected
  • Newlines in env values — still rejected

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (not applicable — no architectural change)

Single-quoted POSIX strings treat newlines as literal characters, not
command separators. The newline rejection prevented multi-line scripts
passed via sandbox exec (e.g. python3 -c with a multi-line body).

Null byte rejection is preserved since null bytes truncate C-level
shell parsing.

Closes NVIDIA#1963

Signed-off-by: Adel Zaalouk <azaalouk@redhat.com>
Add reject_null_bytes() for exec command argument validation, replacing
reject_control_chars() which also rejected newlines. Multi-line scripts
passed to bash -c or python3 -c are legitimate and safe because
shell_escape() wraps them in POSIX single quotes where newlines are
literal.

Newline rejection is preserved for workdir (no legitimate path contains
newlines) and environment values.

Closes NVIDIA#1963

Signed-off-by: Adel Zaalouk <azaalouk@redhat.com>
@copy-pr-bot

copy-pr-bot Bot commented Jun 21, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Comment thread crates/openshell-server/src/grpc/validation.rs
Split reject_control_chars into reject_null_char and reject_newline_chars
so callers can compose the checks they need. reject_control_chars now
delegates to both. Exec command arguments call reject_null_char directly.

Addresses review feedback from @elezar on PR NVIDIA#1965.

Signed-off-by: Adel Zaalouk <azaalouk@redhat.com>
command: vec![
"python3".to_string(),
"-c".to_string(),
"def f():\n return 1\nprint(f())".to_string(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should include both CR/LF and a single quote in the same command argument. The risky interaction here is not just that newlines are accepted, but that multiline script text still composes correctly with POSIX single-quote escaping.

For example, something like print('one')\r\nprint('two') would give us regression coverage for the exact case this helper has to keep safe.

};
assert!(build_remote_exec_command(&req).is_err());
// Validation layer rejects newlines in workdir
assert!(validate_exec_request_fields(&req).is_err());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This test name still says build_remote_exec_command, but the assertion now exercises validate_exec_request_fields(). Could we rename it to validate_exec_rejects_newlines_in_workdir, or drop it since validation.rs already has this coverage?

)));
}
reject_control_chars(arg, &format!("command argument {i}"))?;
reject_null_char(arg, &format!("command argument {i}"))?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: The function comment above still says this validates control characters generally, but command args now intentionally allow CR/LF and only reject NUL. Maybe reword it to something like: Validate exec request size limits and field-specific character constraints.

if value.bytes().any(|b| b == b'\n' || b == b'\r') {
return Err("value contains newline or carriage return".to_string());
}
if value.is_empty() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this helper now allows \n/\r, the downstream command_preview fields in the exec logging paths should not log the assembled command raw. In line-oriented log sinks, user-controlled CR/LF can split records or create forged-looking continuation lines. Can we escape the preview before logging, and reuse the same helper for both non-interactive and interactive exec?

let command_preview: String = command
    .chars()
    .take(120)
    .flat_map(char::escape_default)
    .collect();

@elezar

elezar commented Jun 23, 2026

Copy link
Copy Markdown
Member

nit: crates/openshell-server/src/grpc/validation.rs:57 still calls reject_control_chars() for exec environment values, but validate_exec_env_entries() below routes through validate_env_entries_inner() and checks env values again. Keeping the length check in the direct loop makes sense, but the control-char check could be left to the env-entry validator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sandbox exec rejects multi-line scripts passed to bash -c / python3 -c

2 participants