Do not trust the serialized VM bytecode in rocqchk - #22353
Conversation
|
Does this work when modules are involved? Especially functor applications. |
dce008b to
2b60b53
Compare
Apparently not: is enough to trigger an error with this PR. AI elaborates on two issues
Do you have thoughts on whether we should
? |
|
There's also inlining so trying to adapt the equality doesn't really seem possible IMO |
|
Do we care enough to make vm recompilation of libraries lazy when loading with -norec / -admit? (We should not trust the vm code in |
2b60b53 to
89832f7
Compare
|
|
||
| val dirpath_of_library : compiled_library -> DirPath.t | ||
| val module_of_library : compiled_library -> Mod_declarations.module_body | ||
| val replace_module_of_library : compiled_library -> Mod_declarations.module_body -> compiled_library |
There was a problem hiding this comment.
I think I'd rather have this function perform the recompilation rather than just trusting the module body, and move the compilation code somewhere around the kernel VM API.
| let dp = Safe_typing.dirpath_of_library clib in | ||
| let retro = Safe_typing.retroknowledge_of_library clib in | ||
| let env = env_of_library senv clib in | ||
| let vmtab, clib = compile_vm_library env clib in |
There was a problem hiding this comment.
Is is me or are we now unconditionally recompiling the VM segment even when the VM is disabled?
|
@coqbot run full ci |
| @@ -0,0 +1,10 @@ | |||
| - **Fixed:** | |||
| ``rocqchk`` with ``-bytecode-compiler yes`` no longer trusts the VM bytecode | |||
| serialized in a ``.vo``: it does not read the ``vmlibrary`` segment at all, and | |||
There was a problem hiding this comment.
Does that mean that rocqchk will not report a crafted .vo file? Could we instead have it complain loudly if there is a discrepancy?
There was a problem hiding this comment.
That is what I tried to do originally, but the vm bytecode differs depending on whether a constant was constructed "in the normal way" or whether it is a result of module functor application / module inclusion with parameter inlining. I figured it was better to drop the vm segment entirely (in the same way the checker ignores .coq-native) rather than pull in all the functor machinery.
There was a problem hiding this comment.
I see. Sorry for the noise. I had missed the edits to the original post.
| read. *) | ||
| let recompile_vm_library env lib = | ||
| let vmtab = Vmlibrary.set_path lib.comp_name (Environ.vm_library env) in | ||
| if not (Environ.typing_flags env).enable_VM then vmtab, lib |
There was a problem hiding this comment.
On second reading this check doesn't belong there, it should be the job of the checker to decide whether to recompile or not, and it should do so with the user-facing flag rather than the flag from the environment.
|
@JasonGross I think this deserves to be squashed (and maybe to have my freshest nitpick fixed). Also, I'm waiting for input from @SkySkimmer to be sure he is fine with this PR before merging it. |
rocqchk with -bytecode-compiler yes typechecked each constant's body but took the VM bytecode from the separately serialized vmlibrary segment of the .vo, with nothing binding the two together. A crafted file whose vmlibrary disagrees with its library makes a VM conversion prove False, and rocqchk accepts the result with no axioms reported. The checker now compiles the bytecode itself, from the bodies it checks, so the two agree by construction. The vmlibrary segment is not read at all, and the code descriptor stored in each declaration (const_body_code) is replaced rather than kept: BCalias and the unused-argument mask are attacker-controlled too, and the mask is used by ordinary conversion as well. Libraries imported with -admit or as dependencies of -norec get the same treatment, so the trusted surface is uniformly the declarations that are read. Recompiling the bytecode and comparing it against the stored one instead is not workable: stored and recompiled code legitimately differ in relocation names (user vs canonical), in alias collapsing, in the unused-argument mask, and in the inlining of const_inline_code bodies. The compilation itself lives in Modops, next to add_structure, which threads the environment the same way; Safe_typing.recompile_vm_library performs it and substitutes the result, so the kernel exposes no setter that takes a module body on trust. Whether to recompile at all is the checker's decision, taken from the user-facing -bytecode-compiler flag: with the default, no bytecode is ever run, so the traversal is skipped and an empty table handed over. Fixes rocq-prover#22352. Co-authored-by: Jason Gross <jason@theorem.dev> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9d8b43a to
1ec462f
Compare
|
@ppedrot updated. And with rocq-prover/bot#384 you can now have coqbot squash for you |
rocqchk -bytecode-compiler yestypechecks each constant's body but takes the VM bytecode from the.vo'svmlibrarysegment, with nothing tying the two together. A.vowhose bytecode disagrees with its bodies provesFalseand passes the checker (#22352).This stops reading that segment. The checker compiles bytecode itself, from the bodies it just typechecked, and uses that — so the code it runs agrees with the term it checked by construction, and there is no comparison to get wrong.
const_body_codeis replaced wholesale: the compiled code, the unused-argument mask, the patches and theBCaliasrouting all come from our own compilation, and nothing from the file survives. The mask matters independently of the VM —Conversion.eqapprpasses it toconvert_stacks ~mask, so a lying mask makes ordinary conversion skip comparing arguments, reachable with the default-bytecode-compiler no. I have not built an exploit for that, only closed it.Admitted and
-noreclibraries get the same treatment: their bytecode is compiled from the declarations that were loaded. The trust surface is then exactly "the declarations we read", with no special case.Earlier approach, for reviewers following the thread
The first version recompiled the bytecode and compared it against the stored bytecode. That is unworkable and has been removed. Stored and recompiled code legitimately differ in at least four ways — user vs canonical names in relocations,
get_aliascollapsing, the unused-argument mask, and inlining ofconst_inline_codebodies — and it false-rejected ordinary code.Module N := M.was enough. Thanks to @SkySkimmer for pointing out that inlining puts an equality-based fix out of reach.Effect on the failure mode
The tampered
.voitself is now accepted, correctly: its declarations are well typed, only its bytecode segment lies, and nothing reads that. The unsoundness shows up in a library compiled against it, whose VMcast only typechecked becauserocq compilebelieved the bogus bytecode — and that library is rejected with a plainType error: ActualType, identical to what the non-VM checker already reports. The test asserts exactly that, and fails on master.Checked
12 module/functor shapes accepted (the previous design false-rejected 4 of them), cross-library VM conversions through aliases and functor applications, Corelib 67/67 with
-bytecode-compiler yes(master: 65/67, the two failures being #22360),test-suite/modules, and a 1853-file sweep ofsuccess/bugsunder-norecwith the VM on — identical results to master.Since the segment is no longer read, #22360 no longer applies to the checker; it is still worth landing for
votour.Fixes #22352.
Opened autonomously by Claude (Opus 5) on behalf of Jason Gross (jason@theorem.dev).