Commit Graph
30 Commits
Author SHA1 Message Date
Pranav 0fed26a14f Sandbox: a crate that runs a helper program confined (Landlock and seccomp on Linux, sandbox-exec on macOS)
## Stack

The Python cells work is split by layer. It merges in this order, each with a merge commit rather than a squash. Nobody can switch Python on before #3481.

1. #3475 depends on `main`
2. #3476 depends on `main` **(this pull request)**
3. #3477 depends on [#3475](https://github.com/storytold/gridcraft/pull/3475) and [#3476](https://github.com/storytold/gridcraft/pull/3476)
4. #3478 depends on [#3477](https://github.com/storytold/gridcraft/pull/3477)
5. #3479 depends on [#3478](https://github.com/storytold/gridcraft/pull/3478)
6. #3480 depends on [#3479](https://github.com/storytold/gridcraft/pull/3479)
7. #3481 depends on [#3480](https://github.com/storytold/gridcraft/pull/3480)

## Summary

`crates/sandbox` runs a program that can read only the paths its policy lists, write nothing except `/dev/null`, start no process, and use no network. An application re-executes itself with a launcher flag, closes every inherited descriptor above standard error, applies the confinement, checks that it holds, and only then runs the program. If any of that fails, the program does not run.

## Changes

- **`crates/sandbox`:** the policy, the Linux backend (Landlock, and seccomp that denies all sockets, with an x32 filter), the macOS backend (`/usr/bin/sandbox-exec` with a deny-default profile), the probes and a canary.
- **`apps/gridcraft`, `apps/gridcraft-cli`:** the launcher flag comes first in `main`, before any window or log.
- **`gridcraft-cli sandbox-check`:** says whether this system confines programs, and can run a confined self-test.
- **`docs/sandbox.md`:** what is refused and how.
- **`nix`:** a new dependency on Linux and macOS, for closing descriptors without `unsafe`.

## Compatibility

Windows and FreeBSD have no backend here. There the crate reports that it can't confine, and nothing that needs it runs. On Linux a launch needs Landlock ABI 3, which is Linux 6.2 and later, so an older kernel is treated the same way: this excludes, for example, Debian 12's kernel (6.1) and Ubuntu 22.04's original kernel (5.15). Nothing changes for a process started without the launcher flag.

## Review fixes

- **File metadata on Linux.** Landlock has no right for a file's permissions, owner, extended attributes, times or flags, and the seccomp filter allows what it doesn't name, so a confined program could change them on any file its user owns, outside the policy too. The filter now refuses the `chmod`, `chown`, `setxattr`, `removexattr` and timestamp families with their `*at` variants, by path and by descriptor, and any `ioctl` whose number says it hands the kernel data, as `FS_IOC_SETFLAGS` and `FS_IOC_FSSETXATTR` do on a descriptor opened only to read. A filter allows what it doesn't name, so a call a later kernel adds is allowed until it is named; the newest named is `file_setattr` (Linux 6.17).
- **Truncation on Linux.** Landlock covers truncation only from ABI 3, so on ABI 1 or 2 `os.truncate(path, 0)` emptied any file its user can write, and `open` with `O_RDONLY | O_TRUNC` emptied one it may read. A launch now requires that right. A filter can't take its place: `openat2` hides its flags from seccomp, and refusing `O_TRUNC` would refuse opening `/dev/null` to write, as `open(os.devnull, "w")` does.
- **Standard streams.** They stay open with the rights they were opened with, so a stream that is a file can be written and truncated, and one that is a socket can still send. The page and `launch_command` say so, and that a parent gives the program pipes.
- **The enforcement check, again.** It now also tries to change the canary's permissions and to truncate it, asking for what it already has, so nothing changes if one works.
- **Other processes on Linux.** On a kernel without Landlock's signal scope, a confined program could signal any process of the same user, and `kill(-1, 9)` would reach them all. The seccomp filter now refuses signals to any process but the program itself, whose ID `exec` keeps; changes to the limits, priority or scheduling of any other process; `pidfd_open`, `pidfd_send_signal`, `kcmp`, `get_robust_list`, `migrate_pages` and `move_pages`; every System V shared memory, message queue and semaphore call and every POSIX message queue call, which no path rule covers; and `syslog`.
- **The terminal on Linux.** A program started from a shell could type into its terminal. `TIOCSTI` and `TIOCLINUX` are refused, compared in 32 bits as the kernel reads them, so high bits set on the argument don't get past the rule.
- **The enforcement check.** It now also tries signal 0 on the launcher's parent, which delivers nothing, so `sandbox-check` reports a gap rather than assuming none. The launcher tries the same signal before it confines itself and refuses if that fails, and the check refuses if its parent has changed since, since a parent that refuses anyone would prove nothing. A launcher's arguments must include the current executable, which that check starts; otherwise the file rules refused the start and the check proved nothing about new processes.
- **A refused launch.** The launcher read its arguments only after closing every inherited descriptor, so a refused launch had already closed them. It reads them first.
- **A kernel without Landlock.** The refusal says so in that case too, which the tests that skip on such a machine look for. Only the first ABI's rights being refused, or `ENOSYS` or `EOPNOTSUPP` from creating the ruleset, says so. Any other failure, such as `EINVAL` or too many layers, is reported as what it is and doesn't skip the tests.
- **Folders in the exec list on macOS.** A folder got a rule that matches only the folder, so a launcher under it failed to start where the same policy works on Linux. It now covers what is in it, in both rules.
- **The sandbox page** names only what this layer has: only `gridcraft-cli sandbox-check` starts the launcher here.

