Skip to content

Security: heibench/slicelab

Security

SECURITY.md

Security Policy

Supported versions

Only the latest release is supported. That is 0.0.1, which claims the name and implements which and presets; nothing before it was published. resolve is on main and unreleased.

Reporting a vulnerability

Do not open a public issue for security vulnerabilities.

Report them through GitHub private security advisories.

Include what you did, what happened, what you expected, and the versions of this package and of the slicer involved.

Current attack surface

Three verbs are implemented: slicelab which, slicelab presets and slicelab resolve. Together they:

  • spawn slicer processes, always as an argv list, never through a shell, and always with stdin connected to /dev/null
  • read files: candidate executables (to digest them), Flatpak deployment directories, and — for presets — whatever profile bundle the engine reads under the --datadir you name
  • perform no network access. which and presets read no configuration file of their own; resolve reads the slice.toml you name and nothing else

resolve and credentials. resolve asks the engine to dump its resolved configuration and keeps that dump beside your intent, because it is the evidence the run happened as described. On PrusaSlicer 2.9.6 that dump carries print_host, printhost_apikey, printhost_cafile, printhost_password, printhost_port and printhost_user in cleartext whenever a loaded preset sets them. So:

  • the engine writes into a temporary directory slicelab creates and destroys, and is never granted the path you named. The file you keep is written only by slicelab, only after those keys have been replaced with <redacted>, and only for a run that produced a configuration to redact
  • what was removed is listed in the report by name. The file is the engine's output minus a named list, which is a statable thing; "complete" would be false
  • an engine whose credential-bearing keys have not been measured is refused rather than guessed for. Writing the dump would be the one mistake here that puts a secret in a file someone commits, so the unmeasured case declines
  • the keys were established by enumerating the family and setting each to a marker, not by reading a stock dump. A stock preset sets no digest credentials, so a measurement taken against one finds three of the six and looks complete

Values you write in [set] reach the engine as command-line arguments, and a process's argv is readable by other users on the same machine. On 2.9.6 no credential-bearing option exists — there is no --printhost-apikey — so nothing authorable through [set] is a secret today. If a future engine adds one, this is the boundary that changes.

What gets written, and where. slicelab writes scratch directories under ~/.cache/slicelab/engine-cwd, or under $XDG_CACHE_HOME when that is set to an absolute path inside your home directory — a value outside it is ignored, for the reason below, so a host with XDG_CACHE_HOME=/var/cache/me still gets ~/.cache. One directory is made per engine launch and removed after it; the two parents are created once and persist.

If neither can be created, slicelab tries the home directory itself, and only if that fails does it fall back to $TMPDIR — which under a Flatpak means the engine runs in $HOME after all. That last rung is a real degradation rather than a guarantee, and it is stated here as one. It is reached only when nothing inside the home directory can hold a directory, and on such a host Flatpak itself fails first: every attempt to reach it with a working engine gave exit 4 before slicelab got that far.

The engines it starts write more. OrcaSlicer drops a result.json on any run, including a successful which probe, and a 00000.log on a failing one. Both land in the engine's process working directory, which is why slicelab gives it one it owns rather than letting it inherit yours.

The scratch directory cannot be in /tmp, and that is a security-relevant detail rather than a tidiness one. A Flatpak sandbox has its own /tmp, so a host /tmp path cannot be translated into it — and bwrap does not refuse the request, it drops it and starts the engine in $HOME. Measured by writing a file from inside both sandboxes and looking for it from the host: a cwd under ~/.cache is honoured and the file appears there; a /tmp cwd puts the process in /home/cam instead.

The first attempt at this fix did exactly that — it stopped the litter landing in the directory you were standing in and started it landing in your home directory instead, where it persists and where nobody is looking. The second attempt fell back to $TMPDIR on any OSError, so a single stray file at ~/.cache/slicelab put the whole defect back at exit 0 on a host that was otherwise healthy. Every fallback above the last rung is now inside the home directory — the last rung is still $TMPDIR, and is described above as the degradation it is. A relative XDG_CACHE_HOME is ignored rather than resolved against your working directory, which is what the basedir spec requires and what stopped slicelab creating ./mycache/slicelab/engine-cwd where you were standing; a relative HOME did the same thing through the other variable, because Path.home() hands back $HOME verbatim.

Two more routes in were found after that, both of them absolute paths: an XDG_CACHE_HOME outside the home directory, and a symlink inside the home directory pointing out of it. Neither is exotic — /var/cache/$USER is an ordinary setting — and both put result.json back in $HOME at exit 0. A candidate now qualifies only if it is under the home directory after resolution, and the home directory must itself be absolute before it is resolved, because Path.home() hands back $HOME verbatim and resolving a relative one would anchor it to your working directory.

Until 0.0.1 none of this was true. slicelab which orcaslicer, run in an empty directory, left a 180-byte result.json behind at exit 0 — and this section said the tool wrote nothing. An identical file reached this repository that way and was committed for five commits before anyone noticed.

resolve reads a slice.toml and writes a readback; slice reads a slice.toml and the mesh it names, and writes both a readback and a G-code file; which and presets read no file you name. None of the four writes a lock.

