From 23ff92793debd50fe89f973632a5e10472f2f169 Mon Sep 17 00:00:00 2001 From: Jesse Glick Date: Thu, 28 Sep 2023 13:53:46 -0400 Subject: [PATCH] `TracingRiverMarshallerFactory` to diagnose `StackOverflowError` deserializing `program.dat` --- .../pickles/serialization/RiverReader.java | 59 ++++++++++++++++- .../serialization/RiverReaderTest.java | 65 +++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReaderTest.java diff --git a/src/main/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReader.java b/src/main/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReader.java index 0e8a87f8..bd609750 100644 --- a/src/main/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReader.java +++ b/src/main/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReader.java @@ -57,6 +57,8 @@ import org.apache.commons.io.FileUtils; import org.jboss.marshalling.ByteInput; +import org.jboss.marshalling.reflect.SerializableClassRegistry; +import org.jboss.marshalling.river.RiverUnmarshaller; import org.jenkinsci.plugins.scriptsecurity.sandbox.Whitelist; import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovySandbox; import org.kohsuke.accmod.Restricted; @@ -151,7 +153,7 @@ public ListenableFuture restorePickles(Collection T sandbox(ReadSAM lambda) throws ClassNotFoundException, I } + /** + * Intercepts {@link StackOverflowError} and tries to record the chain of object classes which led to it. + */ + private static final class TracingRiverMarshallerFactory extends RiverMarshallerFactory { + @Override public Unmarshaller createUnmarshaller(MarshallingConfiguration configuration) throws IOException { + return new RiverUnmarshaller(this, SerializableClassRegistry.getInstance(), configuration) { + int depth = 0; + StackOverflowError err; + List trace = new ArrayList<>(); + @Override protected Object doReadNewObject(int streamClassType, boolean unshared, boolean discardMissing) throws ClassNotFoundException, IOException { + depth++; + Object o; + try { + o = super.doReadNewObject(streamClassType, unshared, discardMissing); + } catch (StackOverflowError x) { + // Will cause a StreamCorruptionError eventually, but not before we capture the parent objects: + o = null; + err = x; + } finally { + depth--; + } + if (o != null) { + trace.add(" ".repeat(depth) + o.getClass().getName()); + } + return o; + } + + @Override protected Object doReadObject(boolean unshared) throws ClassNotFoundException, IOException { + Object o; + try { + o = super.doReadObject(unshared); + } catch (ClassNotFoundException | IOException | RuntimeException | Error x) { + if (err != null) { + dumpTrace(); + x.addSuppressed(err); + } + throw x; + } + if (err != null) { + dumpTrace(); + throw err; + } + trace.clear(); + return o; + } + + void dumpTrace() { + for (String line : trace) { + LOGGER.log(Level.WARNING, "StackOverflowError trace: {0}", line); + } + } + }; + } + } + } diff --git a/src/test/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReaderTest.java b/src/test/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReaderTest.java new file mode 100644 index 00000000..0587d6c4 --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/workflow/support/pickles/serialization/RiverReaderTest.java @@ -0,0 +1,65 @@ +/* + * The MIT License + * + * Copyright 2023 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.workflow.support.pickles.serialization; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; + +import hudson.model.Result; +import java.util.logging.Level; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.BuildWatcher; +import org.jvnet.hudson.test.JenkinsSessionRule; +import org.jvnet.hudson.test.LoggerRule; + +public final class RiverReaderTest { + + @Rule public final JenkinsSessionRule rr = new JenkinsSessionRule(); + @Rule public final LoggerRule logging = new LoggerRule().record(RiverReader.class, Level.FINE).capture(2000); + @ClassRule public static final BuildWatcher bw = new BuildWatcher(); + + @Test public void stackOverflow() throws Throwable { + rr.then(r -> { + WorkflowJob p = r.createProject(WorkflowJob.class, "p"); + p.setDefinition(new CpsFlowDefinition("class R {Object f}; def x = new R(); for (int i = 0; i < 1000; i++) {def y = new R(); y.f = x; x = y}; semaphore 'wait'", true)); + SemaphoreStep.waitForStart("wait/1", p.scheduleBuild2(0).getStartCondition().get()); + }); + rr.then(r -> { + WorkflowRun b = r.jenkins.getItemByFullName("p", WorkflowJob.class).getBuildByNumber(1); + SemaphoreStep.success("wait/1", null); + r.assertBuildStatus(Result.FAILURE, r.waitForCompletion(b)); + r.assertLogContains("java.lang.StackOverflowError", b); + r.assertLogContains("at org.jboss.marshalling.river.RiverUnmarshaller.doReadNewObject", b); + assertThat(logging.getMessages(), hasItem("StackOverflowError trace: R")); + }); + } + +}