## Testing

All 10 steps of `cargo xtask ci` pass on macOS (arm64), and `cargo clippy` passes for this crate for both Linux architectures. The gate on ubuntu-24.04 is the hosted one on this pull request, and it executes the Linux backend through the real binary, including `a_confined_program_leaves_other_processes_alone` and `a_confined_program_changes_no_file`, in which a confined Python tries each refused call on a file inside the policy, one outside it and a descriptor, and the test then compares contents, permissions and modification time.

A separate run on the fork, on Linux 7.0 with Landlock ABI 8, covered the same commit's code with parts of the fix taken out. Without the metadata rules, the launch check refuses with `change the canary's permissions: it worked`. Without those rules or the new checks, `a_confined_program_changes_no_file` fails with `permissions of ...: it worked`. With the Landlock rights limited to ABI 2, as on an older kernel, it fails with `truncate ...: it worked`. A kernel that is actually older than ABI 3, or that has no Landlock at all, has not been run. On macOS, terminal input was checked by hand on a pseudo-terminal: `TIOCSTI` is refused when confined and works when not.

The commits group the change by area for reading. Only the last commit of the branch is expected to build.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
  * Added sandbox support for running commands with policy-limited file access and restrictions on network and process operations. The sandbox is available on supported Linux and macOS systems; launches are refused if confinement cannot be established or verified.
  * Added the `sandbox-check` command to check sandbox support and enforcement, or run a specified program in the sandbox. It reports when sandboxing is unsupported.
* **Documentation**
  * Added guidance on sandbox behavior, platform support, access restrictions, launch checks, and `sandbox-check` usage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-11-11 07:31:42 +01:00
Dylan M. Taylor 74111e5a93 check: charge relationship parts to the package's decompression budget (#3185)
* check: charge relationship parts to the package's decompression budget

* address review: charge bytes read before a package part decompression error
2026-11-03 22:50:40 -05:00
Dylan M. Taylor e8ccb5eb15 check: find volatile defined names with a worklist instead of repeated passes (#3187) 2026-11-02 23:33:31 -05:00
Dylan M. Taylor e6df6f423b Refactor the gridcraft apps for strict Clippy checks 2026-10-28 10:52:53 +00:00
aiongg da0cb382ae refactor(gridcraft-cli): move check.rs items to check/ 2026-10-26 21:16:41 +00:00
aiongg a8db625e6b style: cargo fmt on the moved test files 2026-10-26 18:15:59 +00:00
aiongg da40ecba3a refactor(gridcraft-cli): move check tests to src/check/tests.rs 2026-10-26 18:15:39 +00:00
3b65b9af58 Sonar maintainability: explicit imports, method paths, smaller functions (#2412)
* Sonar: explicit imports, fn paths for closures, extract helpers for cognitive complexity

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Fix imports

* Fix serde imports

* Fix engine imports

* Fix clippy gate and a type error from the explicit-import change

Remove the unused imports the review flagged (cf.rs, pivot.rs, tables.rs, cmd/sheet.rs) so clippy -D warnings passes, and point the backstage parent-folder lookup at Path::to_str, which the fn-path rewrite had broken.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Apply rustfmt to the explicit-import cleanup so the fmt gate passes

* Share the per-directory font path expansion in theme.rs

The candidate font lists repeated the same loop over the Linux font
directories in every role. A single helper expands them in the same order,
so the lookup order is unchanged and the duplicated blocks go away.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Read the Windows fonts folder in one place in theme.rs

Every script-font list repeated the same WINDIR lookup. A single helper
returns the same path, so the duplicated lines go away without changing
which font files are tried.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Build the light and dark UI palettes from one table in theme.rs

Tokens::light() and Tokens::dark() listed the same 40 colour fields in the
same order, so every change had to be made twice. A single palette
constructor takes the mode and picks each colour's value, so each field is
written once and both modes keep their exact colours.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Share the Noto CJK font list between the CJK script lists in theme.rs

The Japanese, Simplified and Korean lists each repeated the same loop over
the Linux CJK directories with a different locale file name. One helper
builds that loop, and each list keeps its own face index, so the lookup
order is unchanged.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Share Linux Noto directory list across Thai and Hebrew font lookups

Removes the duplicated directory arrays flagged by SonarCloud new-code duplication.

* Reuse in_dirs for Thai and Hebrew Noto font candidates

Removes the remaining duplicated per-directory loops flagged by SonarCloud.

* Share one font-list builder for the Thai and Hebrew faces

The two per-script functions repeated the same Windows, macOS, Linux and
variable-font setup. A single script_fonts helper now builds both lists,
keeping the candidate order unchanged, which clears the Sonar duplication gate.

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-25 12:32:02 -04:00
NetterSilverandClaude Haiku 5.5 64a78a7210 Print huge and tiny CLI numbers in exponent form
Found while testing apps/gridcraft-cli/src/main.rs:196-199 (number_text, used by value_text and check.rs:627)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 09:44:39 +00:00
Dylan M. Taylor 10bec74810 M6: misc hardening — unbounded picture cache, stats walk, signing tmpdir, web clipboard, dash budget, fs cycle, rezip loop (#2422)
- floating_pictures: draw without retaining when eviction can't free
  enough room, so a large workbook can't pin the cache past its budget
- xtask stats: skip unreadable subtrees instead of aborting the walk
- import-cert.sh: per-run private temp dir instead of predictable
  paths in a shared root
- gridcraft-web: gate clipboard ops behind user activation
- chart raster: cap dash rendering to the stroked budget
- cli check: mark directories visited by identity on unix so hard-link
  loops can't recurse; reuse one decompression of a reused part
2026-10-24 06:33:19 -04:00
Hamza 56207f2d3c run --script: a line's unknown key stops the run (#2256)
A script line whose parameters sat under a key the runner does not read ran the
command with no parameters at all, and the run still exited 0:

    {"command":"home.insertSheet","args":{"name":"Report"}}
    {"command":"home.insertSheet","name":"Report2"}

made Sheet2 and Sheet3 instead of Report and Report2, and said nothing. A typo
in a script produced a valid, wrong workbook.

A line is read for `command` (or `id`) and `params`; any other key is a usage
error now, naming the keys and the line, and the run stops before the workbook
is written:

    gridcraft-cli: s.jsonl:2: unknown key `args`: a line is {"command": "id", "params": {…}}

The usage text says what a line looks like, which is the other half of the
report — the format was not written down anywhere, so the reporter found it by
trying `args`, a flat object and `id` in turn.

Tested at both levels: `parse_command` for the keys, their sorted list when
there are several, and the forms that are read (`id`, `params`, `id={json}`,
comments); and a run of a four-line script that stops at line 2 without writing
the file, then the same script corrected, whose sheets come out named. Without
the check the mistaken line parses again.

An unknown key *inside* `params` is still ignored — the rest of #2256 — and
needs the command's parameter list, which the engine does not expose yet. Noted
on the issue.
2026-10-22 17:50:09 +05:00
aiongg d5cdd2ea2b Integration: track t15
# Conflicts:
#	contributors/people.toml
#	crates/engine/src/tests.rs
#	docs/file-format-parity.md
#	docs/localization-parity.md
2026-10-22 03:05:39 +00:00
aiongg b10e41c1c7 Merge PR #1726: Fail CLI checks on incomplete or empty input discovery 2026-10-22 02:35:17 +00:00
aiongg f295fa0947 Merge PR #629: Make check --json list warning messages instead of a count (#526) 2026-10-22 02:34:30 +00:00
TheRenegadeDev edcc76d0a8 Fail CLI checks on incomplete or empty input discovery (#1051) 2026-10-16 20:39:34 -05:00
derechopiano 78ffd3b4c3 fix(pdf): preserve exported links and allow CLI PDF conversion (#954) 2026-10-14 13:40:16 +01:00
Brandon Thomas 9972a75f10 Merge main into #824
# Conflicts:
#	crates/engine/src/cmd/file.rs
#	crates/engine/src/tests.rs
#	crates/xlsx/src/tests/passthrough_tests.rs
2026-10-13 00:53:41 -04:00
Brandon Thomas 2174db2006 Merge main into #871
# Conflicts:
#	docs/cli.md
2026-10-13 00:09:07 -04:00
Brandon Thomas f0e158f089 Merge pull request #882 from CouchRiv:fix/check-bool-cached-as-number 2026-10-12 23:50:14 -04:00
Brandon ThomasandClaude Opus 5.5 284c33a0f5 Fix-up: rustfmt check.rs after merging the check PRs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:37:40 -04:00
CouchRivandClaude Opus 5.5 c186781d8b check: a boolean cached as 1/0 without t="b" matches and is counted apart
Business Central's "Open in Excel" exports write booleans as
<c><f>TRUE()</f><v>1</v></c> with no t="b", so the cached value reads back
as the number 1 and the calc oracle reported every such cell as a
`number → bool` mismatch. A cached 1 or 0 now matches GridCraft's TRUE or
FALSE, and the cells are counted apart (`boolAsNumber` in --json, a note in
the text report) so a real number/logical disagreement stays visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:36:20 -04:00
Marco Arnulfo 6dc9138a64 check: internal links and empty parts aren't package losses (part of #860) 2026-10-13 03:52:00 +02:00
Marco Arnulfo bf2923c4ba check: the package round trip reports relationship kinds the save lost (part of #860) 2026-10-13 03:04:37 +02:00
Marco Arnulfo 5406fc5629 XLSX: Save As into another folder keeps links on the same workbook (fixes #801)
Excel stores the path of a linked workbook relative to the file that uses it, and rewrites it when the file is saved into another folder. The path was kept as text, so after Save As it named a file next to the new copy: a missing link, or another workbook with the same name. Relative externalLinkPath targets are now resolved against the folder the workbook was read from and written relative to the folder it is saved into (absolute when there is no relative path, on another drive).
2026-10-12 22:55:53 +02:00
MarekSpodBiedry 9a698aa1ff Make gridcraft-cli check compare conditional-format rule contents, not just their count (#559) 2026-10-12 08:36:53 +02:00
MarekSpodBiedry 0019c2c552 Make check --json list warning messages instead of a count (#526) 2026-10-12 08:31:39 +02:00
neilcawseandClaude Opus 5.5 f7bc97b378 CLI: check subcommand - cached-value calc oracle and XLSX round-trip fidelity on any corpus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:39:30 -04:00
MarekSpodBiedry fe162f8f23 Report an error when file commands get no file in MCP and CLI (#135) 2026-10-10 19:05:56 +02:00
harrisonoconnorhover c51e51fe7b #92: import XLSB worksheet data and saved formula results 2026-10-09 22:19:36 -04:00
Brandon ThomasandClaude Opus 5.5 488e12de31 Rename SheetCraft to GridCraft everywhere (crates, apps, bundle ids, packaging, docs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:39:08 -04:00