[ 
https://issues.apache.org/jira/browse/WW-5677?focusedWorklogId=1037283&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1037283
 ]

ASF GitHub Bot logged work on WW-5677:
--------------------------------------

                Author: ASF GitHub Bot
            Created on: 23/Aug/26 16:35
            Start Date: 23/Aug/26 16:35
    Worklog Time Spent: 10m 
      Work Description: lukaszlenart opened a new pull request, #1859:
URL: https://github.com/apache/struts/pull/1859

   Fixes [WW-5677](https://issues.apache.org/jira/browse/WW-5677)
   
   Sub-task of WW-5667. WW-5674 replaced `Class.getPackage()` with the cached 
`Class.getPackageName()` inside `toPackageName`, but deliberately left two 
neighbouring call sites alone as out of scope. Both sit on the same per-access 
path, within twenty lines of it. This finishes the job. **No behaviour change.**
   
   ## 1. `checkDefaultPackageAccess` made four `getPackage()` calls
   
   ```diff
   -if (memberClass.getPackage() == null || 
memberClass.getPackage().getName().isEmpty()) {
   +if (toPackageName(memberClass).isEmpty()) {
   ```
   
   …and the same for `targetClass`. `Class.getPackage()` resolves through the 
defining classloader's package map on every call, and the old condition 
evaluated it twice per class, for up to two classes per access.
   
   The two forms agree for every class shape: `getPackage()` returns `null` for 
arrays, primitives and `void`, and names the unnamed package with the empty 
string — all of which `toPackageName` reports as empty.
   
   ## 2. `isExcludedPackageNamePatterns` recomputed the package name per pattern
   
   ```diff
   -return excludedPackageNamePatterns.stream().anyMatch(pattern -> 
pattern.matcher(toPackageName(clazz)).matches());
   +String packageName = toPackageName(clazz);
   +return excludedPackageNamePatterns.stream().anyMatch(pattern -> 
pattern.matcher(packageName).matches());
   ```
   
   `toPackageName` was evaluated inside the lambda, so it ran once per 
configured pattern. Now once per call.
   
   ## Testing
   
   This is the OGNL security gate, so per the ticket the equivalence is 
**asserted rather than argued**:
   
   - **`defaultPackageConditionMatchesLegacyAcrossClassShapes`** runs the 
replaced condition — frozen verbatim as `legacyDefaultPackageCondition`, which 
calls `getPackage()` directly and never delegates to production code — against 
the new one over the existing `classShapes()` matrix: arrays, primitives, 
`void`, a default-package class, a lambda and a JDK proxy.
   - **`testDefaultPackageAccessPermitsNamedPackageClass`** — a named-package 
class still passes the gate with `struts.disallowDefaultPackageAccess=true`.
   - **`testDefaultPackageAccessBlocksArrayTarget`** — an array target, the 
shape most likely to break the equivalence, stays blocked. The member declares 
in `java.lang`, so only the target branch can block.
   
   All three were mutation-checked rather than assumed non-vacuous:
   
   | mutation | fails |
   |---|---|
   | arrays/primitives resolve to `java.lang` | 
`defaultPackageConditionMatchesLegacyAcrossClassShapes`, 
`testDefaultPackageAccessBlocksArrayTarget` (plus the two WW-5674 equivalence 
tests) |
   | member-class condition inverted | 
`testDefaultPackageAccessPermitsNamedPackageClass` (plus the existing 
`testDefaultPackageExclusionSetting`) |
   
   `mvn test -DskipAssembly -pl core` — 3190 tests, 0 failures, 0 errors.
   
   ## Scope deliberately not taken
   
   - The `LOG.warn` calls in `checkExclusionList` also call `getPackage()`, but 
only on the deny path, so there is no hot-path value — and switching them would 
change the log text from `package java.io` to `java.io`.
   - Not threading a single package name through `isPackageExcluded` into both 
helpers. That would remove the last redundant call, but 
`isExcludedPackageNames`/`isExcludedPackageNamePatterns` are `protected` and 
subclasses may override them, so changing their signatures is source-breaking. 
WW-5678 owns that cleanup for 8.0.0. `toPackageName` is now a cached field read 
plus a branch, so calling it twice is negligible.
   
   ## Impact
   
   Neither path runs by default: `checkDefaultPackageAccess` only when 
`struts.disallowDefaultPackageAccess` is enabled, and the pattern loop only 
when `struts.excludedPackageNamePatterns` is configured — both pattern 
constants are commented out in `struts-excluded-classes.xml`. This is a 
consistency fix for deployments that do enable them, not where the WW-5667 9% 
lives; that was WW-5675.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)




