Skip to content

identity/boolpolicy: parser depth cap doesn't bound the evaluator, and evaluation isn't memoized - #2164

Merged
Effi-S merged 1 commit into
mainfrom
fix-2077
Aug 12, 2026
Merged

identity/boolpolicy: parser depth cap doesn't bound the evaluator, and evaluation isn't memoized#2164
Effi-S merged 1 commit into
mainfrom
fix-2077

Conversation

@Effi-S

@Effi-S Effi-S commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #2077

Summary

The boolpolicy parser's maxParseDepth limits how deeply nested parentheses can be, but the AST
evaluator that walks the parsed policy has no equivalent bound, and evaluation isn't memoized. A
policy string that stays within the parser's depth limit and length limit can still produce a large,
repetitive AST that causes the evaluator to repeat the same verification work many times over.

Where

token/services/identity/boolpolicy/parser.go:38 sets maxParseDepth = 64, incremented only when
entering a parenthesized group (parser.go:259-265). parseOr/parseAnd build their chains
iteratively, so a policy like $0 OR $0 OR ... OR $0 — well within maxPolicyLen (4 KiB) — produces
a long, flat-but-large AST without ever tripping the depth counter.

sig.go:91-110 evalNode walks that AST and, for each RefNode occurrence, calls
v.Verifiers[i].Verify again — so a policy referencing the same signature index many times causes
that verification to run many times, even though the signature itself only needs to be checked once.
parser.go:66-82 String() recurses over the same AST shape.

Impact

Because the parser's depth bound doesn't constrain how large or repetitive the resulting AST can be,
a policy that's well-formed and within the configured size limit can still cause a disproportionate
amount of cryptographic verification work relative to its size, on the identity-matching path. This
is a resource-usage concern rather than a crash, and is worth addressing with either memoization or
an explicit bound on total AST node count / reference count.

Suggested fix

Add a cap on the total number of nodes (or RefNode occurrences) produced during parsing, alongside
the existing depth cap, so the AST size itself is bounded regardless of shape. Separately, memoizing
evalNode's per-index verification result within a single Verify call (a signature at a given
index is being checked against the same data regardless of how many times it's referenced in the
policy) would remove the redundant work without needing to change the AST structure at all.

Severity

MEDIUM-HIGH — bounded by the existing 4 KiB policy-length limit, but the per-length cost is higher
than intended given the parser's depth cap doesn't constrain it.

@Effi-S Effi-S added this to the Q3/26 milestone Aug 9, 2026
@Effi-S Effi-S self-assigned this Aug 9, 2026
Comment thread cmd/benchmarking/bench_parse.py Fixed
Comment thread cmd/benchmarking/bench_parse.py Fixed
@Effi-S
Effi-S force-pushed the fix-2077 branch 2 times, most recently from 1102a40 to 02a90b4 Compare August 10, 2026 11:11
@Effi-S
Effi-S marked this pull request as ready for review August 10, 2026 14:05
@Effi-S
Effi-S force-pushed the fix-2077 branch 2 times, most recently from d2aa9aa to dcc48d8 Compare August 10, 2026 14:14
@AkramBitar
AkramBitar self-requested a review August 10, 2026 16:21
AkramBitar

This comment was marked as duplicate.

AkramBitar

This comment was marked as duplicate.

@AkramBitar AkramBitar left a comment

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 fix is correct and well-structured — maxParseNodes on the parser, memoised evalRef on the evaluator, fuzz target wired into the nightly matrix, unit tests for both the cap and the at-limit boundary. One blocker before I can approve:

parser.go uses fmt.Errorf instead of the required errors.Errorf

Per AGENTS.md, fmt must not be used to build or wrap errors anywhere in the tree — use github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors throughout. The specific line:

p.err = fmt.Errorf("policy expression exceeds maximum node count of %d", maxParseNodes)

should be

p.err = errors.Errorf("policy expression exceeds maximum node count of %d", maxParseNodes)

with the "fmt" import removed from parser.go if that was its only use.

That's the only change needed — everything else looks good.

@Effi-S
Effi-S force-pushed the fix-2077 branch 4 times, most recently from 6093d99 to ab6bbf0 Compare August 12, 2026 12:08
@Effi-S

Effi-S commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

"fmt" is used in other places in parser.go so I'm leaving it.
But for every instance where fmt was uses to format errors I updated to use errors package not fmt

@AkramBitar AkramBitar left a comment

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.

LGTM

Signed-off-by: Effi-S <effi.szt@gmail.com>
@Effi-S
Effi-S merged commit c5fd305 into main Aug 12, 2026
151 checks passed
@Effi-S
Effi-S deleted the fix-2077 branch August 12, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

identity/boolpolicy: parser depth cap doesn't bound the evaluator, and evaluation isn't memoized

3 participants