Repository navigation
Conversation
A disposable call sends its whole header to Lean with no environment, but the allowlist read it line by line. Lean skips comments before and between imports and accepts several imports on one line, so a header such as a block comment followed by an import, or two imports on one line, reached Lean without its later modules being checked. The disposable path now scans the header the way Lean does, reading every import across comments and lines, and rejects header syntax the scanner does not model (module, prelude, public/meta/private import, import all) so validation fails closed.
Deicyde
left a comment
There was a problem hiding this comment.
Please replace the hand-written header scanner (_header_import_modules, servers/repl/core.py:298) with Lean's own header parser, via lean --deps.
Right now the allowlist check depends on a Python reimplementation of Lean's header grammar. It matches Lean on every case tried so far, but any gap between the two is an allowlist bypass, and the grammar changes across Lean versions (module, public/meta import, import all are recent). Lean already ships the parser we need:
lean --deps file.leanparses the header with Lean's real parser and prints the resolved file for each direct import. Run it under the worker'slake envso it resolves against the same search path as the REPL.- On Lean 4.32.0 it handled all the tricky headers correctly: comments before and between imports, several imports on one line, comments inside a module name,
module/public import/import all,prelude, and an import after the body. - An unresolvable module makes it exit nonzero, so failure rejects by default.
- Each call took well under a second, which is small next to the fresh Mathlib-importing REPL each disposable run already starts.
Suggested shape:
- Write the submitted code to a temp file and run
lake env lean --deps <file>with the remaining deadline. - Nonzero exit or timeout: return
Rejected Lean header: ...with Lean's stderr. - Map each printed path back to a module name by stripping the
LEAN_PATHentry it sits under and the.oleansuffix, then check the root againstallowed_importsas today. Skip the implicitInitentries. - Delete
_header_import_modulesand the special-case rejections formodule/prelude/modifiers, since Lean now handles those. - Keep the existing adversarial header cases as tests against the new path. They make good regression tests for the
--depsintegration.
Optional, same PR or a follow-up: the stateful run() path still uses the line-based _split_imports_and_body for its allowlist check (core.py:400). It isn't exploitable today, because stripped-header code reaching Lean as body can't import anything, but using one parser for both paths would remove the second copy of the header rules.
A compiled Lean helper built on Lean.Parser.parseHeader + Elab.headerToImports also works and returns module names directly. --deps is preferred because it needs no extra build artifact tied to the toolchain.
Replace the hand-written Python header scanner with `lean --stdin --deps`, run under `lake env` so it resolves imports against the worker's search path. Each resolved .olean path is mapped back to its module name and its root checked against the allowlist. A header Lean rejects, an unresolvable import, a path outside the search path, or a timeout rejects the run. The check now runs on the exact text sent to Lean, after warmup imports are prepended, and no longer needs special cases for `module`, `prelude`, or import modifiers. Adversarial header cases move to the real-REPL job so they run against the pinned Lean.
…ines exact `lean --deps` reports resolved .olean paths, and mapping them back to module names misreads a quoted atomic name such as «Mathlib.X» as root Mathlib. `lean --deps-json` reports each module name as Lean spells it, needs no search-path mapping, and reports header parse errors in its output, which now reject the run. Its output shape is unchanged from Lean 4.26 to 4.34. LeanRepl.close_with_deadline no longer widens the pool's shared deadline per worker, so shutting down several workers cannot outlast the cleanup budget. close() still reserves cleanup time after an expired request deadline. Also remove the unused startup_stagger setting, document that import filtering is not a security sandbox, and add a real-REPL test that the same declaration succeeds on two consecutive pool calls.
8498813 to
55dd2cc
Compare
|
Superseded by the direct Lean Beam persistent-session architecture in draft #46. That replacement removes the custom REPL/runtime stack instead of extending disposable child execution. The branch remains available for history; #46 is explicitly blocked from merge until its upstream Beam release gates are satisfied. |
|
Restored for active work as draft #101 on a facebookresearch-owned branch. The replacement is merged with current main and preserves the original REPL safety design without resuming development on the old fork branch. |
Stack position: 2 of 3. Builds on #13 (merged as afaf215). The generation-isolation slice follows as a separate PR once this lands.
This PR introduces
LeanRepl.run_disposable()and routes every public pooled REPL call through it. Each call gets a fresh child, exactly one request frame, strict response validation, and verified process-group cleanup before a result is returned.The cutover also:
envandproofStatehandles;lean --deps-json, before adding warmup imports, and accepts the known Lean 4.19 and current output schemas strictly;LEAN_SYSROOT, so a project executable cannot shadowlean;@repl/repltarget and documents the required immutable consumer dependency;Import filtering limits calls to known libraries; it is not a security sandbox, because submitted Lean can still run arbitrary
IO.Validation: Ruff, the full Python suite, the setup example check/render, both pinned REPL builds, and 11 real-REPL tests pass locally. The cleanup-grace fix from #13 is preserved.