Skip to content

Commit 9d9b6f2

Browse files
jbachorikclaude
andcommitted
fix: address scholic-chorus review findings across agent, extension, and plugins
Applies 16 fixes surfaced by a multi-scholic review of the jb/configurations branch. Each item below was verified against current HEAD before editing. agent: - Main.loadEmbeddedProbe: close the probe InputStream via try-with-resources and stop returning true from a stub that installs nothing; log an operator warn so the probes= argument no longer silently succeeds. - Main.appendBootJar / appendSystemJar: synchronize the contains/appendTo/add block on the respective Set so two threads cannot both pass the dedup check and double-append the same jar. - Main libs deprecation: emit at log.warn (was hidden behind isDebugEnabled). - AgentManifestLibs.scanLibTree: close the Files.walk stream via try-with-resources (Files.walk is backed by a DirectoryStream). - AgentManifestLibs.locateAgentPath: broaden the catch to include IllegalArgumentException and FileSystemNotFoundException (both thrown by Paths.get(URI) for non-file code sources), and drop the dead Files.isDirectory/else branch that returned the same value in both arms. - AgentManifestLibs.filterAndNormalize: when toRealPath() fails, compare np against home.toAbsolutePath().normalize() instead of the raw home path so the fallback is at the same normalization level as np, and emit a warn so operators know symlink resolution was bypassed. - RemoteClient.reconnect: publish this.disconnecting=false last so the volatile write acts as a release fence for the prior sock/ois/oos writes. extension: - EmbeddedExtensionRepository.readManifestIndex: trim each id after splitting the BTrace-Embedded-Extensions manifest value so a human-written "ext1, ext2" no longer produces a leading-space id that isValidExtensionId rejects. - EmbeddedExtensionRepository.discoverBundledProbes: read the bundled probe list from the extension's properties file (probes=Probe1,Probe2) instead of always returning an empty list, so ExtensionDescriptorDTO actually carries the probes the extension declares. - MethodHandleCache: add a negative cache so a repeated lookup for a missing method fails fast via the cached LookupRuntimeException instead of re-entering the reflective machinery on every call. core: - ProbeConfiguration.setOutput: use toLowerCase(Locale.ROOT) instead of default-locale toLowerCase() so Turkish dotted/dotless-I does not make "JFR" / "FILE" fall through to IllegalArgumentException. maven plugin: - FatAgentMojo.execute: wrap the staging-directory lifetime in try-finally so deleteDirectory runs on every exit path, not just the happy one. gradle plugin: - BTraceFatAgentExtension Maven/File extractFromZip: switch from project.buildDir (deprecated in Gradle 7, removed in Gradle 9) to project.layout.buildDirectory.dir(...).get().asFile. housekeeping: - Remove the accidentally-committed top-level compile.log and add it to .gitignore so it cannot be reintroduced. Tests: full builds of btrace-core, btrace-extension, btrace-agent, btrace-maven-plugin, and btrace-gradle-plugin are green, including FatAgentMojoTest, AgentManifestLibsTest, SecurityValidationTest, MainTest, and ExtensionBridgeImplPolicyTest. The ClassDataLoader double-define race also surfaced in the review but the chosen fix is still under discussion (see inline PR comment on ClassDataLoader.findClass re: registerAsParallelCapable vs. synchronized block on getClassLoadingLock) and is deliberately not included in this commit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dbd3964 commit 9d9b6f2

10 files changed

Lines changed: 140 additions & 105 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ gradle.properties
4141
gradle-wrapper.properties
4242

4343
/.java.versions
44+
compile.log

