Skip to content

Instantly share code, notes, and snippets.

@RogerRiggs
Last active February 19, 2023 00:15
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save RogerRiggs/94cef77213cd812d3668228e8f8995ab to your computer and use it in GitHub Desktop.
Save RogerRiggs/94cef77213cd812d3668228e8f8995ab to your computer and use it in GitHub Desktop.
Example Using java.lang.ref.Cleaner as a Replacement for Finalize and Testing of the cleanup.
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package example.cleaner;
import java.lang.ref.Cleaner;
import java.util.Arrays;
import java.util.Optional;
public class SensitiveData implements AutoCloseable {
// A cleaner (preferably one shared within a library,
// but for the sake of example, a new one is created here)
private static final Cleaner cleaner = Cleaner.create();
// The sensitive data
private char[] sensitiveData;
// The result of registering with the cleaner
private final Cleaner.Cleanable cleanable;
/**
* Construct an object to hold sensitive data.
* @param sensitiveData a char array, non-null
*/
public SensitiveData(char[] sensitiveData) {
final char[] chars = sensitiveData.clone();
final Runnable F // Pick one of the following cleaner functions
= () -> Arrays.fill(chars, (char) 0); // A lambda
// = new SensitiveCleanable(chars); // a nested record class for cleanup
// = clearCharsRunnable(chars); // lambda from a static context
this.sensitiveData = chars;
this.cleanable = cleaner.register(this, F);
}
/**
* Return an Optional of a copy of the char array.
*/
public Optional<char[]> sensitiveData() {
return Optional.ofNullable(sensitiveData == null ? null : sensitiveData.clone());
}
/**
* Close and cleanup the sensitive data storage.
*/
public void close() {
sensitiveData = null; // Data not available after close
cleanable.clean(); // The cleanable already has the char array reference
}
// Return a lambda to do the clearing, ensure it does not reference 'this'.
private static Runnable clearCharsRunnable(char[] chars) {
return () -> Arrays.fill(chars, (char)0);
}
/*
* Record class to perform the cleanup of a char array.
*/
private record SensitiveCleanable(char[] sensitiveData) implements Runnable {
public void run() {
// cleanup action accessing SensitiveCleanable, executed at most once
Arrays.fill(sensitiveData, (char)0);
}
}
// Encapsulate a string for printing and clear the temporary buffer.
public static void main(String[] args) {
for (String s : args) {
char[] chars = s.toCharArray();
try (SensitiveData sd = new SensitiveData(chars)) {
Arrays.fill(chars, (char) 0);
print(sd);
}
}
}
// Print the sensitive data and clear the temporary buffer.
private static void print(SensitiveData sd) {
char[] chars = sd.sensitiveData().get();
System.out.println(chars);
Arrays.fill(chars, (char)0);
}
}
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package test.cleaner;
import example.cleaner.SensitiveData;
import java.lang.ref.Cleaner;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import static org.testng.Assert.*;
/**
* Tests of SensitiveData example code using cleaners.
*/
public class SensitiveDataTest {
/**
* Test SensitiveData is cleared when explicitly closed (and AutoCloseable).
*/
@org.testng.annotations.Test
public void testAutoClose() {
// The object to be tested
final char[] origChars = "myPrivateData".toCharArray();
char[] implChars;
try (SensitiveData data = new SensitiveData(origChars)) {
// Extract a reference to the implementation sensitiveData char array
implChars = (char[]) getField(SensitiveData.class,
"sensitiveData", data);
assertEquals(implChars, origChars,
"SensitiveData chars changed prematurely: " +
Arrays.toString(implChars));
}
// After the SensitiveData is closed, check the chars have been cleared
char[] zeroChars = new char[implChars.length]; // same number of zero chars
assertEquals(implChars, zeroChars,
"After AutoCloseable.close, SensitiveData chars not zero: " +
Arrays.toString(implChars));
}
/**
* Check that SensitiveData is cleared when GC finds it to be unreferenced.
*/
@org.testng.annotations.Test
public void testUnreferenced() {
final char[] origChars = "myPrivateData".toCharArray();
SensitiveData data = new SensitiveData(origChars);
// Extract a reference to the implementation sensitiveData char array
char[] implChars = (char[]) getField(SensitiveData.class,
"sensitiveData", data);
data = null; // Remove this reference to the SensitiveData
Reference.reachabilityFence(data); // Ensure data is not over-optimitically gc'd
char[] zeroChars = new char[implChars.length]; // same number of zero chars
for (int retries = 10; retries > 0; retries--) {
System.gc();
try {
Thread.sleep(10L);
} catch (InterruptedException ie) { /* ignore */ }
if (Arrays.equals(implChars, zeroChars))
break;
}
// Check and report any errors
assertEquals(implChars, zeroChars,
"After GC, SensitiveData chars not zero: " + Arrays.toString(implChars));
}
/**
* Check that SensitiveData is cleared when GC finds the Cleanable to be unreferenced.
*/
@org.testng.annotations.Test
public void testCleanable() {
// The object to be tested
final char[] origChars = "myPrivateData".toCharArray();
SensitiveData data = new SensitiveData(origChars);
// Extract a reference to the implementation sensitiveData Cleanable
Cleaner.Cleanable cleanable = (Cleaner.Cleanable) getField(SensitiveData.class, "cleanable", data);
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> cleanableRef = new PhantomReference<>(cleanable, queue);
cleanable = null; // Only the Cleaner should still have a strong reference to the Cleanable
// First check that the cleaning does not happen before the reference is cleared.
assertNull(waitForReference(queue),
"SensitiveData cleaned prematurely");
data = null; // Remove this reference to the SensitiveData
Reference.reachabilityFence(data); // Ensure data is not over-optimitically gc'd
assertEquals(waitForReference(queue), cleanableRef,
"After GC, SensitiveData not cleaned");
}
/**
* Get an object from a named field of an object.
*
* @param clazz the class containing the Cleaner.Cleanable
* @param fieldName the field name holding the Cleanable
* @param instance an instance of the class to retrieve it from
* @return an object retrieved from the field
* @throws RuntimeException if the field is not found or not accessible
*/
private static Object getField(Class<?> clazz, String fieldName,
Object instance) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(instance);
} catch (NoSuchFieldException | IllegalAccessException ex) {
throw new RuntimeException("Field not found or not accessible", ex);
}
}
/**
* Wait for any Reference to be enqueued to a ReferenceQueue.
* The garbage collector is invoked to find unreferenced objects.
*
* @param queue a ReferenceQueue
* @return true if the reference was enqueued, false if not enqueued within
*/
private static Reference<?> waitForReference(ReferenceQueue<Object> queue) {
Objects.requireNonNull(queue, "queue should not be null");
for (int retries = 10; retries > 0; retries--) {
System.gc();
try {
var r = queue.remove(10L);
if (r != null) {
return r;
}
} catch (InterruptedException ie) {
// ignore, the loop will try again
}
}
return null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment