Skip to content

fix(boot): handle Spring Session non-public request wrappers#731

Open
vlsi wants to merge 6 commits into
Netcracker:mainfrom
vlsi:fix/profiler-spring-session-illegal-access
Open

fix(boot): handle Spring Session non-public request wrappers#731
vlsi wants to merge 6 commits into
Netcracker:mainfrom
vlsi:fix/profiler-spring-session-illegal-access

Conversation

@vlsi
Copy link
Copy Markdown
Collaborator

@vlsi vlsi commented May 20, 2026

Summary

  • Fix IllegalAccessException in HttpServletRequestAdapter (and 3 sibling adapters) against Spring Session's SessionRepositoryFilter$SessionRepositoryRequestWrapper — a private final inner class whose declaring-class modifiers fail Method.invoke access checks even though the methods themselves are public.
  • Add sample-apps/it-spring-boot-3 — a new Gradle module that boots a tiny Spring Boot 3 + Spring Session app and reproduces the production stacktrace verbatim. Reverting the boot/ change makes the test fail with the same IllegalAccessException at HttpServletRequestAdapter.java:30.
  • Infrastructure: build-logic.java-17-library convention; :sample-apps:* joins :plugins:* in the dependency-submission exclude list; renovate pins this module to Spring Boot 3.x (a major bump triggers a new it-spring-boot-4 sibling).

Test plan

  • ./gradlew --quiet :sample-apps:it-spring-boot-3:test — fails before the fix, passes after
  • ./gradlew --quiet :boot:test — passes (no regressions)

🤖 Generated with Claude Code

vlsi and others added 2 commits May 20, 2026 14:02
Spring Session wraps every incoming request in
SessionRepositoryFilter$SessionRepositoryRequestWrapper, a private final
inner class. Class.getMethod returns a Method whose declaringClass is
that wrapper, and Method.invoke performs a language access check on the
declaring class modifiers -- even when the method itself is public --
producing IllegalAccessException.

The same reflection pattern lives in three sibling adapters
(ServletRequestAdapter, HttpSessionAdapter, CookieAdapter), so they get
the same treatment. CookieAdapter additionally switches from
getDeclaredMethod to getMethod to look up inherited public accessors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…legalAccessException

The agent previously had no Spring Boot test coverage -- Maven samples
under backend/examples/ are deployment demos, not test fixtures. This
adds a new Gradle module under sample-apps/ that boots a tiny Spring
Boot 3 + Spring Session application and exercises
HttpServletRequestAdapter against the real
SessionRepositoryRequestWrapper. Reverting the previous commit makes
this test reproduce the production stacktrace verbatim.

Infrastructure changes that come with this:

- build-logic.java-17-library convention bumps toolchain and bytecode
  target to 17 (Spring Boot 3 requires Java 17)
- sample-apps/ joins :plugins as a directory excluded from the global
  buildscript constraints block (no parent build.gradle.kts)
- kotlin("plugin.spring") added to pluginManagement so @configuration
  classes can be subclassed by Spring's CGLIB proxies
- renovate.json pins this module to Spring Boot 3.x -- a major bump
  triggers a new it-spring-boot-4 sibling instead
- gradle-dependency-submit excludes :sample-apps:.* alongside
  :plugins:.* so sample apps don't generate security alerts for the
  agent project itself

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vlsi vlsi requested review from IldarMinaev and asatt as code owners May 20, 2026 11:02
vlsi and others added 4 commits May 20, 2026 14:46
testImplementation extends implementation, so the platform is inherited
automatically -- no need to bind it to a local val or repeat it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…trumentation on Undertow

The previous iteration of this module reproduced the contract by calling
HttpServletRequestAdapter directly from the controller. That worked but
left the agent itself untested -- the bytecode-injected reflection path
in production was never actually exercised.

This rewires the test to follow the exact production stacktrace:

- Switch the embedded server to Undertow (spring-boot-starter-undertow,
  excluding spring-boot-starter-tomcat). Tomcat's Tomcat10HTTPEnhancer
  only sees org.apache.catalina.connector.Request at the valve level
  -- never the Spring Session wrapper, which lives inside the filter
  chain. Undertow23HTTPEnhancer instead targets
  io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter
  with execute-before='fillNcUser$profiler(p1)' -- fired once per
  filter step, so it picks up the wrapper that
  SessionRepositoryFilter just installed.

- Drop the manual HttpServletRequestAdapter call from the controller.
  The /probe endpoint is now a plain Spring MVC handler.

- Attach the agent as a real -javaagent on the Gradle test JVM. The
  installer zip from :installer is extracted into build/profiler-home
  and the path is wired through jvmArgumentProviders (same pattern as
  installer-zip-test). projects.boot moves to testCompileOnly: at
  runtime boot.jar is loaded by the bootstrap classloader via the
  agent jar.