btrace-agent/src/main/java/org/openjdk/btrace/agent/AgentManifestLibs.java

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.net.URI;
1111
import java.net.URISyntaxException;
1212
import java.net.URL;
13+
import java.nio.file.FileSystemNotFoundException;
1314
import java.nio.file.Files;
1415
import java.nio.file.Path;
1516
import java.nio.file.Paths;
@@ -122,12 +123,8 @@ private static Path locateAgentPath(Class<?> anchor) {
122123
URL url = anchor.getProtectionDomain().getCodeSource().getLocation();
123124
if (url == null) return null;
124125
URI uri = url.toURI();
125-
Path p = Paths.get(uri);
126-
if (Files.isDirectory(p)) {
127-
return p; // exploded
128-
}
129-
return p;
130-
} catch (URISyntaxException e) {
126+
return Paths.get(uri);
127+
} catch (URISyntaxException | IllegalArgumentException | FileSystemNotFoundException e) {
131128
if (log.isDebugEnabled()) log.debug("Failed to locate agent path: {}", e.toString());
132129
return null;
133130
}
@@ -168,10 +165,10 @@ static Path resolveEntry(String entry, Path baseDir) {
168165

169166
// Package-private for testing
170167
static void scanLibTree(Path root, Set<Path> out) {
171-
try {
172-
if (root == null || !Files.exists(root)) return;
173-
Files.walk(root)
174-
.filter(f -> Files.isRegularFile(f))
168+
if (root == null || !Files.exists(root)) return;
169+
try (java.util.stream.Stream<Path> stream = Files.walk(root)) {
170+
stream
171+
.filter(Files::isRegularFile)
175172
.filter(f -> f.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".jar"))
176173
.forEach(out::add);
177174
} catch (IOException e) {
@@ -231,12 +228,21 @@ static List<Path> filterAndNormalize(Set<Path> entries, Path home, boolean allow
231228
continue;
232229
}
233230
} catch (IOException e) {
234-
// toRealPath failed (file may not exist or path issue); fall back to non-canonical check
231+
// toRealPath failed (file may not exist or path issue); fall back to a
232+
// normalized non-canonical check. Use the same absolute+normalize pipeline as np
233+
// so the comparison is apples-to-apples, and surface the weakened check so operators
234+
// know symlink resolution could not be applied.
235235
if (log.isDebugEnabled()) log.debug("toRealPath failed for {}: {}", np, e.getMessage());
236-
if (!np.startsWith(home)) {
237-
log.warn("Rejecting manifest lib outside BTRACE_HOME: {}", np);
236+
Path normalizedHome = home.toAbsolutePath().normalize();
237+
if (!np.startsWith(normalizedHome)) {
238+
log.warn(
239+
"Rejecting manifest lib outside BTRACE_HOME (symlink resolution failed, using normalized path): {}",
240+
np);
238241
continue;
239242
}
243+
log.warn(
244+
"Symlink resolution failed for manifest lib {}; accepted against normalized BTRACE_HOME only",
245+
np);
240246
}
241247
}
242248
out.add(np);

btrace-agent/src/main/java/org/openjdk/btrace/agent/Main.java

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -443,37 +443,29 @@ private static void loadBundledProbes() {
443443

444444
/**
445445
* Load an embedded probe from classpath resources.
446+
*
447+
* <p>Embedded probe activation is not yet wired through {@code BTraceProbeFactory} and the
448+
* transformer pipeline, so this method currently verifies the resource exists and warns the
449+
* operator that the {@code probes=} argument is a no-op. It returns {@code false} so the caller
450+
* does not emit a misleading "Loaded bundled probe" info line.
446451
*/
447452
private static boolean loadEmbeddedProbe(String resourcePath, String probeName, boolean traceToStdOut) {
448-
try {
449-
InputStream is = Main.class.getClassLoader().getResourceAsStream(resourcePath);
453+
try (InputStream is = Main.class.getClassLoader().getResourceAsStream(resourcePath)) {
450454
if (is == null) {
451455
log.debug("Probe resource not found: {}", resourcePath);
452456
return false;
453457
}
454-
455458
byte[] probeBytes = readAllBytes(is);
456-
is.close();
457-
458-
// Create settings for this probe
459-
SharedSettings probeSettings = new SharedSettings();
460-
probeSettings.from(settings);
461-
probeSettings.setClientName(probeName);
462-
if (traceToStdOut) {
463-
probeSettings.setOutputFile("::stdout");
464-
}
465-
466-
ClientContext ctx = new ClientContext(inst, transformer, argMap, probeSettings);
467-
// Load as bytes - similar to how Client handles compiled probes
468-
// This is a simplified version; full implementation would use BTraceProbeFactory
469-
log.debug("Loaded probe bytes for: {} ({} bytes)", probeName, probeBytes.length);
470-
471-
// For now, log that probe loading from embedded resources is available
472-
// Full implementation would instantiate and activate the probe
473-
return true;
474-
475-
} catch (Exception e) {
476-
log.warn("Failed to load embedded probe {}: {}", probeName, e.getMessage());
459+
log.warn(
460+
"Embedded probe {} found at {} ({} bytes) but probe activation is not yet implemented; "
461+
+ "the 'probes=' agent argument is currently a no-op. Configure the probe via the "
462+
+ "client submit path instead.",
463+
probeName,
464+
resourcePath,
465+
probeBytes.length);
466+
return false;
467+
} catch (IOException e) {
468+
log.warn("Failed to read embedded probe {}: {}", probeName, e.getMessage());
477469
return false;
478470
}
479471
}
@@ -621,12 +613,10 @@ private static void parseArgs() {
621613

622614
String libs = argMap.get(LIBS);
623615
if (libs != null && !libs.isEmpty()) {
624-
if (log.isDebugEnabled()) {
625-
log.debug(
626-
"The 'libs' profile feature is deprecated and will be removed in a future release. "
627-
+ "Prefer packaging integrations as BTrace extensions (API on bootstrap, impl isolated). "
628-
+ "See docs/architecture/agent-manifest-libs.md for migration guidance.");
629-
}
616+
log.warn(
617+
"The 'libs' profile feature is deprecated and will be removed in a future release. "
618+
+ "Prefer packaging integrations as BTrace extensions (API on bootstrap, impl isolated). "
619+
+ "See docs/architecture/agent-manifest-libs.md for migration guidance.");
630620
}
631621
String config = argMap.get(CONFIG);
632622
processClasspaths(libs);
@@ -995,16 +985,19 @@ private static JarFile asJarFile(File path) throws IOException {
995985
private static void appendBootJar(Path jarPath) {
996986
try {
997987
Path rp = jarPath.toAbsolutePath().normalize();
998-
if (BOOT_ADDED.contains(rp)) {
999-
if (log.isDebugEnabled()) log.debug("Skipping duplicate bootstrap jar: {}", rp);
1000-
return;
1001-
}
1002988
if (!Files.exists(rp)) {
1003989
if (log.isDebugEnabled()) log.debug("Bootstrap jar does not exist: {}", rp);
1004990
return;
1005991
}
1006-
inst.appendToBootstrapClassLoaderSearch(asJarFile(rp.toFile()));
1007-
BOOT_ADDED.add(rp);
992+
// Synchronize the check-then-act so two threads cannot both pass the "not yet added"
993+
// check and both call appendToBootstrapClassLoaderSearch for the same jar.
994+
synchronized (BOOT_ADDED) {
995+
if (!BOOT_ADDED.add(rp)) {
996+
if (log.isDebugEnabled()) log.debug("Skipping duplicate bootstrap jar: {}", rp);
997+
return;
998+
}
999+
inst.appendToBootstrapClassLoaderSearch(asJarFile(rp.toFile()));
1000+
}
10081001
if (log.isDebugEnabled()) log.debug("Added to bootstrap: {}", rp);
10091002
} catch (IOException e) {
10101003
log.debug("Failed to append bootstrap jar {}: {}", jarPath, e.toString());
@@ -1014,16 +1007,17 @@ private static void appendBootJar(Path jarPath) {
10141007
private static void appendSystemJar(Path jarPath) {
10151008
try {
10161009
Path rp = jarPath.toAbsolutePath().normalize();
1017-
if (SYSTEM_ADDED.contains(rp)) {
1018-
if (log.isDebugEnabled()) log.debug("Skipping duplicate system jar: {}", rp);
1019-
return;
1020-
}
10211010
if (!Files.exists(rp)) {
10221011
if (log.isDebugEnabled()) log.debug("System jar does not exist: {}", rp);
10231012
return;
10241013
}
1025-
inst.appendToSystemClassLoaderSearch(asJarFile(rp.toFile()));
1026-
SYSTEM_ADDED.add(rp);
1014+
synchronized (SYSTEM_ADDED) {
1015+
if (!SYSTEM_ADDED.add(rp)) {
1016+
if (log.isDebugEnabled()) log.debug("Skipping duplicate system jar: {}", rp);
1017+
return;
1018+
}
1019+
inst.appendToSystemClassLoaderSearch(asJarFile(rp.toFile()));
1020+
}
10271021
if (log.isDebugEnabled()) log.debug("Added to system: {}", rp);
10281022
} catch (IOException e) {
10291023
log.debug("Failed to append system jar {}: {}", jarPath, e.toString());

btrace-agent/src/main/java/org/openjdk/btrace/agent/RemoteClient.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,13 @@ protected void closeAll() throws IOException {
391391
}
392392

393393
void reconnect(ObjectInputStream ois, ObjectOutputStream oos, Socket socket) throws IOException {
394-
this.disconnecting = false;
395394
this.sock = socket;
396395
this.ois = ois;
397396
this.oos = oos;
397+
// Publish last: the volatile write to `disconnecting` acts as a release fence so that any
398+
// thread observing disconnecting==false via isDisconnected() is guaranteed to also see
399+
// the updated socket triple above.
400+
this.disconnecting = false;
398401
onCommand(Command.NULL);
399402
}
400403
}

btrace-core/src/main/java/org/openjdk/btrace/core/extensions/ProbeConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.Collections;
66
import java.util.HashMap;
77
import java.util.List;
8+
import java.util.Locale;
89
import java.util.Map;
910

1011
/**
@@ -65,7 +66,7 @@ public ProbeConfiguration setOutput(String output) {
6566
if (output == null || output.isEmpty()) {
6667
return this;
6768
}
68-
switch (output.toLowerCase()) {
69+
switch (output.toLowerCase(Locale.ROOT)) {
6970
case "jfr":
7071
this.output = Output.JFR;
7172
break;

btrace-extension/src/main/java/org/openjdk/btrace/extension/impl/EmbeddedExtensionRepository.java

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,14 @@ private List<String> readManifestIndex() {
137137
Attributes attrs = manifest.getMainAttributes();
138138
String embeddedExtensions = attrs.getValue(MANIFEST_ATTR);
139139
if (embeddedExtensions != null && !embeddedExtensions.trim().isEmpty()) {
140-
return Arrays.asList(embeddedExtensions.split(","));
140+
List<String> ids = new ArrayList<>();
141+
for (String entry : embeddedExtensions.split(",")) {
142+
String trimmed = entry.trim();
143+
if (!trimmed.isEmpty()) {
144+
ids.add(trimmed);
145+
}
146+
}
147+
return ids;
141148
}
142149
}
143150
}
@@ -186,8 +193,8 @@ private ExtensionDescriptorDTO parseEmbeddedExtension(String extensionId) {
186193
configurator = null;
187194
}
188195

189-
// Discover bundled probes
190-
List<String> bundledProbes = discoverBundledProbes(extensionId);
196+
// Discover bundled probes (declared via the 'probes' property)
197+
List<String> bundledProbes = discoverBundledProbes(extensionId, props);
191198

192199
// For embedded extensions, jarPath points to a virtual path
193200
// The actual loading happens via ClassDataLoader
@@ -214,13 +221,23 @@ private ExtensionDescriptorDTO parseEmbeddedExtension(String extensionId) {
214221
}
215222

216223
/**
217-
* Discovers bundled probe class files in the extension's probes/ directory.
224+
* Discovers bundled probe class names for an extension by reading the {@code probes}
225+
* property from its {@code extension.properties}. The value is a comma-separated list
226+
* of fully-qualified probe class names; entries are trimmed and validated.
218227
*/
219-
private List<String> discoverBundledProbes(String extensionId) {
220-
// Note: Discovering resources without filesystem access is tricky.
221-
// The probes list should be declared in extension.properties instead.
222-
// For now, return empty and rely on extension.properties "probes" property.
223-
return Collections.emptyList();
228+
private List<String> discoverBundledProbes(String extensionId, Properties props) {
229+
String probesStr = props.getProperty("probes", "");
230+
if (probesStr.isEmpty()) {
231+
return Collections.emptyList();
232+
}
233+
List<String> raw = new ArrayList<>();
234+
for (String entry : probesStr.split(",")) {
235+
String trimmed = entry.trim();
236+
if (!trimmed.isEmpty()) {
237+
raw.add(trimmed);
238+
}
239+
}
240+
return validateClassNames(raw, "probe");
224241
}
225242

226243
@Override

btrace-extension/src/main/java/org/openjdk/btrace/extension/util/MethodHandleCache.java

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
public final class MethodHandleCache {
1414
private final ConcurrentHashMap<Key, MethodHandle> cache = new ConcurrentHashMap<>();
15+
private final ConcurrentHashMap<Key, LookupRuntimeException> negativeCache = new ConcurrentHashMap<>();
1516
private final MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
1617

1718
public MethodHandleCache() {}
@@ -20,30 +21,44 @@ public MethodHandle findVirtual(Class<?> receiver, String name, Class<?> rtype,
2021
throws NoSuchMethodException, IllegalAccessException {
2122
MethodType mt = MethodType.methodType(rtype, ptypes);
2223
Key k = Key.of(receiver, name, mt, false);
23-
return cache.computeIfAbsent(
24-
k,
25-
key -> {
26-
try {
27-
return publicLookup.findVirtual(receiver, name, mt);
28-
} catch (NoSuchMethodException | IllegalAccessException e) {
29-
throw new LookupRuntimeException(e);
30-
}
31-
});
24+
LookupRuntimeException cachedFailure = negativeCache.get(k);
25+
if (cachedFailure != null) throw cachedFailure;
26+
try {
27+
return cache.computeIfAbsent(
28+
k,
29+
key -> {
30+
try {
31+
return publicLookup.findVirtual(receiver, name, mt);
32+
} catch (NoSuchMethodException | IllegalAccessException e) {
33+
throw new LookupRuntimeException(e);
34+
}
35+
});
36+
} catch (LookupRuntimeException e) {
37+
negativeCache.putIfAbsent(k, e);
38+
throw e;
39+
}
3240
}
3341

3442
public MethodHandle findStatic(Class<?> owner, String name, Class<?> rtype, Class<?>... ptypes)
3543
throws NoSuchMethodException, IllegalAccessException {
3644
MethodType mt = MethodType.methodType(rtype, ptypes);
3745
Key k = Key.of(owner, name, mt, true);
38-
return cache.computeIfAbsent(
39-
k,
40-
key -> {
41-
try {
42-
return publicLookup.findStatic(owner, name, mt);
43-
} catch (NoSuchMethodException | IllegalAccessException e) {
44-
throw new LookupRuntimeException(e);
45-
}
46-
});
46+
LookupRuntimeException cachedFailure = negativeCache.get(k);
47+
if (cachedFailure != null) throw cachedFailure;
48+
try {
49+
return cache.computeIfAbsent(
50+
k,
51+
key -> {
52+
try {
53+
return publicLookup.findStatic(owner, name, mt);
54+
} catch (NoSuchMethodException | IllegalAccessException e) {
55+
throw new LookupRuntimeException(e);
56+
}
57+
});
58+
} catch (LookupRuntimeException e) {
59+
negativeCache.putIfAbsent(k, e);
60+
throw e;
61+
}
4762
}
4863

4964
public static final class LookupRuntimeException extends RuntimeException {

btrace-gradle-plugin/src/main/groovy/org/openjdk/btrace/gradle/BTraceFatAgentExtension.groovy

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,10 @@ class MavenExtensionSource extends ExtensionSource {
351351
}
352352

353353
private ResolvedExtension extractFromZip(File zipFile) {
354-
def tempDir = new File(project.buildDir, "fat-agent-staging/maven/${coordinates.replace(':', '_')}")
354+
def tempDir = project.layout.buildDirectory
355+
.dir("fat-agent-staging/maven/${coordinates.replace(':', '_')}")
356+
.get()
357+
.asFile
355358
tempDir.mkdirs()
356359

357360
// Extract ZIP
@@ -417,7 +420,10 @@ class FileExtensionSource extends ExtensionSource {
417420
}
418421

419422
private ResolvedExtension extractFromZip(File zipFile) {
420-
def tempDir = new File(project.buildDir, "fat-agent-staging/file/${zipFile.name.replace('.zip', '')}")
423+
def tempDir = project.layout.buildDirectory
424+
.dir("fat-agent-staging/file/${zipFile.name.replace('.zip', '')}")
425+
.get()
426+
.asFile
421427
tempDir.mkdirs()
422428

423429
project.copy {

0 commit comments

Comments
 (0)