/******************************************************************************* * Copyright (c) 2001, 2010 Mathew A. Nelson and Robocode contributors * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://robocode.sourceforge.net/license/epl-v10.html * * Contributors: * Mathew A. Nelson * - Initial API and implementation * Flemming N. Larsen * - Code cleanup * - Ported to Java 5.0 * - Updated to use methods from the Logger, which replaces logger methods * that have been (re)moved from the robocode.util.Utils class * - Fixed method synchronization issues with several member fields * Matthew Reeder * - Fixed compiler problem with protectionDomain * Robert D. Maupin * - Replaced old collection types like Vector and Hashtable with * synchronized List and HashMap * Nathaniel Troutman * - Added cleanup() method for cleaning up references to internal classes * to prevent circular references causing memory leaks * - Added cleanup of hidden ClassLoader.class.classes *******************************************************************************/ package net.sf.robocode.host.security; import net.sf.robocode.core.Container; import net.sf.robocode.host.IHostedThread; import net.sf.robocode.host.IRobotClassLoader; import net.sf.robocode.io.FileUtil; import net.sf.robocode.io.Logger; import net.sf.robocode.io.URLJarCollector; import robocode.robotinterfaces.IBasicRobot; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.nio.ByteBuffer; import java.security.*; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * This class loader is used by robots. It isolates classes which belong to robot and load them locally. * General java classes or robocode.api classes are loaded by parent loader and shared with Robocode engine. * Attempts to load classes of Robocode engine are blocked. * * @author Mathew A. Nelson (original) * @author Flemming N. Larsen (contributor) * @author Matthew Reeder (contributor) * @author Robert D. Maupin (contributor) * @author Nathaniel Troutman (contributor) */ public class RobotClassLoader extends URLClassLoader implements IRobotClassLoader { public static final String untrustedURL = "http://robocode.sf.net/untrusted"; private static final boolean IS_SECURITY_ON = !System.getProperty("NOSECURITY", "false").equals("true"); private static final PermissionCollection EMPTY_PERMISSIONS = new Permissions(); protected final URL robotClassPath; protected final String fullClassName; private ClassLoader parent; private CodeSource codeSource; private IHostedThread robotProxy; protected Class<?> robotClass; private Set<String> referencedClasses = new HashSet<String>(); private String[] staticRobotInstanceWarning; // cached warning messages public RobotClassLoader(URL robotClassPath, String robotFullClassName) { super(new URL[] { robotClassPath}, Container.systemLoader); fullClassName = robotFullClassName; this.robotClassPath = robotClassPath; parent = getParent(); try { codeSource = new CodeSource(new URL(untrustedURL), (Certificate[]) null); } catch (MalformedURLException ignored) {} } public void setRobotProxy(Object robotProxy) { this.robotProxy = (IHostedThread) robotProxy; } public synchronized void addURL(URL url) { super.addURL(url); } public synchronized Class<?> loadClass(final String name, boolean resolve) throws ClassNotFoundException { if (name.startsWith("java.lang")) { // we always delegate java.lang stuff to parent loader return super.loadClass(name, resolve); } if (IS_SECURITY_ON) { testPackages(name); } if (!name.startsWith("robocode")) { final Class<?> result = loadRobotClassLocaly(name, resolve); if (result != null) { // yes, it is in robot's class path // we loaded it locally return result; } } // it is robot API // or java class // or security is off // so we delegate to parent class loader return parent.loadClass(name); } private void testPackages(String name) throws ClassNotFoundException { if (name.startsWith("net.sf.robocode")) { final String message = "Robots are not allowed to reference Robocode engine in package: net.sf.robocode"; punishSecurityViolation(message); throw new ClassNotFoundException(message); } if (name.startsWith("robocode.control")) { final String message = "Robots are not allowed to reference Robocode engine in package: robocode.control"; punishSecurityViolation(message); throw new ClassNotFoundException(message); } if (IS_SECURITY_ON && name.startsWith("javax.swing")) { final String message = "Robots are not allowed to reference Robocode engine in package: javax.swing"; punishSecurityViolation(message); throw new ClassNotFoundException(message); } } private Class<?> loadRobotClassLocaly(String name, boolean resolve) throws ClassNotFoundException { Class<?> result = findLoadedClass(name); if (result == null) { final ByteBuffer resource = findLocalResource(name); if (resource != null) { result = defineClass(name, resource, codeSource); if (resolve) { resolveClass(result); } ClassAnalyzer.getReferencedClasses(resource, referencedClasses); } } return result; } // this whole fun is there to be able to provide defineClass with bytes // we need to call defineClass to be able to set codeSource to untrustedLocation private ByteBuffer findLocalResource(final String name) { return AccessController.doPrivileged(new PrivilegedAction<ByteBuffer>() { public ByteBuffer run() { // try to find it in robot's class path // this is URL, don't change to File.pathSeparator String path = name.replace('.', '/').concat(".class"); final URL url = findResource(path); ByteBuffer result = null; InputStream is = null; BufferedInputStream bis = null; if (url != null) { try { final URLConnection connection = URLJarCollector.openConnection(url); is = connection.getInputStream(); bis = new BufferedInputStream(is); result = ByteBuffer.allocate(1024 * 8); boolean done = false; do { do { int res = bis.read(result.array(), result.position(), result.remaining()); if (res == -1) { done = true; break; } result.position(result.position() + res); } while (result.remaining() != 0); result.flip(); if (!done) { result = ByteBuffer.allocate(result.capacity() * 2).put(result); } } while (!done); } catch (IOException e) { Logger.logError(e); return null; } finally { FileUtil.cleanupStream(bis); FileUtil.cleanupStream(is); } } return result; } }); } private void punishSecurityViolation(String message) { if (robotProxy != null) { robotProxy.punishSecurityViolation(message); } } protected PermissionCollection getPermissions(CodeSource codesource) { if (IS_SECURITY_ON) { return EMPTY_PERMISSIONS; } return super.getPermissions(codesource); } public String[] getReferencedClasses() { return referencedClasses.toArray(new String[referencedClasses.size()]); } public synchronized Class<?> loadRobotMainClass(boolean resolve) throws ClassNotFoundException { try { if (robotClass == null) { robotClass = loadClass(fullClassName, resolve); if (!IBasicRobot.class.isAssignableFrom(robotClass)) { // that's not robot return null; } if (resolve) { // resolve methods to see more referenced classes robotClass.getMethods(); // iterate thru dependencies until we didn't found any new HashSet<String> clone; do { clone = new HashSet<String>(referencedClasses); for (String reference : clone) { testPackages(reference); if (!isSystemClass(reference)) { loadClass(reference, true); } } } while (referencedClasses.size() != clone.size()); } } else { warnIfStaticRobotInstanceFields(); } } catch (Throwable e) { robotClass = null; throw new ClassNotFoundException(e.getMessage(), e); } return robotClass; } public IBasicRobot createRobotInstance() throws ClassNotFoundException, InstantiationException, IllegalAccessException { loadRobotMainClass(true); return (IBasicRobot) robotClass.newInstance(); } public void cleanup() { // Bug fix [2930266] - Robot static data isn't being GCed after battle for (String className : getReferencedClasses()) { cleanStaticReferences(className); } parent = null; codeSource = null; robotProxy = null; robotClass = null; referencedClasses = null; } /** * Cleans all static field references on a class. * * @param className the name of the class containing the static references to clean. */ private void cleanStaticReferences(String className) { if (isSystemClass(className)) { return; } Class<?> type = null; try { type = loadRobotClassLocaly(className, false); } catch (Throwable t) { return; } if (type != null) { for (Field field : getAllFields(new ArrayList<Field>(), type)) { if (isStaticReference(field)) { cleanStaticReference(field); } } } } private void warnIfStaticRobotInstanceFields() { if (staticRobotInstanceWarning == null) { List<Field> staticRobotReferences = new ArrayList<Field>(); for (String className : getReferencedClasses()) { // Bug fix [3028102] - ConcurrentModificationException if (isSystemClass(className)) { continue; } Class<?> type = null; try { type = loadRobotClassLocaly(className, false); } catch (Throwable t) { continue; } if (type != null) { for (Field field : getAllFields(new ArrayList<Field>(), type)) { if (isStaticReference(field) && IBasicRobot.class.isAssignableFrom(field.getType()) && field.getAnnotation(robocode.annotation.SafeStatic.class) == null) { staticRobotReferences.add(field); } } } } if (staticRobotReferences.size() > 0) { StringBuilder buf = new StringBuilder(); buf.append("Warning: ").append(fullClassName).append( " uses static reference to a robot with the following field(s):"); for (Field field : staticRobotReferences) { buf.append("\n\t").append(field.getDeclaringClass().getName()).append('.').append(field.getName()).append(", which points to a ").append( field.getType().getName()); } staticRobotInstanceWarning = new String[] { buf.toString(), "Static references to robots can cause unwanted behaviour with the robot using these.", "Please change static robot references to non-static references and recompile the robot."}; } else { staticRobotInstanceWarning = new String[] {}; // Signal that there is no warnings to cache } } else if (staticRobotInstanceWarning.length == 0) { return; // Return, as no warnings should be written out in the robot console } // Write out warnings to the robot console if (robotProxy != null) { for (String line : staticRobotInstanceWarning) { robotProxy.getOut().println("SYSTEM: " + line); } } } /** * Cleans a static field reference class, even when it is 'private static final' * * @param field the field to clean, if it is a static reference. */ private void cleanStaticReference(Field field) { field.setAccessible(true); try { // In order to set a 'private static field', we need to fix the modifier, i.e. use magic! ;-) Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); final int modifiers = modifiersField.getInt(field); modifiersField.setInt(field, modifiers & ~Modifier.FINAL); // Remove the FINAL modifier field.set(null, null); } catch (Throwable ignore) {} } /** * Gets all fields of a class (public, protected, private) and the ones inherited from all super classes. * @param fields the list where the fields will be added as a result of calling this method. * @param type the class to retrieve all the fields from * @return the list specified as input parameter containing all the retrieved fields */ private static List<Field> getAllFields(List<Field> fields, Class<?> type) { if (type == null || isSystemClass(type.getName())) { return fields; } try { for (Field field: type.getDeclaredFields()) { fields.add(field); } } catch (Throwable ignore) {// NoClassDefFoundError does occur with some robots, e.g. sgp.Drunken [1.12] // We ignore all exceptions and errors here so we can proceed to retrieve // field from super classes. } if (type.getSuperclass() != null) { fields = getAllFields(fields, type.getSuperclass()); } return fields; } /** * Checks if a specified class name is a Java system class or internal Robocode class. * @param className the class name to check. * @return true if the class name is a system class; false otherwise. */ private static boolean isSystemClass(String className) { return className.startsWith("java.") || className.startsWith("javax.") || className.startsWith("robocode.") || className.startsWith("net.sf.robocode.") || className.startsWith("tested.robots."); } /** * Checks if a specified field is a static reference. * * @param field the field to check. * @return true if the field is static reference; false otherwise. */ private static boolean isStaticReference(Field field) { return Modifier.isStatic(field.getModifiers()) && !(field.getType().isPrimitive() || field.isEnumConstant() || field.isSynthetic()); } }