- The test swaps ProfilerData.pluginLogger with a capturing
  delegate before the request and asserts no IllegalAccessException
  was swallowed. The contract is: TomcatHTTPEnhancer/Undertow23HTTPEnhancer
  fillNcUser$profiler must not raise IllegalAccessException when
  HttpServletLogUtils.fillNcUser constructs HttpServletRequestAdapter
  against a non-public servlet wrapper.

Reverting setAccessible(true) in HttpServletRequestAdapter makes the
test fail with three captured IllegalAccessExceptions whose messages
match the production stacktrace verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…the Maven demo

Reuse backend/examples/spring-boot-3-undertow as the sample-app: same
source code that ships as a deployable demo doubles as the integration
test fixture. The Gradle Exec task invokes mvn package on the demo;
Testcontainers builds a throwaway eclipse-temurin:17-jre image bundling
the demo jar plus the extracted profiler installer, and starts the
container with -javaagent attached via JAVA_TOOL_OPTIONS. The test hits
/health and verifies that the agent's Undertow23 enhancer does not
swallow an IllegalAccessException while constructing
HttpServletRequestAdapter against SessionRepositoryRequestWrapper.

Companion changes:

- backend/examples/spring-boot-3-undertow:
  - Bump spring-boot-starter-parent 3.1.2 -> 3.5.0 so Spring Session's
    SessionRepositoryFilter is observed in the chain (in 3.1 the
    @EnableSpringHttpSession bean was not auto-registered as a servlet
    filter; bumping fixed that path).
  - Add spring-session-core dependency and SessionConfig with an
    explicit FilterRegistrationBean so the wrapper materialises
    deterministically regardless of which Spring Boot autoconfig
    bridges Spring Session in any given minor.

- boot/.../Profiler.java: add ECHO_PLUGIN_EXCEPTION_TO_STDERR_PROPERTY
  ("com.netcracker.profiler.agent.echoPluginExceptionToStderr"), a
  gated, default-off fail-loud diagnostic that mirrors every swallowed
  plugin exception to System.err. The slf4j/Logback binding for
  ProfilerPluginLoggerImpl is unreliable in containers where the agent
  loads Logback early (bootstrap classloader) and Spring Boot
  reconfigures Logback later (app classloader) — the two LoggerContexts
  are not guaranteed to share appender routing. This property gives the
  integration test (and field operators) a classloader-independent
  channel; isolating the agent's Logback context is left to a follow-up
  task.

- sample-apps/it-spring-boot-3:
  - Drop the previous Kotlin TestApp source set; the demo provides the
    application now.
  - Replace ProcessBuilder forking with Testcontainers
    (org.testcontainers:testcontainers-bom:1.21.0). Force docker-java's
    advertised API version to 1.43 to satisfy modern Docker daemons
    (OrbStack / Docker Desktop >= 25 reject the bundled 1.32 default).
  - The container's JAVA_TOOL_OPTIONS sets
    -Dcom.netcracker.profiler.agent.echoPluginExceptionToStderr=true so
    the test reads container stderr via withLogConsumer for the
    assertion.

Reverting setAccessible(true) on getSession in HttpServletRequestAdapter
makes the new test fail with three captured IllegalAccessException
entries, matching the production stacktrace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er-latest

The previous Testcontainers iteration built a throwaway eclipse-temurin
image and copied the unpacked profiler installer into it. That worked
but duplicated work the :installer:buildBaseImage task already does and
diverged from the production runtime layout.

Switch to FROM qubership/qubership-core-base-image:profiler-latest — the
same image shipped to customers — and just COPY the demo jar on top.
The base image already has /app/diag wired up; setting
NC_DIAGNOSTIC_MODE=prod makes its entrypoint set
JAVA_TOOL_OPTIONS=-javaagent:/app/diag/lib/agent.jar ... before exec'ing
the user CMD, so the agent attaches transparently.

Companion changes:
- Drop the local extractInstaller Sync task (the installer is now baked
  into the base image, not copied per test).
- Add a task dependency on :installer:buildBaseImage so the local image
  is rebuilt with the current agent jar when this test runs.
- Replace test.profilerHomeDir system property with test.baseImageTag.
- The fail-loud System.err diagnostic
  (-Dcom.netcracker.profiler.agent.echoPluginExceptionToStderr=true)
  is now passed via the CMD line (still picked up by the JVM together
  with the entrypoint-injected JAVA_TOOL_OPTIONS).

Reverting setAccessible(true) on getSession again makes the test fail
with 12 captured lines (Profiler plugin exception + first stack frame
for each of the 3 instrumented filter steps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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