Skip to content

WW-5677 Remove the remaining redundant getPackage() lookups on the OGNL member-access path - #1859

Merged
lukaszlenart merged 1 commit into
mainfrom
WW-5677-remove-residual-getpackage-lookups
Aug 23, 2026
Merged

WW-5677 Remove the remaining redundant getPackage() lookups on the OGNL member-access path#1859
lukaszlenart merged 1 commit into
mainfrom
WW-5677-remove-residual-getpackage-lookups

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Fixes 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

-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

-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

…ess path

WW-5674 replaced Class.getPackage() with the cached Class.getPackageName()
in 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.

checkDefaultPackageAccess still tested
`getPackage() == null || getPackage().getName().isEmpty()`, which resolves
through the defining classloader's package map twice per class, for up to
two classes per access. It now tests toPackageName(clazz).isEmpty(). The
two forms agree for every class shape: getPackage() is null for arrays,
primitives and void, and names the unnamed package with the empty string,
all of which toPackageName reports as empty.

isExcludedPackageNamePatterns evaluated toPackageName inside the lambda,
so it re-resolved the package name once per configured pattern. It is now
resolved once per call.

Both changes are behaviour-preserving. Per the ticket, the equivalence is
asserted rather than argued: defaultPackageConditionMatchesLegacyAcrossClassShapes
runs the replaced condition, frozen verbatim as an oracle that calls
getPackage() directly, against the new one over the existing class-shape
matrix -- arrays, primitives, void, a default-package class, a lambda and
a JDK proxy. Two behavioural tests cover the gate itself: a named-package
class still passes, and an array target, the shape most likely to break
the equivalence, stays blocked.

All three tests were mutation-checked. Resolving arrays to java.lang fails
the equivalence test and the array-target test; inverting the member-class
condition fails the named-package test.

Neither path runs by default -- checkDefaultPackageAccess only when
struts.disallowDefaultPackageAccess is enabled, and the pattern loop only
when struts.excludedPackageNamePatterns is configured, both commented out
in struts-excluded-classes.xml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@lukaszlenart
lukaszlenart merged commit 74d7ec0 into main Aug 23, 2026
13 of 14 checks passed
@lukaszlenart
lukaszlenart deleted the WW-5677-remove-residual-getpackage-lookups branch August 23, 2026 17:06
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.

1 participant