slice writes to two paths you chose, so it refuses before running the engine if the G-code collides with the readback, with the mesh, or with the intent file -- one run writing the artifact and the readback to one path was measured to report success over a file holding the wrong one. The engine writes only into a scratch directory slicelab owns; both of your paths are written by slicelab, atomically, replacing rather than truncating.

The two are not written under the same rule, and the difference matters if you are relying on one. The G-code is written only for an outcome slicelab stands behind — sliced, or empty where there was nothing to verify. The readback is written on any outcome that was adjudicated at all, incomplete included (D31): it is the evidence for the verdict, and a verdict whose evidence was withheld because the verdict was bad is not worth having.

A run refused before adjudication writes neither — the gate cases, an argv the engine rejects, a destination collision. So the readback beside your intent may describe an earlier run, and slicelab does not say so: it is written when there was a verdict to evidence, left alone when there was not, and its age is not reported either way.

A scratch directory outlives the run only when it holds an artifact slicelab produced and would not hand over, and the report names it when it does.

Inputs. Their own argv — including a --datadir path, which is resolved to an absolute path and handed to the engine, never interpreted by slicelab — plus, for resolve and slice, the slice.toml you name; for slice, the mesh that intent names, which is passed to the engine as a path and never parsed by slicelab; and whatever the engines print. The intent file is parsed as TOML and checked for shape. An unrecognised key under [set] is not refused before the run: it becomes a --key=value argument and the engine rejects it, which slicelab reports as incomplete having handed nothing over. The check that a key means what you think it means is the readback adjudication, and it happens after the engine has answered, not before it is asked. Engine output is decoded with errors="replace" and is never evaluated: it is matched against a version pattern, parsed as JSON or as key = value lines and checked for shape, and printed.

Processes they will start. which invokes discovered engines with --help and with one flag they are expected to reject. presets invokes them with the adapter's enumeration flag. resolve and slice invoke the engine once per run to ask for its configuration -- slice asks for the artifact in the same invocation — and, the first time it meets a given build, a few thousand more times to measure the option-to-key map, which is then cached per engine and version. Three samples of a dozen of 2.9.6's 416 option spellings extrapolate to 1,800–2,800 invocations; the cost per option depends on how many sentinel types the option rejects before one is accepted, so the sampling is unstable and only the order of magnitude is established here. On a host where an untrusted binary is earlier on PATH than the real slicer, that binary is what gets executed — ordinary PATH semantics, but worth stating for a tool whose job is finding executables.

This section is a status claim and part of the gate (org AGENTS.md 2.5). It has been rewritten every time one of its own sentences turned out to be false: when the CLI began parsing arguments; when which began launching engines; when a measurement showed the "writes nothing" sentence had been false since which shipped; when the fix for that relocated the problem rather than removing it; when review found further routes back into it; and when the correction for those was found to have left the same claim standing twenty lines further down. And again when a correction fixed this section’s header, left its body describing two verbs, and was caught by the same review that had just flagged that habit.

There is deliberately no count here any more. This paragraph carried one, and it kept being the false sentence — including in the change written to fix a stale self-referential count, which replaced it with a fresh integer ("stale three revisions running") that no command could settle. An integer that has to be incremented by hand is a claim like any other, and this one kept being the false one. The causes are the record; adding a cause is the same action as rewriting the section, which is what the counter kept failing to be.

Intended posture, once there is code

Stated now so the design is reviewable before it exists, not to describe behaviour that ships today.

Arguments will reach an external program. Values authored in slice.toml become arguments to a slicer. They will be passed as an argv list, never through a shell. A caller who forwards untrusted input is still choosing what a local binary is asked to do.

Configuration is data, and data can execute. PrusaSlicer honours a post_process key that runs an arbitrary command, and it is reachable from a --loaded config file rather than only from a flag — see notes/evidence.md V15, where it also causes the engine to block on stdin at exit 0 while producing no artifact. slicelab therefore refuses a resolved configuration carrying a post-processing script rather than slicing and hoping — will, like everything in this section. It does not yet: slicelab names no such key, and since slice shipped the route is live rather than hypothetical, because a post-process in [set] becomes a flag on an engine that runs it. What stands there today is the artifact gate, which reports that run incomplete and hands nothing over — that is a check on the outcome, not a refusal of the input, and the two are not the same guarantee. An authored slice.toml from an untrusted source must be read as executable input, because the engine treats it that way.

Flatpak filesystem grants. Where the engine is a Flatpak, only the specific directories slicelab computed from the run's own path set are granted, never --filesystem=host. A caller who passes paths in sensitive locations is granting the engine access to them.

Secrets in resolved configuration. A slicer's exported configuration can contain print-host credentials in cleartext — --save emits printhost_apikey where the G-code footer strips it. slicelab redacts those before anything is written, and records that it redacted them. A slice.lock should still be read as configuration, not as a public artifact, until you have looked at it.

Vulnerabilities in the slicers themselves belong upstream: PrusaSlicer, OrcaSlicer.

There aren't any published security advisories