[ 
https://issues.apache.org/jira/browse/GROOVY-12244?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18103521#comment-18103521
 ] 

ASF GitHub Bot commented on GROOVY-12244:
-----------------------------------------

codecov-commenter commented on PR #2779:
URL: https://github.com/apache/groovy/pull/2779#issuecomment-5247262735

   ## 
[Codecov](https://app.codecov.io/gh/apache/groovy/pull/2779?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
 Report
   :x: Patch coverage is `77.14286%` with `8 lines` in your changes missing 
coverage. Please review.
   :white_check_mark: Project coverage is 70.0688%. Comparing base 
([`e3de6b4`](https://app.codecov.io/gh/apache/groovy/commit/e3de6b4e1df0a4440dc68fcd6cc8c9d50584ebd7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache))
 to head 
([`328d5d4`](https://app.codecov.io/gh/apache/groovy/commit/328d5d4a42c13e843bcfbcc4d17056c0d688e6ee?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   :warning: Report is 1 commits behind head on master.
   
   | [Files with missing 
lines](https://app.codecov.io/gh/apache/groovy/pull/2779?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
 | Patch % | Lines |
   |---|---|---|
   | 
[...roovy/control/customizers/SecureASTCustomizer.java](https://app.codecov.io/gh/apache/groovy/pull/2779?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Forg%2Fcodehaus%2Fgroovy%2Fcontrol%2Fcustomizers%2FSecureASTCustomizer.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3JjL21haW4vamF2YS9vcmcvY29kZWhhdXMvZ3Jvb3Z5L2NvbnRyb2wvY3VzdG9taXplcnMvU2VjdXJlQVNUQ3VzdG9taXplci5qYXZh)
 | 77.1429% | [4 Missing and 4 partials :warning: 
](https://app.codecov.io/gh/apache/groovy/pull/2779?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
 |
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   [![Impacted file tree 
graph](https://app.codecov.io/gh/apache/groovy/pull/2779/graphs/tree.svg?width=650&height=150&src=pr&token=1r45138NfQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)](https://app.codecov.io/gh/apache/groovy/pull/2779?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
   
   ```diff
   @@                Coverage Diff                 @@
   ##               master      #2779        +/-   ##
   ==================================================
   + Coverage     70.0527%   70.0688%   +0.0160%     
   - Complexity      35654      35674        +20     
   ==================================================
     Files            1560       1560                
     Lines          131885     131919        +34     
     Branches        24209      24220        +11     
   ==================================================
   + Hits            92389      92434        +45     
   + Misses          31143      31132        -11     
     Partials         8353       8353                
   ```
   
   | [Files with missing 
lines](https://app.codecov.io/gh/apache/groovy/pull/2779?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
 | Coverage Δ | |
   |---|---|---|
   | 
[...roovy/control/customizers/SecureASTCustomizer.java](https://app.codecov.io/gh/apache/groovy/pull/2779?src=pr&el=tree&filepath=src%2Fmain%2Fjava%2Forg%2Fcodehaus%2Fgroovy%2Fcontrol%2Fcustomizers%2FSecureASTCustomizer.java&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3JjL21haW4vamF2YS9vcmcvY29kZWhhdXMvZ3Jvb3Z5L2NvbnRyb2wvY3VzdG9taXplcnMvU2VjdXJlQVNUQ3VzdG9taXplci5qYXZh)
 | `54.2345% <77.1429%> (+3.3725%)` | :arrow_up: |
   
   ... and [6 files with indirect coverage 
changes](https://app.codecov.io/gh/apache/groovy/pull/2779/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)
   </details>
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :snowflake: [Test 
Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, 
report on failures, and find test suite problems.
   - :package: [JS Bundle 
Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save 
yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>




> SecureASTCustomizer does not check authored code relocated into a synthetic 
> method
> ----------------------------------------------------------------------------------
>
>                 Key: GROOVY-12244
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12244
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> h2. Summary up front
> This is about *consistency of treatment*, not about catching more code.
> Someone who configures {{disallowedReceivers = ['java.lang.System']}} holds a 
> single expectation: System calls are rejected in the source they compile. 
> After GROOVY-12238 that expectation holds everywhere - method bodies, 
> constructor bodies, static and instance initializers, field initializers, 
> closures relocated into a generated constructor by 
> {{@TupleConstructor(pre=...)}}, groovy-contracts conditions inlined into loop 
> bodies - with exactly one exception: {{@ConditionalInterrupt}}.
> Nothing visible to the user explains the exception. It exists because that 
> transformation relocates its closure into a *synthetic* method while the 
> others relocate into constructors, ordinary methods or generated classes, and 
> the customizer skips synthetic methods. That is an implementation detail of 
> one transformation leaking into observable behaviour, and no user could be 
> expected to predict it.
> Measured across 1632 real files the change produces *zero false positives*, 
> with the checks firing 78 times throughout. It produced no new catches on 
> that corpus, because Groovy's test tree happens to contain no case combining 
> {{@ConditionalInterrupt}} with a restriction - but the case for the change 
> does not rest on catching more code, it rests on the rule being uniform.
> Depends on GROOVY-12238, which introduces the two helpers it reuses.
> h2. Problem
> {{SecureASTCustomizer}} skips synthetic methods when visiting method bodies:
> {code:java}
> for (MethodNode methodNode : clNode.getMethods()) {
>     if (!methodNode.isSynthetic() && methodNode.getCode() != null) {
>         methodNode.getCode().visit(visitor);
>     }
> }
> {code}
> That is right for compiler-generated members, but a transformation may 
> relocate code the *user wrote* into a synthetic method. 
> {{ConditionalInterruptibleASTTransformation}} does exactly this - it lifts 
> the closure supplied to {{@ConditionalInterrupt}} into a private synthetic 
> method and injects calls to it at every method start and every loop:
> {code:groovy}
> type.addSyntheticMethod(conditionMethod, ACC_PRIVATE, ClassHelper.OBJECT_TYPE,
>         Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, conditionNode.code)
> {code}
> So with {{disallowedReceivers = ['java.lang.System']}} configured, this 
> compiles and runs today:
> {code:groovy}
> import groovy.transform.ConditionalInterrupt
> @ConditionalInterrupt({ System.getProperty('x') != null })
> class A { def m() { 1 } }
> {code}
> while the same call written directly in a method body is correctly rejected.
> This is the same relocation story as GROOVY-12238, one destination further 
> on. There, authored code moved into a generated *constructor* escaped the 
> checks; here it is a synthetic *method*. Dumping the AST at CANONICALIZATION 
> shows the code arrives with its source position intact, exactly as in the 
> constructor case:
> {noformat}
> @ConditionalInterrupt({ System.getProperty('x') != null })
>   method conditionalTransform...$condition  line=-1 synthetic=true codeLine=2
>     stmt[0] line=2   (java.lang.System.getProperty(x) != null)   <- authored, 
> relocated here
> {noformat}
> Set against the other transformations which relocate a user-supplied closure, 
> {{@ConditionalInterrupt}} is the only one left uncovered:
> ||Transformation||Where the authored closure lands||Checked after 
> GROOVY-12238?||
> |{{@TupleConstructor(pre/post)}}, {{@MapConstructor(pre/post)}}|generated 
> constructor|yes|
> |{{@AutoImplement(code)}}|generated method|yes|
> |groovy-contracts {{@Requires}}/{{@Ensures}}/{{@Invariant}}|generated closure 
> class added to the module|yes|
> |groovy-contracts loop {{@Invariant}}/{{@Decreases}}|inlined into the loop 
> body|yes|
> |*{{@ConditionalInterrupt}}*|*synthetic method*|*no*|
> |{{@ASTTest}}|nowhere in the AST - rebuilt from raw source text|no, and 
> unreachable by any AST-level check; it has its own off-switch, 
> {{groovy.asttest.enable}}|
> Each destination was chosen for a good reason - {{@ConditionalInterrupt}} 
> injects at every method start and every loop, so one method called many times 
> is cheaper than inlining the condition dozens of times per class. The point 
> is not that the transformation is wrong, but that its choice is invisible to 
> whoever configured the customizer.
> h2. Change
> About six lines, reusing {{visitAuthoredStatementsOf}} and {{isFromSource}} 
> from GROOVY-12238:
> {code:java}
> for (MethodNode method : clNode.getMethods()) {
>     if (method.isSynthetic() && !"<clinit>".equals(method.getName()) && 
> method.getCode() != null) {
>         visitAuthoredStatementsOf(method.getCode(), visitor);
>     }
> }
> {code}
> The per-statement source-position filter is what makes this safe: a synthetic 
> method's *generated* statements carry no source position and are skipped, 
> while relocated authored statements carry one and are checked. {{<clinit>}} 
> is excluded because GROOVY-12238 already handles it.
> h2. Evidence
> *Exposure is far narrower than "synthetic" suggests.* Nine common constructs 
> were scanned for synthetic methods containing source-positioned statements - 
> trait implementations, {{@Delegate}}, records, enums, plain properties, 
> closures in methods, {{@Immutable}}, {{@Sortable}}, 
> {{@ConditionalInterrupt}}. Only {{@ConditionalInterrupt}} produced one. 
> Generated accessors, delegate forwarders, record components, enum machinery 
> and trait bridges all carry unpositioned statements, so the filter excludes 
> them without needing to enumerate them. Notably, a trait's method body is 
> *not* copied onto the implementing class as a source-positioned synthetic 
> method, so there is no duplicate-checking problem there.
> *Corpus: zero false positives; no new catches, for want of a case to catch.* 
> Every {{.groovy}} file under {{src/test}} (1632 files; 22 using 
> {{@Grab}}/{{@Grapes}} excluded) compiled under a {{SecureASTCustomizer}} with 
> {{disallowedReceivers = ['java.lang.System', 'java.lang.Thread', 
> 'java.lang.Runtime', 'java.lang.ProcessBuilder']}}, on top of GROOVY-12238, 
> with and without this change:
> ||Corpus outcome||Without||With||
> |Compiled|1554|1554|
> |Rejected|78|78|
> Byte-identical. The 78 rejections show the checks were live throughout, so 
> the unchanged verdicts mean the change was exercised and stayed quiet. But no 
> file in the corpus was newly caught either: the three files using 
> {{@ConditionalInterrupt}} have conditions that do not touch the restricted 
> receivers, so the corpus could not demonstrate an upside.
> *Hand-written case:* {{@ConditionalInterrupt}} with a disallowed receiver is 
> permitted without the change and rejected with it. Full test suite passes.
> h2. Is it worth doing
> The case for it is consistency. A user configuring the customizer has no way 
> to know that one transformation relocates into a synthetic method while its 
> neighbours relocate into constructors, generated methods, generated classes 
> or the loop body itself, and that the customizer looks in every one of those 
> but the first. Uniform treatment is what makes the configuration 
> comprehensible; an exception that cannot be predicted from the source is the 
> kind of surprise THREAT_MODEL.md section 3 identifies as worth acting on, 
> whether or not it is common.
> It also generalises. The fix catches *any* transformation that relocates 
> authored code into a synthetic method, including third-party ones that will 
> never be reviewed here, which is the property that a per-transformation fix 
> would not have.
> The case against is that one first-party annotation is known to benefit, and 
> there is no evidence that anyone combines {{@ConditionalInterrupt}} with 
> {{SecureASTCustomizer}} today.
> That objection is worth weighing carefully, because it is an absence of 
> evidence rather than evidence of absence: the corpus is Groovy's own test 
> tree, never written to exercise that combination, so it could not have shown 
> a benefit however useful the change is. But the consistency argument does not 
> depend on the answer. Even at zero current users, the rule "restrictions 
> apply to the code you wrote" either holds uniformly or it does not, and today 
> it does not.
> h2. Why not change the transform instead
> Making the condition method non-synthetic would remove the need for this 
> change, and looks like a one-word fix. It is not:
> * {{isSynthetic()}} is load-bearing in three places in 
> {{ConditionalInterruptibleASTTransformation}} - the injection-eligibility 
> check, the traversal check, and a defensive guard on the condition method 
> itself. Two of them are what stop the condition method being instrumented 
> with its own interrupt check.
> * {{filterMethods}} excludes synthetic methods, so a non-synthetic condition 
> method would count as a *method definition*. Any class using 
> {{@ConditionalInterrupt}} would then be rejected under 
> {{methodDefinitionAllowed = false}} - a new false positive in 
> {{SecureASTCustomizer}}, created by trying to make code visible to 
> {{SecureASTCustomizer}}.
> More broadly, the transform is not doing anything wrong. It relocates 
> authored code into the AST with its source position intact, which is the 
> convention documented in {{ARCHITECTURE.md}} and the user guide; the 
> customizer is simply not looking there. Fixing the consumer covers every 
> transformation at once, whereas changing this transformation fixes one and 
> leaves the next one silently uncovered.
> h2. Scope note
> {{SecureASTCustomizer}} is a best-effort grammar filter, not a security 
> boundary - see THREAT_MODEL.md sections 3, 9 and 11a. This is hardening which 
> removes surprising behaviour; it does not alter that position, and a 
> demonstrated bypass remains by design rather than a vulnerability.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to