Skip to content

Commit 73237b7

Browse files
committed
[GR-36755] Prevent conflicting writes to configuration file directories.
PullRequest: graal/10998
2 parents c26d397 + 487f151 commit 73237b7

File tree

8 files changed

+169
-48
lines changed

8 files changed

+169
-48
lines changed

docs/reference-manual/native-image/Agent.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,6 @@ This tool must first be built with:
236236
native-image --macro:native-image-configure-launcher
237237
```
238238

239-
> Note: The Native Image Configure Tool is only available if [`native-image` is built via `mx`](https://github.com/oracle/graal/blob/master/substratevm/SubstrateVM.md). This configuration tool is not part of any GraalVM distribution by default.
240-
241239
Then, the tool can be used to merge sets of configuration files as follows:
242240
```shell
243241
native-image-configure generate --input-dir=/path/to/config-dir-0/ --input-dir=/path/to/config-dir-1/ --output-dir=/path/to/merged-config-dir/

substratevm/src/com.oracle.svm.agent/src/com/oracle/svm/agent/NativeImageAgent.java

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@
2727
import java.io.FileNotFoundException;
2828
import java.io.IOException;
2929
import java.nio.file.AtomicMoveNotSupportedException;
30+
import java.nio.file.FileAlreadyExistsException;
3031
import java.nio.file.Files;
3132
import java.nio.file.NoSuchFileException;
3233
import java.nio.file.Path;
3334
import java.nio.file.Paths;
3435
import java.nio.file.StandardCopyOption;
36+
import java.nio.file.StandardOpenOption;
37+
import java.nio.file.attribute.FileTime;
3538
import java.text.DateFormat;
3639
import java.text.SimpleDateFormat;
3740
import java.util.ArrayList;
3841
import java.util.Arrays;
42+
import java.util.ConcurrentModificationException;
3943
import java.util.Date;
4044
import java.util.List;
4145
import java.util.TimeZone;
@@ -89,6 +93,8 @@ public final class NativeImageAgent extends JvmtiAgentBase<NativeImageAgentJNIHa
8993
private TracingResultWriter tracingResultWriter;
9094

9195
private Path configOutputDirPath;
96+
private Path configOutputLockFilePath;
97+
private FileTime expectedConfigModifiedBefore;
9298

