Skip to content

Commit 858f3c9

Browse files
committed
1 parent f9d98b0 commit 858f3c9

File tree

5 files changed

+208
-6
lines changed

5 files changed

+208
-6
lines changed

src/main/java/hudson/remoting/Channel.java

+43-5
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@
4343
import java.net.URL;
4444
import java.nio.channels.ClosedChannelException;
4545
import java.nio.charset.StandardCharsets;
46+
import java.util.Arrays;
4647
import java.util.Collections;
4748
import java.util.Date;
49+
import java.util.HashSet;
4850
import java.util.List;
4951
import java.util.Locale;
5052
import java.util.Map;
53+
import java.util.Set;
5154
import java.util.WeakHashMap;
5255
import java.util.concurrent.ConcurrentHashMap;
5356
import java.util.concurrent.CopyOnWriteArrayList;
@@ -993,16 +996,51 @@ public boolean preloadJar(Callable<?, ?> classLoaderRef, Class<?>... classesInJa
993996
return preloadJar(UserRequest.getClassLoader(classLoaderRef), classesInJar);
994997
}
995998

999+
@SuppressFBWarnings(
1000+
value = "DMI_COLLECTION_OF_URLS",
1001+
justification = "All URLs point to local files, so no DNS lookup.")
9961002
public boolean preloadJar(ClassLoader local, Class<?>... classesInJar) throws IOException, InterruptedException {
997-
URL[] jars = new URL[classesInJar.length];
998-
for (int i = 0; i < classesInJar.length; i++) {
999-
jars[i] = Which.jarFile(classesInJar[i]).toURI().toURL();
1003+
Set<URL> jarSet = new HashSet<>();
1004+
for (Class<?> clazz : classesInJar) {
1005+
jarSet.add(Which.jarFile(clazz).toURI().toURL());
10001006
}
1001-
return call(new PreloadJarTask(jars, local));
1007+
URL[] jars = jarSet.toArray(new URL[0]);
1008+
return preloadJar(local, jars);
10021009
}
10031010

1011+
@SuppressFBWarnings(value = "URLCONNECTION_SSRF_FD", justification = "Callers are privileged controller-side code.")
10041012
public boolean preloadJar(ClassLoader local, URL... jars) throws IOException, InterruptedException {
1005-
return call(new PreloadJarTask(jars, local));
1013+
byte[][] contents = new byte[jars.length][0];
1014+
1015+
List<URL> jarList = Arrays.asList(jars);
1016+
for (int i = 0; i < jarList.size(); i++) {
1017+
final URL url = jarList.get(i);
1018+
jars[i] = url;
1019+
contents[i] = Util.readFully(url.openStream());
1020+
}
1021+
try {
1022+
return call(new PreloadJarTask2(jars, contents, local));
1023+
} catch (IOException ex) {
1024+
if (ex.getCause() instanceof IllegalAccessError) {
1025+
logger.log(
1026+
Level.FINE,
1027+
ex,
1028+
() -> "Failed to call PreloadJarTask2 on " + this + ", retrying with PreloadJarTask");
1029+
// When the agent is running an outdated version of remoting, we cannot access nonpublic classes in the
1030+
// same package, as PreloadJarTask2 would be loaded from the controller, and hence a different module/
1031+
// classloader, than the rest of remoting. As a result PreloadJarTask2 will throw IllegalAccessError:
1032+
//
1033+
// java.lang.IllegalAccessError: failed to access class hudson.remoting.RemoteClassLoader from class
1034+
// hudson.remoting.PreloadJarTask2 (hudson.remoting.RemoteClassLoader is in unnamed module of loader
1035+
// 'app'; hudson.remoting.PreloadJarTask2 is in unnamed module of loader 'Jenkins v${project.version}'
1036+
// @795f104a)
1037+
//
1038+
// Identify this error here and fall back to PreloadJarTask, relying on the restrictive controller-side
1039+
// implementation of IClassLoader#fetchJar.
1040+
return call(new PreloadJarTask(jars, local));
1041+
}
1042+
throw ex;
1043+
}
10061044
}
10071045

10081046
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package hudson.remoting;
2+
3+
import java.io.IOException;
4+
import java.net.URL;
5+
6+
/**
7+
* Validate a URL attempted to be read by the remote end (agent side).
8+
*
9+
* @deprecated Do not use, intended as a temporary workaround only.
10+
*/
11+
// TODO Remove once we no longer require compatibility with remoting before 2024-08.
12+
@Deprecated
13+
public interface JarURLValidator {
14+
void validate(URL url) throws IOException;
15+
}

src/main/java/hudson/remoting/PreloadJarTask.java

+2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
* {@link Callable} used to deliver a jar file to {@link RemoteClassLoader}.
3434
*
3535
* @author Kohsuke Kawaguchi
36+
* @deprecated Retained for compatibility with pre-2024-08 remoting only (see {@link Channel#preloadJar(ClassLoader, java.net.URL...)}), use {@link hudson.remoting.PreloadJarTask2}.
3637
*/
38+
@Deprecated
3739
final class PreloadJarTask implements DelegatingCallable<Boolean, IOException> {
3840
/**
3941
* Jar file to be preloaded.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
package hudson.remoting;
25+
26+
import edu.umd.cs.findbugs.annotations.CheckForNull;
27+
import java.io.IOException;
28+
import java.net.URL;
29+
import org.jenkinsci.remoting.Role;
30+
import org.jenkinsci.remoting.RoleChecker;
31+
32+
/**
33+
* {@link Callable} used to deliver a jar file to {@link RemoteClassLoader}.
34+
* <p>
35+
* This replaces {@link hudson.remoting.PreloadJarTask} and delivers the jar contents as part of the Callable rather
36+
* than needing to call {@link hudson.remoting.RemoteClassLoader#prefetch(java.net.URL)}.
37+
* </p>
38+
* @since TODO 2024-08
39+
*/
40+
final class PreloadJarTask2 implements DelegatingCallable<Boolean, IOException> {
41+
/**
42+
* Jar file to be preloaded.
43+
*/
44+
private final URL[] jars;
45+
46+
private final byte[][] contents;
47+
48+
// TODO: This implementation exists starting from
49+
// https://github.com/jenkinsci/remoting/commit/f3d0a81fdf46a10c3c6193faf252efaeaee98823
50+
// Since this time nothing has blown up, but it still seems to be suspicious.
51+
// The solution for null classloaders is available in RemoteDiagnostics.Script#call() in the Jenkins core codebase
52+
@CheckForNull
53+
private transient ClassLoader target = null;
54+
55+
PreloadJarTask2(URL[] jars, byte[][] contents, @CheckForNull ClassLoader target) {
56+
if (jars.length != contents.length) {
57+
throw new IllegalArgumentException("Got " + jars.length + " jars and " + contents.length + " contents");
58+
}
59+
this.jars = jars;
60+
this.contents = contents;
61+
this.target = target;
62+
}
63+
64+
@Override
65+
public ClassLoader getClassLoader() {
66+
return target;
67+
}
68+
69+
@Override
70+
public Boolean call() throws IOException {
71+
ClassLoader cl = Thread.currentThread().getContextClassLoader();
72+
73+
try {
74+
if (!(cl instanceof RemoteClassLoader)) {
75+
return false;
76+
}
77+
final RemoteClassLoader rcl = (RemoteClassLoader) cl;
78+
79+
boolean r = false;
80+
for (int i = 0; i < jars.length; i++) {
81+
r |= rcl.prefetch(jars[i], contents[i]);
82+
}
83+
return r;
84+
} catch (IllegalAccessError iae) {
85+
// Catch the IAE instead of letting it be wrapped by remoting to suppress warnings logged on the agent-side
86+
throw new IOException(iae);
87+
}
88+
}
89+
90+
/**
91+
* This task is only useful in the context that allows remote classloading, and by that point
92+
* any access control check is pointless. So just declare the worst possible role.
93+
*/
94+
@Override
95+
public void checkRoles(RoleChecker checker) throws SecurityException {
96+
checker.check(this, Role.UNKNOWN);
97+
}
98+
99+
private static final long serialVersionUID = -773448303394727271L;
100+
}

src/main/java/hudson/remoting/RemoteClassLoader.java

+48-1
Original file line numberDiff line numberDiff line change
@@ -681,8 +681,12 @@ public static void deleteDirectoryOnExit(File dir) {
681681
* @param jar Jar to be prefetched. Note that this file is an file on the other end,
682682
* and doesn't point to anything meaningful locally.
683683
* @return true if the prefetch happened. false if the jar is already prefetched.
684+
* @deprecated Only left in for compatibility with pre-2024-08 remoting. Use {@link #prefetch(java.net.URL, byte[])} instead.
684685
* @see Channel#preloadJar(Callable, Class[])
686+
* @see hudson.remoting.PreloadJarTask
687+
* @see hudson.remoting.PreloadJarTask2
685688
*/
689+
@Deprecated
686690
/*package*/ boolean prefetch(URL jar) throws IOException {
687691
synchronized (prefetchedJars) {
688692
if (prefetchedJars.contains(jar)) {
@@ -698,6 +702,31 @@ public static void deleteDirectoryOnExit(File dir) {
698702
}
699703
}
700704

705+
/**
706+
* Prefetches the specified jar with the specified content into this classloader.
707+
* @param jar Jar to be prefetched. Note that this file is an file on the other end,
708+
* and doesn't point to anything meaningful locally.
709+
* @param content the jar content
710+
* @return true if the prefetch happened. false if the jar is already prefetched.
711+
* @see Channel#preloadJar(Callable, Class[])
712+
* @see hudson.remoting.PreloadJarTask2
713+
* @since TODO 2024-08
714+
*/
715+
/*package*/ boolean prefetch(URL jar, byte[] content) throws IOException {
716+
synchronized (prefetchedJars) {
717+
if (prefetchedJars.contains(jar)) {
718+
return false;
719+
}
720+
721+
String p = jar.getPath().replace('\\', '/');
722+
p = Util.getBaseName(p);
723+
File localJar = Util.makeResource(p, content);
724+
addURL(localJar.toURI().toURL());
725+
prefetchedJars.add(jar);
726+
return true;
727+
}
728+
}
729+
701730
/**
702731
* Receiver-side of {@link ClassFile2} uses this to remember the prefetch information.
703732
*/
@@ -844,6 +873,7 @@ public static class ClassFile2 extends ResourceFile {
844873
* Remoting interface.
845874
*/
846875
public interface IClassLoader {
876+
@Deprecated
847877
byte[] fetchJar(URL url) throws IOException;
848878

849879
/**
@@ -971,8 +1001,25 @@ public ClassLoaderProxy(@NonNull ClassLoader cl, Channel channel) {
9711001
@Override
9721002
@SuppressFBWarnings(
9731003
value = "URLCONNECTION_SSRF_FD",
974-
justification = "This is only used for managing the jar cache as files.")
1004+
justification = "URL validation is being done through JarURLValidator")
9751005
public byte[] fetchJar(URL url) throws IOException {
1006+
final Object o = channel.getProperty(JarURLValidator.class);
1007+
if (o == null) {
1008+
final boolean disabled = Boolean.getBoolean(Channel.class.getName() + ".DISABLE_JAR_URL_VALIDATOR");
1009+
LOGGER.log(Level.FINE, "Default behavior for URL: " + url + " with disabled flag: " + disabled);
1010+
if (!disabled) {
1011+
throw new IOException(
1012+
"No hudson.remoting.JarURLValidator has been set for this channel, so all #fetchJar calls are rejected."
1013+
+ " This is likely a bug in Jenkins."
1014+
+ " As a workaround, try updating the agent.jar file.");
1015+
}
1016+
} else {
1017+
if (o instanceof JarURLValidator) {
1018+
((JarURLValidator) o).validate(url);
1019+
} else {
1020+
throw new IOException("Unexpected channel property hudson.remoting.JarURLValidator value: " + o);
1021+
}
1022+
}
9761023
return Util.readFully(url.openStream());
9771024
}
9781025

0 commit comments

Comments
 (0)