mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
panic_hunt: don't fuzz the print commands into a real printer (#4467)
* panic_hunt: don't fuzz the print commands into a real printer file.print and file.printOneCopy spool to CUPS, so every run of the adversarial-params test printed a blank page on the default printer. The hunt now skips those two ids (their params are validated like everyone else's; spooling is not what the test is for). * Guard the print spooler itself and the CI environment - print_cmds: disable_printing() (a process-wide flag the panic hunt sets) and the PHOTOCRAFT_NO_PRINT environment variable make spool_pdf refuse before running lp; dryRun still renders and reports the command. Test. - CI: PHOTOCRAFT_NO_PRINT=1 and LPDEST=photocraft-no-such-printer, so a regression fails loudly instead of printing. Documented in docs/development.md › Environment variables.
This commit is contained in:
1 parent
15aac8d7b6
commit
70e182b08f
5 files changed
+51
-1
No files matched your search
@@ -17,6 +17,10 @@ on:
|
|||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
|
# No test or fuzz run may print a page: the engine refuses to spool, and if something ever
|
||||||
|
# bypasses that, lp fails loudly on a printer that does not exist.
|
||||||
|
PHOTOCRAFT_NO_PRINT: "1"
|
||||||
|
LPDEST: photocraft-no-such-printer
|
||||||
# Debug and test builds keep line tables (file:line in panic backtraces) but drop the rest of the
|
# Debug and test builds keep line tables (file:line in panic backtraces) but drop the rest of the
|
||||||
# debug info: faster codegen and links (Windows PDBs most of all), and a target dir small enough
|
# debug info: faster codegen and links (Windows PDBs most of all), and a target dir small enough
|
||||||
# for the Linux job's second, craft-fonts build (it ran out of disk with full debug info).
|
# for the Linux job's second, craft-fonts build (it ran out of disk with full debug info).
|
||||||
|
|||||||
@@ -409,6 +409,13 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result<Value> {
|
|||||||
let mut sent = false;
|
let mut sent = false;
|
||||||
let mut spool = Value::Null;
|
let mut spool = Value::Null;
|
||||||
if send && !dry {
|
if send && !dry {
|
||||||
|
// The last line of defence: a test or fuzz run must never print a page (`dryRun` still
|
||||||
|
// renders and reports the command).
|
||||||
|
if printing_disabled() {
|
||||||
|
return Err(other(
|
||||||
|
"printing is disabled in this process (photocraft_engine::print_cmds::disable_printing or PHOTOCRAFT_NO_PRINT); use dryRun: true",
|
||||||
|
));
|
||||||
|
}
|
||||||
spool = json!(craft_print::submit(&pdf, &job).map_err(other)?);
|
spool = json!(craft_print::submit(&pdf, &job).map_err(other)?);
|
||||||
sent = true;
|
sent = true;
|
||||||
}
|
}
|
||||||
@@ -429,6 +436,20 @@ fn remember_print(s: &mut Session, p: &Value) {
|
|||||||
s.file_menu.last_print = Some(remembered);
|
s.file_menu.last_print = Some(remembered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set once by test harnesses (the panic hunt) so no command in the process can reach a printer.
|
||||||
|
static PRINTING_DISABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// Refuse to spool anything from this process from now on (tests, fuzzing). The
|
||||||
|
/// `PHOTOCRAFT_NO_PRINT` environment variable does the same for a whole shell (CI).
|
||||||
|
pub fn disable_printing() {
|
||||||
|
PRINTING_DISABLED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether spooling is blocked by [`disable_printing`] or `PHOTOCRAFT_NO_PRINT`.
|
||||||
|
pub fn printing_disabled() -> bool {
|
||||||
|
PRINTING_DISABLED.load(std::sync::atomic::Ordering::Relaxed) || std::env::var_os("PHOTOCRAFT_NO_PRINT").is_some_and(|v| !v.is_empty() && v != "0")
|
||||||
|
}
|
||||||
|
|
||||||
fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
fn print(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
do_print(s, p, "file.print")
|
do_print(s, p, "file.print")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,8 +345,26 @@ fn spool_transport_streams_pdf_and_propagates_refusal() {
|
|||||||
.args(["--exact", "print_cmds::tests::spool_transport_streams_pdf_and_propagates_refusal", "--nocapture"])
|
.args(["--exact", "print_cmds::tests::spool_transport_streams_pdf_and_propagates_refusal", "--nocapture"])
|
||||||
.env(CHILD, dir.path())
|
.env(CHILD, dir.path())
|
||||||
.env("PATH", dir.path())
|
.env("PATH", dir.path())
|
||||||
|
// The child spools into the fake `lp` above, so a shell that blocks real printing
|
||||||
|
// (`PHOTOCRAFT_NO_PRINT`, see `disabled_printing_never_spools`) must not stop it.
|
||||||
|
.env_remove("PHOTOCRAFT_NO_PRINT")
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(result.status.success(), "{}\n{}", String::from_utf8_lossy(&result.stdout), String::from_utf8_lossy(&result.stderr));
|
assert!(result.status.success(), "{}\n{}", String::from_utf8_lossy(&result.stdout), String::from_utf8_lossy(&result.stderr));
|
||||||
assert!(String::from_utf8_lossy(&result.stdout).contains("1 passed"), "child must select the transport test");
|
assert!(String::from_utf8_lossy(&result.stdout).contains("1 passed"), "child must select the transport test");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_printing_never_spools() {
|
||||||
|
let mut s = Session::new();
|
||||||
|
s.execute("file.new", json!({"width": 8, "height": 8})).unwrap();
|
||||||
|
super::disable_printing();
|
||||||
|
assert!(super::printing_disabled());
|
||||||
|
let e = s.execute("file.print", json!({})).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("printing is disabled"), "{e}");
|
||||||
|
assert!(s.execute("file.printOneCopy", json!({})).is_err());
|
||||||
|
// A dry run still renders and reports the command it would have run.
|
||||||
|
let r = s.execute("file.print", json!({"dryRun": true})).unwrap();
|
||||||
|
assert_eq!(r["sent"], false);
|
||||||
|
assert!(r["command"].is_array(), "{r}");
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ use std::sync::mpsc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
fn fresh() -> Session {
|
fn fresh() -> Session {
|
||||||
|
// Belt and braces with the EXTERNAL skip below: nothing in this process may reach a printer.
|
||||||
|
photocraft_engine::print_cmds::disable_printing();
|
||||||
let mut s = Session::new();
|
let mut s = Session::new();
|
||||||
s.execute("file.new", json!({"width": 24, "height": 16})).unwrap();
|
s.execute("file.new", json!({"width": 24, "height": 16})).unwrap();
|
||||||
s.execute("layer.new.layer", json!({})).unwrap();
|
s.execute("layer.new.layer", json!({})).unwrap();
|
||||||
@@ -58,8 +60,12 @@ fn no_command_panics_or_hangs_on_adversarial_params() {
|
|||||||
json!({"x": 1e5, "y": -1e5, "radius": 300, "amount": 300, "angle": 1e5, "opacity": -100, "scale": -5, "gamma": -1}),
|
json!({"x": 1e5, "y": -1e5, "radius": 300, "amount": 300, "angle": 1e5, "opacity": -100, "scale": -5, "gamma": -1}),
|
||||||
json!({"points": [], "from": [0, 0], "to": [0, 0], "stops": [], "matrix": [0, 0, 0, 0, 0, 0], "colors": [], "name": "", "mode": "", "style": ""}),
|
json!({"points": [], "from": [0, 0], "to": [0, 0], "stops": [], "matrix": [0, 0, 0, 0, 0, 0], "colors": [], "name": "", "mode": "", "style": ""}),
|
||||||
];
|
];
|
||||||
|
// Commands whose success reaches outside the process: `file.print` and `file.printOneCopy`
|
||||||
|
// spool to CUPS, so a fuzz run with `{}` printed a blank page on the default printer. Their
|
||||||
|
// params are validated like everyone else's; the spooling itself is not fuzzed.
|
||||||
|
const EXTERNAL: &[&str] = &["file.print", "file.printOneCopy"];
|
||||||
let mut bad: Vec<String> = Vec::new();
|
let mut bad: Vec<String> = Vec::new();
|
||||||
for spec in command_specs() {
|
for spec in command_specs().iter().filter(|spec| !EXTERNAL.contains(&spec.id)) {
|
||||||
for p in ¶ms {
|
for p in ¶ms {
|
||||||
if let Err(why) = run_guarded(spec.id, p) {
|
if let Err(why) = run_guarded(spec.id, p) {
|
||||||
bad.push(format!("{} [{why}] <- {p}", spec.id));
|
bad.push(format!("{} [{why}] <- {p}", spec.id));
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ By default PhotoCraft's own crates log at `info` and everything else at `warn`.
|
|||||||
| Variable | Effect |
|
| Variable | Effect |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `PHOTOCRAFT_CONTROL_PORT` | Same as `--control <port>` |
|
| `PHOTOCRAFT_CONTROL_PORT` | Same as `--control <port>` |
|
||||||
|
| `PHOTOCRAFT_NO_PRINT` | Set to anything but `0`: `file.print` / `file.printOneCopy` refuse to spool (`dryRun` still works). CI sets it, with `LPDEST=photocraft-no-such-printer` as a second net; set both when running the panic hunt or fuzzers locally |
|
||||||
| `RUST_LOG` | Log levels for standard error and the log file (see [Logs](#logs)) |
|
| `RUST_LOG` | Log levels for standard error and the log file (see [Logs](#logs)) |
|
||||||
| `PHOTOCRAFT_CONTROL_TOKEN` | 64-hex bearer token for control TCP (avoid on shared systems where environment inspection is possible) |
|
| `PHOTOCRAFT_CONTROL_TOKEN` | 64-hex bearer token for control TCP (avoid on shared systems where environment inspection is possible) |
|
||||||
| `PHOTOCRAFT_CONTROL_TOKEN_FILE` | Read, or create for a server, the control bearer-token file |
|
| `PHOTOCRAFT_CONTROL_TOKEN_FILE` | Read, or create for a server, the control bearer-token file |
|
||||||
|
|||||||
Reference in new issue
Block a user