Skip to content

fix(phl): emit the non-negativity of the bound as a separate goal in rnd (on #1105) - #1135

Open
Yiping106283 wants to merge 1 commit into
EasyCrypt:mainfrom
Yiping106283:fix-phoare-rnd-nonneg
Open

Yiping106283 wants to merge 1 commit into
EasyCrypt:mainfrom
Yiping106283:fix-phoare-rnd-nonneg

Conversation

@Yiping106283

@Yiping106283 Yiping106283 commented Sep 11, 2026 •

Copy link
Copy Markdown

Fixes #1119. Based on main with #1105 merged (the side goal is unconditional for the reason below).

Summary

The pHL rnd tactic accepted phoare[M.f : true ==> true] <= (-1)%r for a procedure that diverges before its sampling, from which false follows with Pr[mu_ge0]. #1105 does not reach this: the bound check stays inside the hoare post-condition and never passes through conseq, bypr or exfalso, so with bb75dba the reproducer is still accepted and still proves false. The rule now emits the missing premise as a separate goal quantified over all memories, and the rnd tactic tries t_trivial on it.

Under #1105's semantics a pHL judgement is false as soon as its bound is negative in some memory, whether or not that memory satisfies the pre-condition; the side goal is therefore unconditional (forall &hr, 0%r <= bd) because a goal restricted to pre, as in #1135, would still let the rule prove a judgement whose bound is negative outside pre.

Root cause (src/phl/ecPhlRnd.ml, Core.t_bdhoare_rnd_r)

For an upper bound, the rule lifts the bound check mu d E <= bd into the post-condition of a hoare judgment on the statements s preceding the sampling. A hoare post-condition only has to hold on terminating runs, so a diverging prefix discharges it vacuously. The check is sound for the mass of the terminating runs (Pr[s; x <$ d : Q] = sum_m' Pr[s](m') * mu d(m') Q <= Pr[s : true] * bd), but the non-terminating runs contribute probability 0 to the conclusion, which is bounded by bd only if 0%r <= bd: that premise was missing from the rule.

Fix

The two <= branches that emit the hoare goal (explicit event, and event inferred from a post-condition depending on the sampled variable) now also emit forall &hr, 0%r <= bd as a last goal. The rule always emits it; the rnd tactic (process_rnd) tries t_trivial on it, so trivially non-negative bounds stay effort-free and a genuinely negative bound is left as an unprovable goal. auto now only recurses into the program-logic sub-goals produced by rnd (src/phl/ecPhlAuto.ml, t_auto_phl_rnd_r), so it keeps applying rnd when the extra goal is present, and its own final t_trivial closes the goal when it is trivial. The documented rule in doc/tactics/rnd.rst is updated accordingly (its example proof already does not check on main: by smt(dbool1E) does not prove the 1%r/2%r bound with current provers; only the goal structure is updated here).

Impact

  • Behavioural change: scripts that discharged the old post-condition-embedded check with rnd; skip, rnd; auto or rnd=> // now reach the extra goal through ; and must close it when it is not trivial (a symbolic bound without 0%r <= bd in context). Since the goal does not carry the pre-condition, non-negativity facts that used to come from it (0 <= fsize m, 0 <= qF, ...) must come from the library lemmas.
  • Library and examples, each adapted with a one-line smt and the relevant non-negativity lemmas on that goal, on top of Change pHL to prevent negative probabilities #1105's own script changes to the same files: theories/crypto/Birthday.eca, theories/crypto/PROM.ec, theories/crypto/RndExcept.eca, theories/crypto/prp_prf/Strong_RP_RF.eca, examples/PRG.ec, examples/ChaChaPoly/chacha_poly.ec, examples/cramer-shoup/cramer_shoup.ec, examples/global-hybrid/GlobalHybridExamp1.ec, examples/prg-tutorial/PRGc.ec. None of them proves the same side condition twice: Change pHL to prevent negative probabilities #1105 adds 0%r <= bd at conseq/bypr/exfalso, this PR at the rnd rule.
  • One consequence of the unconditional goal that Change pHL to prevent negative probabilities #1105 already met in Strong_RP_RF.eca: a fel bound function must be non-negative for every counter value, not only for 0 <= c < q. In PRGc.ec the bound (i + 1)%r * pr_dstate is negative for i < -1, so the per-query judgement is false under the new semantics; the bound becomes (max 0 i + 1)%r * pr_dstate and the sum is rewritten back with eq_big_seq (one line). The same pattern is what the downstream sites will need.
  • >= and = judgements, and <= with a post-condition independent of the sampled variable (which drops the sampling), are unchanged.

Downstream

The extra goal reaches the CI external projects at symbolic-bound rnd sites: cryptobox (2), sha3 (16), xmss-security (3); sphincsplus and xsalsa20 are unaffected. Those sites need script changes on top of the ones #1105 already requires (the unconditional goal also drops the pre-condition facts the #1135 versions of those changes used); diffs to follow once the base is settled, as merge requests on each project.

Test

@Yiping106283 Yiping106283 changed the title fix(phl): emit the non-negativity of the bound as a separate goal in rnd fix(phl): emit the non-negativity of the bound as a separate goal in rnd (on #1105) Sep 13, 2026
@Yiping106283
Yiping106283 changed the base branch from main to negative-phoare-false September 13, 2026 02:56
@oskgo
oskgo requested a review from lyonel2017 September 14, 2026 10:08
Comment thread src/phl/ecPhlRnd.ml Outdated
Comment thread src/phl/ecPhlRnd.ml
@Yiping106283

Copy link
Copy Markdown
Author

Thanks, both done. The long comment is gone and each of the two <= cases of the pattern matching (lines 242 and 268) now has a one-line comment on why 0%r <= bd is required. The t_try t_trivial moved into Core.t_bdhoare_rnd_r on those two cases, and process_rnd is back to t_bdhoare_rnd tac_info tc. The guard in ecPhlAuto.ml stays: it keeps auto from giving up on rnd when a non-trivial 0%r <= bd remains (t_wp would fail on it).

@fdupress
fdupress force-pushed the negative-phoare-false branch 2 times, most recently from 26f2bfc to c2fdd9b Compare September 18, 2026 18:16
@oskgo
oskgo force-pushed the negative-phoare-false branch from c2fdd9b to 9e533cf Compare September 19, 2026 17:35
@fdupress
fdupress deleted the branch EasyCrypt:main September 20, 2026 17:39
@fdupress fdupress closed this Sep 20, 2026
@oskgo oskgo reopened this Sep 20, 2026
@Yiping106283
Yiping106283 changed the base branch from negative-phoare-false to main September 24, 2026 23:53
…`rnd`

Summary: the pHL `rnd` tactic accepted `phoare[M.f : true ==> true] <= (-1)%r`
for a procedure that diverges before its sampling (upstream EasyCrypt#1119), from which
`false` follows with `Pr[mu_ge0]`.

Root cause (src/phl/ecPhlRnd.ml, `Core.t_bdhoare_rnd_r`): for an upper bound,
the rule lifts the bound check `mu d E <= bd` into the post-condition of a
hoare judgment on the statements preceding the sampling. A hoare
post-condition only has to hold on terminating runs, so a diverging prefix
discharges it vacuously. The check is sound for the mass of the terminating
runs, but the non-terminating runs contribute probability 0 to the
conclusion, which is bounded by `bd` only if `0%r <= bd`: that premise was
missing from the rule.

Fix: the two `<=` branches that emit the hoare goal (explicit event, and
event inferred from a post-condition depending on the sampled variable) now
also emit `forall &hr, 0%r <= bd` as a last goal. Based on EasyCrypt#1105: under its
semantics the bound must be non-negative in every memory, so the goal is
unconditional (restricting it to memories satisfying `pre` would still let
the rule prove a judgment whose bound is negative outside `pre`). The rule
always emits it, and the rule closes the goal itself when it is trivial
(`t_try t_trivial` on the last subgoal of the two `<=` arms, as
`t_bdhoare_seq_r` does for its side-conditions), so trivially non-negative
bounds stay effort-free; `auto` recurses only into the program-logic
sub-goals of `rnd` (src/phl/ecPhlAuto.ml), so it keeps applying `rnd` when
a non-trivial goal remains, and a genuinely negative bound is left as an
unprovable goal. The documented rule in doc/tactics/rnd.rst is updated
accordingly.

Scripts that discharged the old post-condition with `rnd; skip`, `rnd; auto`
or `rnd=> //` now reach the extra goal through `;`: theories/crypto/
{Birthday.eca, PROM.ec, RndExcept.eca, prp_prf/Strong_RP_RF.eca} and
examples/{PRG.ec, ChaChaPoly/chacha_poly.ec, cramer-shoup/cramer_shoup.ec,
global-hybrid/GlobalHybridExamp1.ec, prg-tutorial/PRGc.ec} close it with
`smt` and the relevant non-negativity lemmas (since the goal no longer
carries the pre-condition, facts such as `0 <= fsize m` come from the
library lemmas instead). In examples/prg-tutorial/PRGc.ec the `fel` bound
`(i + 1)%r * pr_dstate` is negative for `i < -1`, so under EasyCrypt#1105 the
per-query judgment is false for such counter values; the bound becomes
`(max 0 i + 1)%r * pr_dstate` (as EasyCrypt#1105 does in Strong_RP_RF.eca) and the
sum is rewritten back with `eq_big_seq`.

Test: tests/phoare-rnd-neg-bound.ec (explicit event, inferred event, and a
loop-free variant). The remaining goal `forall &hr, 0%r <= -1%r` is
introduced with `move=> &hr` (which fails when the goal is not emitted) and
asserted unprovable with `fail (by smt())`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Yiping106283

Copy link
Copy Markdown
Author

The downstream script changes for this PR, on top of the #1105 ones now in the projects:

With them, the sponge, cryptobox and xmss security scenarios pass fully with this PR's build.

Yiping106283 added a commit to Yiping106283/easycrypt that referenced this pull request Sep 25, 2026
@fdupress

fdupress commented Oct 1, 2026

Copy link
Copy Markdown
Member

@lyonel2017 can you please check the changes and mark them done if they are done (approving if you're happy).
I'm now getting to work on the external CI proofs. (Sorry for the delay.)

Comment thread src/phl/ecPhlAuto.ml
tc

(* Recursion guard: a non-trivial [0%r <= bd] left by [bdhoare-rnd] would make
[t_wp] fail and [auto] silently give up on [rnd]: only recurse into phl goals. *)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a preference for the old comment which was clearer to me :

(* [t_auto_rnd] may emit pure side-conditions (the non-negativity of the
   bound in [bdhoare-rnd]): only recurse into the program-logic sub-goals. *)

Comment thread doc/tactics/rnd.rst
whose bound is negative in some memory is false, whether or not that
memory satisfies the precondition). That goal is closed automatically
when it is trivial.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be simplified :

The `rnd` tactic additionally verifies that the bound is non-negative, as a separate
goal and tries to closed it automatically.

Comment thread doc/tactics/rnd.rst
previous postcondition. *)
skip => *;split.
+ by smt(dbool1E).
previous postcondition. A second goal requires the bound to

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-negativity check would be a third goal. Here the second goal is mention twice for different things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pHL rnd E soundness

4 participants