diff --git a/CHANGES.txt b/CHANGES.txt index 969b959e01..6761949457 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 5.1 Merged from 5.0: + * Prevent InaccessibleObjectException when the Leak Detector is traversing objects (CASSANDRA-18708) * Remove legacy command line options from cassandra-stress (CASSANDRA-18529) * Remove commitlog_sync_batch_window_in_ms (CASSANDRA-17161) * Upgrade JMH from 1.21 to 1.36 (CASSANDRA-18696) diff --git a/src/java/org/apache/cassandra/exceptions/UnaccessibleFieldException.java b/src/java/org/apache/cassandra/exceptions/UnaccessibleFieldException.java new file mode 100644 index 0000000000..d68be42487 --- /dev/null +++ b/src/java/org/apache/cassandra/exceptions/UnaccessibleFieldException.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.exceptions; + +public class UnaccessibleFieldException extends RuntimeException +{ + private static final long serialVersionUID = 7340063332562337064L; + + public UnaccessibleFieldException(String message) + { + super(message); + } + + public UnaccessibleFieldException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/java/org/apache/cassandra/utils/concurrent/Ref.java b/src/java/org/apache/cassandra/utils/concurrent/Ref.java index 706a982f59..e268f5fd73 100644 --- a/src/java/org/apache/cassandra/utils/concurrent/Ref.java +++ b/src/java/org/apache/cassandra/utils/concurrent/Ref.java @@ -31,6 +31,8 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.apache.cassandra.exceptions.UnaccessibleFieldException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +51,7 @@ import org.apache.cassandra.utils.ExecutorUtils; import org.apache.cassandra.utils.NoSpamLogger; import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.Shared; +import sun.misc.Unsafe; import sun.nio.ch.DirectBuffer; import org.cliffc.high_scale_lib.NonBlockingHashMap; @@ -73,10 +76,10 @@ import static org.apache.cassandra.utils.Throwables.merge; * - encapsulates a Ref, we'll call selfRef, to which it proxies all calls to RefCounted behaviours * - users must ensure no references to the selfRef leak, or are retained outside of a method scope. * (to ensure the selfRef is collected with the object, so that leaks may be detected and corrected) - * + *

* This class' functionality is achieved by what may look at first glance like a complex web of references, * but boils down to: - * + *

* {@code * Target --> selfRef --> [Ref.State] <--> Ref.GlobalState --> Tidy * ^ @@ -86,10 +89,10 @@ import static org.apache.cassandra.utils.Throwables.merge; * Global ------------------------------------- * } * So that, if Target is collected, Impl is collected and, hence, so is selfRef. - * + *

* Once ref or selfRef are collected, the paired Ref.State's release method is called, which if it had * not already been called will update Ref.GlobalState and log an error. - * + *

* Once the Ref.GlobalState has been completely released, the Tidy method is called and it removes the global reference * to itself so it may also be collected. */ @@ -400,7 +403,7 @@ public final class Ref implements RefCounted } } - static final Deque inProgressVisitPool = new ArrayDeque(); + static final Deque inProgressVisitPool = new ArrayDeque<>(); @SuppressWarnings({ "rawtypes", "unchecked" }) static InProgressVisit newInProgressVisit(Object o, List fields, Field field, String name) @@ -525,16 +528,16 @@ public final class Ref implements RefCounted if (o instanceof WeakReference & nextField.getDeclaringClass() == Reference.class) continue; - Object nextObject = nextField.get(o); + Object nextObject = getFieldValue(o, nextField); if (nextObject != null) - return Pair.create(nextField.get(o), nextField); + return Pair.create(getFieldValue(o, nextField), nextField); } } @Override public String toString() { - return field == null ? name : field.toString() + "-" + o.getClass().getName(); + return field == null ? name : field + "-" + o.getClass().getName(); } } @@ -611,7 +614,6 @@ public final class Ref implements RefCounted { path.offer(inProgress); inProgress = newInProgressVisit(child, getFields(child.getClass()), field, null); - continue; } else if (visiting == child) { @@ -629,7 +631,6 @@ public final class Ref implements RefCounted { returnInProgressVisit(inProgress); inProgress = null; - continue; } } catch (IllegalAccessException e) @@ -653,13 +654,68 @@ public final class Ref implements RefCounted { if (field.getType().isPrimitive() || Modifier.isStatic(field.getModifiers())) continue; - field.setAccessible(true); fields.add(field); } fields.addAll(getFields(clazz.getSuperclass())); return fields; } + /** + * The unsafe instance used to access object protected by the Module System + */ + private static final Unsafe unsafe = loadUnsafe(); + + private static Unsafe loadUnsafe() + { + try + { + return Unsafe.getUnsafe(); + } + catch (final Exception ex) + { + try + { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } + catch (Exception e) + { + return null; + } + } + } + + public static Object getFieldValue(Object object, Field field) + { + try + { + // This call will unfortunately emit a warning for some scenario (which was a weird decision from the JVM designer) + if (field.trySetAccessible()) + { + // The field is accessible lets use reflection. + return field.get(object); + } + + // The access to the field is being restricted by the module system. Let's try to go around it through Unsafe. + if (unsafe == null) + throw new UnaccessibleFieldException("The value of the '" + field.getName() + "' field from " + object.getClass().getName() + + " cannot be retrieved as the field cannot be made accessible and Unsafe is unavailable"); + + long offset = unsafe.objectFieldOffset(field); + + boolean isFinal = Modifier.isFinal(field.getModifiers()); + boolean isVolatile = Modifier.isVolatile(field.getModifiers()); + + return isFinal || isVolatile ? unsafe.getObjectVolatile(object, offset) : unsafe.getObject(object, offset); + + } + catch (Throwable e) + { + throw new UnaccessibleFieldException("The value of the '" + field.getName() + "' field from " + object.getClass().getName() + " cannot be retrieved", e); + } + } + public static class IdentityCollection { final Set candidates;