9399
private static String getTokenValue(String token) {
94100
return token.substring(token.indexOf('=') + 1);
@@ -244,9 +250,24 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
244250
return usage(1, "can only once specify exactly one of trace-output=, config-output-dir= or config-merge-dir=.");
245251
}
246252
try {
247-
configOutputDirPath = Paths.get(configOutputDir);
248-
if (!Files.exists(configOutputDirPath)) {
249-
Files.createDirectories(configOutputDirPath);
253+
configOutputDirPath = Files.createDirectories(Path.of(configOutputDir));
254+
configOutputLockFilePath = configOutputDirPath.resolve(ConfigurationFile.LOCK_FILE_NAME);
255+
try {
256+
Files.writeString(configOutputLockFilePath, Long.toString(ProcessProperties.getProcessID()),
257+
StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
258+
} catch (FileAlreadyExistsException e) {
259+
String process;
260+
try {
261+
process = Files.readString(configOutputLockFilePath).stripTrailing();
262+
} catch (Exception ignored) {
263+
process = "(unknown)";
264+
}
265+
return error(2, "Output directory '" + configOutputDirPath + "' is locked by process " + process + ", " +
266+
"which means another agent instance is already writing to this directory. " +
267+
"Only one agent instance can safely write to a specific target directory at the same time. " +
268+
"Unless file '" + ConfigurationFile.LOCK_FILE_NAME + "' is a leftover from an earlier process that terminated abruptly, it is unsafe to delete it. " +
269+
"For running multiple processes with agents at the same time to create a single configuration, read Agent.md " +
270+
"or https://www.graalvm.org/reference-manual/native-image/Agent/ on how to use the native-image-configure tool.");
250271
}
251272
if (experimentalOmitClasspathConfig) {
252273
ignoreConfigFromClasspath(jvmti, omittedConfigs);
@@ -265,12 +286,12 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
265286
shouldExcludeClassesWithHash = omittedConfigProcessor.getPredefinedClassesConfiguration()::containsClassWithHash;
266287
}
267288

268-
Path[] predefinedClassDestinationDirs = {configOutputDirPath.resolve(ConfigurationFile.PREDEFINED_CLASSES_AGENT_EXTRACTED_SUBDIR)};
269289
if (configurationWithOrigins) {
270290
ConfigurationWithOriginsResultWriter writer = new ConfigurationWithOriginsResultWriter(advisor, recordKeeper);
271291
tracer = writer;
272292
tracingResultWriter = writer;
273293
} else {
294+
Path[] predefinedClassDestDirs = {Files.createDirectories(configOutputDirPath.resolve(ConfigurationFile.PREDEFINED_CLASSES_AGENT_EXTRACTED_SUBDIR))};
274295
Function<IOException, Exception> handler = e -> {
275296
if (e instanceof NoSuchFileException) {
276297
warn("file " + ((NoSuchFileException) e).getFile() + " for merging could not be found, skipping");
@@ -283,11 +304,12 @@ protected int onLoadCallback(JNIJavaVM vm, JvmtiEnv jvmti, JvmtiEventCallbacks c
283304
};
284305
TraceProcessor processor = new TraceProcessor(advisor, mergeConfigs.loadJniConfig(handler), mergeConfigs.loadReflectConfig(handler),
285306
mergeConfigs.loadProxyConfig(handler), mergeConfigs.loadResourceConfig(handler), mergeConfigs.loadSerializationConfig(handler),
286-
mergeConfigs.loadPredefinedClassesConfig(predefinedClassDestinationDirs, shouldExcludeClassesWithHash, handler), omittedConfigProcessor);
307+
mergeConfigs.loadPredefinedClassesConfig(predefinedClassDestDirs, shouldExcludeClassesWithHash, handler), omittedConfigProcessor);
287308
ConfigurationResultWriter writer = new ConfigurationResultWriter(processor);
288309
tracer = writer;
289310
tracingResultWriter = writer;
290311
}
312+
expectedConfigModifiedBefore = getMostRecentlyModified(configOutputDirPath, getMostRecentlyModified(configOutputLockFilePath, null));
291313
} catch (Throwable t) {
292314
return error(2, t.toString());
293315
}
@@ -342,7 +364,7 @@ private static <T> T error(T result, String message) {
342364
private static <T> T usage(T result, String message) {
343365
inform(message);
344366
inform("Example usage: -agentlib:native-image-agent=config-output-dir=/path/to/config-dir/");
345-
inform("For details, please read BuildConfiguration.md or https://www.graalvm.org/reference-manual/native-image/BuildConfiguration/");
367+
inform("For details, please read Agent.md or https://www.graalvm.org/reference-manual/native-image/Agent/");
346368
return result;
347369
}
348370

@@ -505,20 +527,39 @@ protected void onVMDeathCallback(JvmtiEnv jvmti, JNIEnvironment jni) {
505527

506528
private static final int MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES = 5;
507529
private static int currentFailuresWritingConfigs = 0;
530+
private static int currentFailuresModifiedTargetDirectory = 0;
508531

509532
private void writeConfigurationFiles() {
533+
Path tempDirectory = null;
510534
try {
511-
final Path tempDirectory = configOutputDirPath.toFile().exists()
512-
? Files.createTempDirectory(configOutputDirPath, "tempConfig-")
513-
: Files.createTempDirectory("tempConfig-");
514-
List<Path> writtenFilePaths = tracingResultWriter.writeToDirectory(tempDirectory);
515-
516-
for (Path writtenFilePath : writtenFilePaths) {
517-
Path fileName = tempDirectory.relativize(writtenFilePath);
518-
Path target = configOutputDirPath.resolve(fileName);
519-
tryAtomicMove(writtenFilePath, target);
535+
FileTime mostRecent = getMostRecentlyModified(configOutputDirPath, expectedConfigModifiedBefore);
536+
537+
// Write files first before failing any modification checks
538+
tempDirectory = Files.createTempDirectory(configOutputDirPath, transformPath("agent-pid{pid}-{datetime}.tmp"));
539+
List<Path> tempFilePaths = tracingResultWriter.writeToDirectory(tempDirectory);
540+
541+
if (!Files.exists(configOutputLockFilePath)) {
542+
throw unexpectedlyModified(configOutputLockFilePath);
543+
}
544+
expectUnmodified(configOutputLockFilePath);
545+
if (!mostRecent.equals(expectedConfigModifiedBefore)) {
546+
throw unexpectedlyModified(configOutputDirPath);
547+
}
548+
549+
Path[] targetFilePaths = new Path[tempFilePaths.size()];
550+
for (int i = 0; i < tempFilePaths.size(); i++) {
551+
Path fileName = tempDirectory.relativize(tempFilePaths.get(i));
552+
targetFilePaths[i] = configOutputDirPath.resolve(fileName);
553+
expectUnmodified(targetFilePaths[i]);
520554
}
521555

556+
for (int i = 0; i < tempFilePaths.size(); i++) {
557+
tryAtomicMove(tempFilePaths.get(i), targetFilePaths[i]);
558+
mostRecent = getMostRecentlyModified(targetFilePaths[i], mostRecent);
559+
}
560+
mostRecent = getMostRecentlyModified(configOutputDirPath, mostRecent);
561+
expectedConfigModifiedBefore = mostRecent;
562+
522563
/*
523564
* Note that sidecar files may be written directly to the final output directory, such
524565
* as the class files from predefined class tracking. However, such files generally
@@ -527,8 +568,39 @@ private void writeConfigurationFiles() {
527568

528569
compulsoryDelete(tempDirectory);
529570
} catch (IOException e) {
530-
warnUpToLimit(currentFailuresWritingConfigs++, MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES, "Error when writing configuration files: " + e.toString());
571+
warnUpToLimit(currentFailuresWritingConfigs++, MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES, "Error when writing configuration files: " + e);
572+
} catch (ConcurrentModificationException e) {
573+
warnUpToLimit(currentFailuresModifiedTargetDirectory++, MAX_WARNINGS_FOR_WRITING_CONFIGS_FAILURES,
574+
"file or directory '" + e.getMessage() + "' has been modified by another process. " +
575+
"All output files remain in the temporary directory '" + configOutputDirPath.resolve("..").relativize(tempDirectory) + "'. " +
576+
"Ensure that only one agent instance and no other processes are writing to the output directory '" + configOutputDirPath + "' at the same time. " +
577+
"For running multiple processes with agents at the same time to create a single configuration, read Agent.md " +
578+
"or https://www.graalvm.org/reference-manual/native-image/Agent/ on how to use the native-image-configure tool.");
579+
}
580+
}
581+
582+
private void expectUnmodified(Path path) {
583+
try {
584+
if (Files.getLastModifiedTime(path).compareTo(expectedConfigModifiedBefore) > 0) {
585+
throw unexpectedlyModified(path);
586+
}
587+
} catch (IOException ignored) {
588+
// best effort
589+
}
590+
}
591+
592+
private static ConcurrentModificationException unexpectedlyModified(Path path) {
593+
throw new ConcurrentModificationException(path.getFileName().toString());
594+
}
595+
596+
private static FileTime getMostRecentlyModified(Path path, FileTime other) {
597+
FileTime modified;
598+
try {
599+
modified = Files.getLastModifiedTime(path);
600+
} catch (IOException ignored) {
601+
return other; // best effort
531602
}
603+
return (other == null || other.compareTo(modified) < 0) ? modified : other;
532604
}
533605

534606
private static void compulsoryDelete(Path pathToDelete) {
@@ -559,7 +631,7 @@ private static void tryAtomicMove(final Path source, final Path target) throws I
559631
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
560632
} catch (AtomicMoveNotSupportedException e) {
561633
warnUpToLimit(currentFailuresAtomicMove++, MAX_FAILURES_ATOMIC_MOVE,
562-
String.format("Could not move temporary configuration profile from (%s) to (%s) atomically. " +
634+
String.format("Could not move temporary configuration profile from '%s' to '%s' atomically. " +
563635
"This might result in inconsistencies.", source.toAbsolutePath(), target.toAbsolutePath()));
564636
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
565637
}
@@ -585,6 +657,8 @@ protected int onUnloadCallback(JNIJavaVM vm) {
585657
if (tracingResultWriter.supportsOnUnloadTraceWriting()) {
586658
if (configOutputDirPath != null) {
587659
writeConfigurationFiles();
660+
compulsoryDelete(configOutputLockFilePath);
661+
configOutputLockFilePath = null;
588662
configOutputDirPath = null;
589663
}
590664
}

substratevm/src/com.oracle.svm.configure.test/src/com/oracle/svm/configure/test/config/OmitPreviousConfigTests.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,7 @@ private static TraceProcessor loadTraceProcessorFromResourceDirectory(String res
6767
try {
6868
String resourceName = resourceDirectory + "/" + resourceFileName;
6969
URL resourceURL = OmitPreviousConfigTests.class.getResource(resourceName);
70-
if (resourceURL == null) {
71-
Assert.fail("Configuration file " + resourceName + " does not exist. Make sure that the test or the config directory have not been moved.");
72-
}
73-
return resourceURL.toURI();
70+
return (resourceURL != null) ? resourceURL.toURI() : null;
7471
} catch (Exception e) {
7572
throw VMError.shouldNotReachHere("Unexpected error while locating the configuration files.", e);
7673
}

substratevm/src/com.oracle.svm.configure/src/com/oracle/svm/configure/ConfigurationTool.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838
import java.nio.file.Paths;
3939
import java.util.ArrayList;
4040
import java.util.Arrays;
41+
import java.util.HashSet;
4142
import java.util.Iterator;
4243
import java.util.List;
44+
import java.util.Set;
4345
import java.util.function.Predicate;
4446
import java.util.stream.Collectors;
4547

@@ -239,6 +241,7 @@ private static void generate(Iterator<String> argsIter, boolean acceptTraceFileA
239241
break;
240242
}
241243
}
244+
failIfAgentLockFilesPresent(inputSet, omittedInputSet, outputSet);
242245

243246
RuleNode callersFilter = null;
244247
if (!builtinCallerFilter) {
@@ -275,7 +278,9 @@ private static void generate(Iterator<String> argsIter, boolean acceptTraceFileA
275278
null);
276279
List<Path> predefinedClassDestDirs = new ArrayList<>();
277280
for (URI pathUri : outputSet.getPredefinedClassesConfigPaths()) {
278-
predefinedClassDestDirs.add(Paths.get(pathUri).getParent().resolve(ConfigurationFile.PREDEFINED_CLASSES_AGENT_EXTRACTED_SUBDIR));
281+
Path subdir = Files.createDirectories(Paths.get(pathUri).getParent().resolve(ConfigurationFile.PREDEFINED_CLASSES_AGENT_EXTRACTED_SUBDIR));
282+
subdir = Files.createDirectories(subdir);
283+
predefinedClassDestDirs.add(subdir);
279284
}
280285
Predicate<String> shouldExcludeClassesWithHash = omittedInputTraceProcessor.getPredefinedClassesConfiguration()::containsClassWithHash;
281286
p = new TraceProcessor(advisor, inputSet.loadJniConfig(ConfigurationSet.FAIL_ON_EXCEPTION), inputSet.loadReflectConfig(ConfigurationSet.FAIL_ON_EXCEPTION),
@@ -332,6 +337,24 @@ private static void generate(Iterator<String> argsIter, boolean acceptTraceFileA
332337
}
333338
}
334339

340+
private static void failIfAgentLockFilesPresent(ConfigurationSet... sets) {
341+
Set<String> paths = null;
342+
for (ConfigurationSet set : sets) {
343+
for (URI path : set.getDetectedAgentLockPaths()) {
344+
if (paths == null) {
345+
paths = new HashSet<>();
346+
}
347+
paths.add(path.toString());
348+
}
349+
}
350+
if (paths != null && !paths.isEmpty()) {
351+
throw new UsageException("The following agent lock files were found in specified configuration directories, which means an agent is currently writing to them. " +
352+
"The agent must finish execution before its configuration can be safely accessed. " +
353+
"Unless a lock file is a leftover from an earlier process that terminated abruptly, it is unsafe to delete it." + System.lineSeparator() +
354+
String.join(System.lineSeparator(), paths));
355+
}
356+
}
357+
335358
private static void generateFilterRules(Iterator<String> argsIter) throws IOException {
336359
Path outputPath = null;
337360
boolean reduce = false;

0 commit comments

Comments
 (0)