Issue Time Tracking
-------------------

            Worklog Id:     (was: 1037283)
    Remaining Estimate: 0h
            Time Spent: 10m

> Remove the remaining redundant getPackage() lookups on the OGNL member-access 
> path
> ----------------------------------------------------------------------------------
>
>                 Key: WW-5677
>                 URL: https://issues.apache.org/jira/browse/WW-5677
>             Project: Struts 2
>          Issue Type: Sub-task
>            Reporter: Lukasz Lenart
>            Assignee: Lukasz Lenart
>            Priority: Major
>             Fix For: 7.4.0
>
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> Sub-task of WW-5667. Residual items found while reviewing WW-5674, which 
> optimised {{isClassBelongsToPackages}} and {{toPackageName}} but deliberately 
> left these two call sites alone as out of scope.
> Both sit on the same per-access hot path WW-5674 just optimised, within 
> twenty lines of it, so leaving them is a visible inconsistency.
> h2. 1. {{checkDefaultPackageAccess}} makes four {{getPackage()}} calls
> {{SecurityMemberAccess.checkDefaultPackageAccess}} still calls 
> {{clazz.getPackage()}} directly, twice per class and for up to two classes:
> {code:java}
> if (memberClass.getPackage() == null || 
> memberClass.getPackage().getName().isEmpty()) {
> {code}
> {{Class.getPackage()}} resolves through the defining classloader's package 
> map on every call. WW-5674 removed exactly this lookup from {{toPackageName}} 
> by switching to the cached {{Class.getPackageName()}}, so this method is now 
> the only remaining user of the slow form.
> The condition is equivalent to {{toPackageName(memberClass).isEmpty()}} — 
> including for arrays and primitives, where both forms treat the class as 
> being in the default package and therefore block. Replacing it would also 
> collapse the double {{getPackage()}} evaluation per class.
> Note that this method only runs when {{struts.disallowDefaultPackageAccess}} 
> is enabled, so the impact is limited to deployments that turn it on.
> h2. 2. {{isExcludedPackageNamePatterns}} recomputes the package name per 
> pattern
> {code:java}
> protected boolean isExcludedPackageNamePatterns(Class clazz) {
>     return excludedPackageNamePatterns.stream().anyMatch(pattern -> 
> pattern.matcher(toPackageName(clazz)).matches());
> }
> {code}
> {{toPackageName(clazz)}} is evaluated inside the lambda, so it runs once per 
> pattern rather than once per call. Hoisting it out of the stream is a 
> one-line change.
> The call is cheaper after WW-5674, but it is still N redundant calls. 
> {{struts.excludedPackageNamePatterns}} is empty by default (both pattern 
> constants are commented out in {{struts-excluded-classes.xml}}), so this only 
> affects deployments that configure it.
> h2. Constraints
> This is the OGNL security gate. Both changes must be behaviour-preserving, 
> and the equivalence of {{getPackage() == null || 
> getPackage().getName().isEmpty()}} with {{toPackageName(...).isEmpty()}} must 
> be asserted by test rather than argued — in particular for arrays, 
> primitives, {{void}} and default-package classes.
> {{SecurityMemberAccessPackageMatchingTest}} already provides the class-shape 
> matrix and the frozen {{legacyToPackageName}} reference oracle to build on.
> h2. Related
> {{ConfigParseUtil.validatePackageNames}} evaluating 
> {{Pattern.compile("\\s")}} once per package name rather than once overall is 
> a per-instantiation cost, not a per-access one, and is tracked on WW-5675 
> instead.



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

Reply via email to