mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-13 14:59:06 +00: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:
|
||||
CARGO_TERM_COLOR: always
|
||||
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 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).
|
||||
|
||||
@@ -409,6 +409,13 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result<Value> {
|
||||
let mut sent = false;
|
||||
let mut spool = Value::Null;
|
||||
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)?);
|
||||
sent = true;
|
||||
}
|
||||
@@ -429,6 +436,20 @@ fn remember_print(s: &mut Session, p: &Value) {
|
||||
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> {
|
||||
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"])
|
||||
.env(CHILD, 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()
|
||||
.unwrap();
|
||||
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");
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
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();
|
||||
s.execute("file.new", json!({"width": 24, "height": 16})).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!({"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();
|
||||
for spec in command_specs() {
|
||||
for spec in command_specs().iter().filter(|spec| !EXTERNAL.contains(&spec.id)) {
|
||||
for p in ¶ms {
|
||||
if let Err(why) = run_guarded(spec.id, p) {
|
||||
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 |
|
||||
|---|---|
|
||||
| `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)) |
|
||||
| `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 |
|
||||
|
||||
Reference in new issue
Block a user