8307526: [JFR] Better handling of tampered JFR repository

Reviewed-by: egahlin
This commit is contained in:
Joakim Nordström 2023-07-05 22:26:57 +00:00 committed by Erik Gahlin
parent 0616648c59
commit 66d2736521
8 changed files with 136 additions and 42 deletions

@ -389,3 +389,7 @@ JVM_ENTRY_NO_ENV(jlong, jfr_host_total_memory(JNIEnv* env, jclass jvm))
return os::physical_memory();
#endif
JVM_END
JVM_ENTRY_NO_ENV(void, jfr_emit_data_loss(JNIEnv* env, jclass jvm, jlong bytes))
EventDataLoss::commit(bytes, min_jlong);
JVM_END

@ -156,6 +156,8 @@ jboolean JNICALL jfr_is_containerized(JNIEnv* env, jclass jvm);
jlong JNICALL jfr_host_total_memory(JNIEnv* env, jclass jvm);
void JNICALL jfr_emit_data_loss(JNIEnv* env, jclass jvm, jlong bytes);
#ifdef __cplusplus
}
#endif

@ -95,7 +95,8 @@ JfrJniMethodRegistration::JfrJniMethodRegistration(JNIEnv* env) {
(char*)"isExcluded", (char*)"(Ljava/lang/Class;)Z", (void*)jfr_is_class_excluded,
(char*)"isInstrumented", (char*)"(Ljava/lang/Class;)Z", (void*) jfr_is_class_instrumented,
(char*)"isContainerized", (char*)"()Z", (void*) jfr_is_containerized,
(char*)"hostTotalMemory", (char*)"()J", (void*) jfr_host_total_memory
(char*)"hostTotalMemory", (char*)"()J", (void*) jfr_host_total_memory,
(char*)"emitDataLoss", (char*)"(J)V", (void*)jfr_emit_data_loss
};
const size_t method_array_length = sizeof(method) / sizeof(JNINativeMethod);

