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:negative-phoare-falsefrom
Yiping106283:fix-phoare-rnd-nonneg
Open

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

Conversation

@Yiping106283

@Yiping106283 Yiping106283 commented Sep 11, 2026

Copy link
Copy Markdown

Fixes #1119. Based on #1105 (negative-phoare-false, head bb75dba); this replaces the earlier version of this PR, which was based on main.

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

Note on CI: the external-project checks fail here for the same reason as on #1105 itself — CI looks for a merge-<this branch> branch on each downstream repository and otherwise checks out their default branch, which does not have the #1105 script changes yet.

@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
…`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; 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), 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.

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>
Comment thread src/phl/ecPhlRnd.ml
[bd] only if [bd] is non-negative. Moreover a pHL judgment is false as
soon as its bound is negative in some memory (whether or not that
memory satisfies the pre-condition): emit [0 <= bd] as a separate
(last) goal, quantified over all memories, unconditionally. *)

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 would remove the above comment. It is at the wrong place and not clear. A comment can be added at line 242 and 268 to explain why bound check are added (a short clear one liner is ok).

Comment thread src/phl/ecPhlRnd.ml
| (PNoRndParams | PSingleRndParam _), FHle -> t_side
| _ -> t_id in
FApi.t_last t_side (t_bdhoare_rnd tac_info tc)

@lyonel2017 lyonel2017 Sep 14, 2026

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.

This could be done directly inside the t_bdhoare_rnd_r function, inside the existing pattern matching. Would avoiding doing the two pattern matching here, and, if t_bdhoare_rnd is called multiple time, duplicating this code (cf. the changes in ecPhlAuto.ml and ecPhlHiAuto.ml).

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

2 participants