@ -33,7 +33,7 @@ import java.util.SequencedSet;
import jdk.jfr.internal.SecuritySupport.SafePath;
// This class keeps track of files that can't be deleted
// so they can a later staged be removed.
// so they can at a later staged be removed.
final class FilePurger {
private static final SequencedSet<SafePath> paths = new LinkedHashSet<>();
@ -62,6 +62,13 @@ final class FilePurger {
}
private static boolean delete(SafePath p) {
try {
if (!SecuritySupport.exists(p)) {
return true;
}
} catch (IOException e) {
// ignore
}
try {
SecuritySupport.delete(p);
return true;

@ -611,4 +611,11 @@ public final class JVM {
* JVM runs in a container.
*/
public static native long hostTotalMemory();
/**
* Emit a jdk.DataLoss event for the specified amount of bytes.
*
* @param bytes number of bytes that were lost
*/
public static native void emitDataLoss(long bytes);
}

@ -25,6 +25,7 @@
package jdk.jfr.internal;
import static jdk.jfr.internal.LogLevel.ERROR;
import static jdk.jfr.internal.LogLevel.INFO;
import static jdk.jfr.internal.LogLevel.TRACE;
import static jdk.jfr.internal.LogLevel.WARN;
@ -448,10 +449,19 @@ public final class PlatformRecorder {
}
private void finishChunk(RepositoryChunk chunk, Instant time, PlatformRecording ignoreMe) {
chunk.finish(time);
for (PlatformRecording r : getRecordings()) {
if (r != ignoreMe && r.getState() == RecordingState.RUNNING) {
r.appendChunk(chunk);
if (chunk.finish(time)) {
for (PlatformRecording r : getRecordings()) {
if (r != ignoreMe && r.getState() == RecordingState.RUNNING) {
r.appendChunk(chunk);
}
}
} else {
if (chunk.isMissingFile()) {
// With one chunkfile found missing, its likely more could've been removed too. Iterate through all recordings,
// and check for missing files. This will emit more error logs that can be seen in subsequent recordings.
for (PlatformRecording r : getRecordings()) {
r.removeNonExistantPaths();
}
}
}
// Decrease initial reference count
@ -498,17 +508,24 @@ public final class PlatformRecorder {
return;
}
while (true) {
synchronized (this) {
if (JVM.shouldRotateDisk()) {
rotateDisk();
}
if (isToDisk()) {
EventLog.update();
long wait = Options.getWaitInterval();
try {
synchronized (this) {
if (JVM.shouldRotateDisk()) {
rotateDisk();
}
if (isToDisk()) {
EventLog.update();
}
}
long minDelta = PeriodicEvents.doPeriodic();
wait = Math.min(minDelta, Options.getWaitInterval());
} catch (Throwable t) {
// Catch everything and log, but don't allow it to end the periodic task
Logger.log(JFR_SYSTEM, ERROR, "Error in Periodic task: " + t.getClass().getName());
} finally {
takeNap(wait);
}
long minDelta = PeriodicEvents.doPeriodic();
long wait = Math.min(minDelta, Options.getWaitInterval());
takeNap(wait);
}
}

@ -26,12 +26,15 @@
package jdk.jfr.internal;
import static jdk.jfr.internal.LogLevel.DEBUG;
import static jdk.jfr.internal.LogLevel.ERROR;
import static jdk.jfr.internal.LogLevel.INFO;
import static jdk.jfr.internal.LogLevel.WARN;
import static jdk.jfr.internal.LogTag.JFR;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardOpenOption;
import java.security.AccessControlContext;
import java.security.AccessController;
@ -718,17 +721,33 @@ public final class PlatformRecording implements AutoCloseable {
public void dumpStopped(WriteableUserPath userPath) throws IOException {
synchronized (recorder) {
userPath.doPrivilegedIO(() -> {
try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
long bytes = cc.transferTo(fc);
Logger.log(LogTag.JFR, LogLevel.INFO, "Transferred " + bytes + " bytes from the disk repository");
// No need to force if no data was transferred, which avoids IOException when device is /dev/null
if (bytes != 0) {
fc.force(true);
}
}
return null;
});
transferChunksWithRetry(userPath);
}
}
private void transferChunksWithRetry(WriteableUserPath userPath) throws IOException {
userPath.doPrivilegedIO(() -> {
try {
transferChunks(userPath);
} catch (NoSuchFileException nsfe) {
Logger.log(LogTag.JFR, LogLevel.ERROR, "Missing chunkfile when writing recording \"" + name + "\" (" + id + ") to " + userPath.getRealPathText() + ".");
// if one chunkfile was missing, its likely more are missing
removeNonExistantPaths();
// and try the transfer again
transferChunks(userPath);
}
return null;
});
}
private void transferChunks(WriteableUserPath userPath) throws IOException {
try (ChunksChannel cc = new ChunksChannel(chunks); FileChannel fc = FileChannel.open(userPath.getReal(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
long bytes = cc.transferTo(fc);
Logger.log(LogTag.JFR, LogLevel.INFO, "Transferred " + bytes + " bytes from the disk repository");
// No need to force if no data was transferred, which avoids IOException when device is /dev/null
if (bytes != 0) {
fc.force(true);
}
}
}
@ -880,4 +899,27 @@ public final class PlatformRecording implements AutoCloseable {
}
}
}
void removeNonExistantPaths() {
synchronized (recorder) {
Iterator<RepositoryChunk> it = chunks.iterator();
Logger.log(JFR, INFO, "Checking for missing chunkfiles for recording \"" + name + "\" (" + id + ")");
while (it.hasNext()) {
RepositoryChunk chunk = it.next();
if (chunk.isMissingFile()) {
String msg = "Chunkfile \"" + chunk.getFile() + "\" is missing. " +
"Data loss might occur from " + chunk.getStartTime();
if (chunk.getEndTime() != null) {
msg += " to " + chunk.getEndTime();
}
Logger.log(JFR, ERROR, msg);
JVM.emitDataLoss(chunk.getSize());
it.remove();
removed(chunk);
}
}
}
}
}

@ -29,7 +29,10 @@ import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.ReadableByteChannel;
import java.time.Instant;
import java.time.Period;
import java.time.Duration;
import java.util.Comparator;
import java.util.Optional;
import jdk.jfr.internal.SecuritySupport.SafePath;
@ -55,20 +58,25 @@ public final class RepositoryChunk {
this.unFinishedRAF = SecuritySupport.createRandomAccessFile(chunkFile);
}
void finish(Instant endTime) {
boolean finish(Instant endTime) {
try {
finishWithException(endTime);
unFinishedRAF.close();
size = SecuritySupport.getFileSize(chunkFile);
this.endTime = endTime;
if (Logger.shouldLog(LogTag.JFR_SYSTEM, LogLevel.DEBUG)) {
Logger.log(LogTag.JFR_SYSTEM, LogLevel.DEBUG, "Chunk finished: " + chunkFile);
}
return true;
} catch (IOException e) {
Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not finish chunk. " + e.getClass() + " "+ e.getMessage());
}
}
private void finishWithException(Instant endTime) throws IOException {
unFinishedRAF.close();
this.size = SecuritySupport.getFileSize(chunkFile);
this.endTime = endTime;
if (Logger.shouldLog(LogTag.JFR_SYSTEM, LogLevel.DEBUG)) {
Logger.log(LogTag.JFR_SYSTEM, LogLevel.DEBUG, "Chunk finished: " + chunkFile);
final String reason;
if (isMissingFile()) {
reason = "Chunkfile \""+ getFile() + "\" is missing. " +
"Data loss might occur from " + getStartTime() + " to " + endTime;
} else {
reason = e.getClass().getName();
}
Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not finish chunk. " + reason);
return false;
}
}
@ -103,16 +111,14 @@ public final class RepositoryChunk {
}
private void destroy() {
if (!isFinished()) {
finish(Instant.MIN);
}
delete(chunkFile);
try {
unFinishedRAF.close();
} catch (IOException e) {
if (Logger.shouldLog(LogTag.JFR, LogLevel.ERROR)) {
Logger.log(LogTag.JFR, LogLevel.ERROR, "Could not close random access file: " + chunkFile.toString() + ". File will not be deleted due to: " + e.getMessage());
}
} finally {
delete(chunkFile);
}
}
@ -174,4 +180,12 @@ public final class RepositoryChunk {
return 0L;
}
}
boolean isMissingFile() {
try {
return !SecuritySupport.exists(chunkFile);
} catch (IOException ioe) {
return true;
}
}
}