From e04d9ec716960677643c3a80e612356ffbd4f1a5 Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Mon, 4 May 2026 10:22:51 -0400 Subject: [PATCH] GP-6719 Improved Ghidra Server RMI deserialization filter and added client-side module-based RMI deserialization filter. --- .../HeadlessBSimApplicationConfiguration.java | 19 +- ...eadlessGhidraApplicationConfiguration.java | 28 +- .../Features/GhidraServer/data/serial.filter | 71 ++- .../main/java/ghidra/server/Repository.java | 21 +- .../java/ghidra/server/RepositoryManager.java | 30 +- .../ghidra/server/remote/GhidraServer.java | 216 ++----- .../server/remote}/RemoteBufferFileImpl.java | 112 ++-- .../server/remote/RemoteExceptionUtil.java | 118 ++++ .../server/remote/RemoteLoggingUtil.java | 111 ++++ .../remote}/RemoteManagedBufferFileImpl.java | 94 ++- .../server/remote/RepositoryHandleImpl.java | 420 ++++++++++---- .../remote/RepositoryServerHandleImpl.java | 121 ++-- .../java/ghidra/server/remote/ServerHelp.txt | 3 +- .../security/PKIAuthenticationModule.java | 4 +- .../security/SSHAuthenticationModule.java | 4 +- .../ExternalProgramLoginModule.java | 14 +- .../ghidra/server/store/RepositoryFile.java | 9 +- .../ghidra/server/store/RepositoryFolder.java | 10 +- .../java/db/buffers/BufferFileAdapter.java | 17 +- .../java/db/buffers/BufferFileHandle.java | 5 +- .../db/buffers/LocalManagedBufferFile.java | 4 +- .../db/buffers/RemoteBufferFileHandle.java | 29 +- .../RemoteManagedBufferFileHandle.java | 35 +- .../FileSystem/certification.manifest | 2 + .../FileSystem/data/client.rmi.serial.filter | 108 ++++ .../FileSystem/data/serialFilterREADME.md | 100 ++++ .../ghidra/framework/client/ClientUtil.java | 6 +- .../framework/client/RepositoryAdapter.java | 3 + .../remote/GhidraObjectInputFilter.java | 542 ++++++++++++++++++ .../remote/GhidraSerialFilterFactory.java | 93 +++ .../framework/remote/GhidraServerHandle.java | 12 +- .../remote/RemoteRepositoryHandle.java | 7 +- .../remote/RemoteRepositoryServerHandle.java | 23 +- .../framework/remote/SignatureCallback.java | 40 +- .../store/local/CheckoutManager.java | 51 +- .../store/local/LocalFolderItem.java | 38 +- .../store/local/RepositoryLogger.java | 11 +- .../net/ApplicationKeyManagerFactory.java | 2 +- .../util/exception/FileInUseException.java | 14 +- .../RuntimeScripts/Common/server/server.conf | 20 +- .../GhidraServerSerialFilterFailureTest.java | 8 +- .../ghidra/server/remote/ServerTestUtil.java | 2 - build.gradle | 27 + gradle/javaTestProject.gradle | 34 +- gradle/root/test.gradle | 35 +- 45 files changed, 1981 insertions(+), 692 deletions(-) rename Ghidra/Features/GhidraServer/src/main/java/{db/buffers => ghidra/server/remote}/RemoteBufferFileImpl.java (74%) create mode 100644 Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteExceptionUtil.java create mode 100644 Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteLoggingUtil.java rename Ghidra/Features/GhidraServer/src/main/java/{db/buffers => ghidra/server/remote}/RemoteManagedBufferFileImpl.java (53%) create mode 100644 Ghidra/Framework/FileSystem/data/client.rmi.serial.filter create mode 100644 Ghidra/Framework/FileSystem/data/serialFilterREADME.md create mode 100644 Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraObjectInputFilter.java create mode 100644 Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraSerialFilterFactory.java diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/HeadlessBSimApplicationConfiguration.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/HeadlessBSimApplicationConfiguration.java index d2df0bb25c..bcd7312d7f 100644 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/HeadlessBSimApplicationConfiguration.java +++ b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/query/ingest/HeadlessBSimApplicationConfiguration.java @@ -20,7 +20,9 @@ import java.util.List; import generic.jar.ResourceFile; import ghidra.framework.*; +import ghidra.framework.remote.GhidraObjectInputFilter; import ghidra.net.DefaultTrustManagerFactory; +import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; public class HeadlessBSimApplicationConfiguration extends ApplicationConfiguration { @@ -29,11 +31,20 @@ public class HeadlessBSimApplicationConfiguration extends ApplicationConfigurati protected void initializeApplication() { super.initializeApplication(); - // Locate certs if found (must be done before module initialization) - locateCACertsFile(); + try { + // Install client-side deserialization filters (data/*.serial.filter) + GhidraObjectInputFilter.configureClientSerialFilter(); - monitor.setMessage("Performing module initialization..."); - performModuleInitialization(); + // Locate certs if found (must be done before module initialization) + locateCACertsFile(); + + monitor.setMessage("Performing module initialization..."); + performModuleInitialization(); + } + catch (Throwable t) { + Msg.error(this, "Ghidra encountered a severe error during initialization", t); + System.exit(-1); + } monitor.setMessage("Done initializing"); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/HeadlessGhidraApplicationConfiguration.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/HeadlessGhidraApplicationConfiguration.java index 219f0d9009..45e05c88d0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/HeadlessGhidraApplicationConfiguration.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/HeadlessGhidraApplicationConfiguration.java @@ -21,6 +21,7 @@ import java.util.List; import generic.jar.ResourceFile; import ghidra.GhidraClassLoader; import ghidra.framework.preferences.Preferences; +import ghidra.framework.remote.GhidraObjectInputFilter; import ghidra.net.DefaultTrustManagerFactory; import ghidra.util.Msg; import ghidra.util.classfinder.ClassSearcher; @@ -31,19 +32,28 @@ public class HeadlessGhidraApplicationConfiguration extends ApplicationConfigura @Override protected void initializeApplication() { super.initializeApplication(); + + try { + // Install client-side deserialization filters (data/*.serial.filter) + GhidraObjectInputFilter.configureClientSerialFilter(); - // Now that preferences are accessible, finalize classpath by adding user plugin paths. - // This must be done before class searching. - addUserJarAndPluginPathsToClasspath(); + // Now that preferences are accessible, finalize classpath by adding user plugin paths. + // This must be done before class searching. + addUserJarAndPluginPathsToClasspath(); - monitor.setMessage("Performing class searching..."); - performClassSearching(); + monitor.setMessage("Performing class searching..."); + performClassSearching(); - // Locate certs if found (must be done before module initialization) - locateCACertsFile(); + // Locate certs if found (must be done before module initialization) + locateCACertsFile(); - monitor.setMessage("Performing module initialization..."); - performModuleInitialization(); + monitor.setMessage("Performing module initialization..."); + performModuleInitialization(); + } + catch (Throwable t) { + Msg.error(this, "Ghidra encountered a severe error during initialization", t); + System.exit(-1); + } monitor.setMessage("Done initializing"); } diff --git a/Ghidra/Features/GhidraServer/data/serial.filter b/Ghidra/Features/GhidraServer/data/serial.filter index 5dc35e3823..99022f5455 100644 --- a/Ghidra/Features/GhidraServer/data/serial.filter +++ b/Ghidra/Features/GhidraServer/data/serial.filter @@ -1,22 +1,67 @@ # Ghidra Server serialization filter patterns # See java.io.ObjectInputFilter.Config#createFilter(String) -# -# This file establishes allowed and disallowed inbound class de-serialization -# rules for the Ghidra Server. If not specifically allowed or disallowed a -# de-serialized class will be subject to an internal filter which allows all -# primitive and primitive array classes while rejecting all other classes. -# - -java.base/java.lang.*; -java.base/java.security.**; -java.base/javax.security.**; -java.base/sun.security.**; -java.base/java.util.**; ghidra.framework.remote.GhidraPrincipal; ghidra.framework.remote.AnonymousCallback; ghidra.framework.remote.SSHSignatureCallback; ghidra.framework.remote.SignatureCallback; ghidra.framework.remote.User; -ghidra.framework.remote.User[]; +[Lghidra.framework.remote.User; ghidra.framework.store.CheckoutType; + +java.lang.Object; +java.lang.String; +java.lang.Class; +java.lang.Enum; + +java.util.Collections$SynchronizedSet; +java.util.Collections$SynchronizedCollection; +java.util.LinkedList; + +java.security.cert.Certificate$CertificateRep; +java.security.cert.X509Certificate; +[Ljava.security.cert.X509Certificate; + +javax.security.auth.Subject; +javax.security.auth.Subject$*; +javax.security.auth.x500.X500Principal; +[Ljavax.security.auth.x500.X500Principal; +javax.security.auth.callback.Callback; +[Ljavax.security.auth.callback.Callback; +javax.security.auth.callback.NameCallback; +javax.security.auth.callback.PasswordCallback; + +# TODO: Server Remote API needs to be revised serialize certificates in PEM form. +# This affects ghidra.framework.remote.SignatureCallback implementation. +sun.security.x509.X509CertImpl; + +# RMI related classes + +java.rmi.server.UID; +java.rmi.server.ObjID; + +java.rmi.dgc.DGC; +java.rmi.dgc.Lease; +java.rmi.dgc.VMID; + +java.rmi.RemoteException; +java.rmi.AccessException; +java.rmi.ConnectException; +java.rmi.ConnectIOException; +java.rmi.MarshalException; +java.rmi.UnmarshalException; +java.rmi.NoSuchObjectException; +java.rmi.ServerException; +java.rmi.ServerRuntimeException; +java.rmi.UnexpectedException; +java.rmi.UnknownHostException; + +# The following additional entries may be required if using JMX over RMI for remote +# profiling (e.g., VisualVM). + +#javax.management.remote.rmi.RMIConnectionImpl_Stub; +#javax.management.remote.rmi.RMIServerImpl_Stub; +#java.rmi.server.RemoteObjectInvocationHandler; +#sun.rmi.server.UnicastRef; +#sun.rmi.transport.LiveRef; + diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/Repository.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/Repository.java index fccb2b1569..bc130df54a 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/Repository.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/Repository.java @@ -25,6 +25,7 @@ import ghidra.framework.remote.*; import ghidra.framework.store.FileSystem; import ghidra.framework.store.FileSystemListener; import ghidra.framework.store.local.*; +import ghidra.server.remote.RemoteLoggingUtil; import ghidra.server.remote.RepositoryHandleImpl; import ghidra.server.store.RepositoryFile; import ghidra.server.store.RepositoryFolder; @@ -327,9 +328,8 @@ public class Repository implements FileSystemListener, RepositoryLogger { * defined to the repository user manager. * @param currentUser user performing request * @return list of user names. - * @throws IOException if an IO error occurs */ - public String[] getServerUserList(String currentUser) throws IOException { + public String[] getServerUserList(String currentUser) { if (UserManager.ANONYMOUS_USERNAME.equals(currentUser)) { return new String[0]; } @@ -779,7 +779,7 @@ public class Repository implements FileSystemListener, RepositoryLogger { RepositoryFolder folder = getFolder(null, parentPath, false); if (folder == null || folder.getFolder(folderName) == null) { RepositoryManager.log(name, RepositoryFolder.makePathname(parentPath, folderName), - "ERROR! folder not found", null); + "ERROR! folder not found"); return; } } @@ -788,7 +788,7 @@ public class Repository implements FileSystemListener, RepositoryLogger { } catch (IOException e) { RepositoryManager.log(name, RepositoryFolder.makePathname(parentPath, folderName), - "ERROR! " + e.getMessage(), null); + "ERROR! " + e.getMessage()); } RepositoryChangeEvent event = new RepositoryChangeEvent( @@ -808,7 +808,7 @@ public class Repository implements FileSystemListener, RepositoryLogger { RepositoryFolder folder = getFolder(null, parentPath, false); if (folder == null || folder.getFile(itemName) == null) { RepositoryManager.log(name, RepositoryFolder.makePathname(parentPath, itemName), - "file not found", null); + "file not found"); return; } } @@ -817,7 +817,7 @@ public class Repository implements FileSystemListener, RepositoryLogger { } catch (IOException e) { RepositoryManager.log(name, RepositoryFolder.makePathname(parentPath, itemName), - "ERROR! " + e.getMessage(), null); + "ERROR! " + e.getMessage()); } RepositoryChangeEvent event = new RepositoryChangeEvent( @@ -844,7 +844,7 @@ public class Repository implements FileSystemListener, RepositoryLogger { throw new AssertException(); } catch (IOException e) { - RepositoryManager.log(name, parentPath, "ERROR! " + e.getMessage(), null); + RepositoryManager.log(name, parentPath, "ERROR! " + e.getMessage()); } RepositoryChangeEvent event = new RepositoryChangeEvent( @@ -933,11 +933,10 @@ public class Repository implements FileSystemListener, RepositoryLogger { } catch (IOException e) { RepositoryManager.log(name, RepositoryFolder.makePathname(parentPath, itemName), - "ERROR! " + e.getMessage(), null); + "ERROR! " + e.getMessage()); } if (syncErr) { - RepositoryManager.log(name, null, "ERROR! Repository instance may be out-of-sync", - null); + RepositoryManager.log(name, null, "ERROR! Repository instance may be out-of-sync"); return; } @@ -956,7 +955,7 @@ public class Repository implements FileSystemListener, RepositoryLogger { @Override public void log(String path, String msg, String user) { - RepositoryManager.log(name, path, msg, user); + RemoteLoggingUtil.log(name, path, msg, user, false); } static boolean markRepositoryForIndexMigration(File serverDir, String repositoryName, diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java index a2fd22595a..003fc22bbc 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/RepositoryManager.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -119,10 +119,12 @@ public class RepositoryManager { * given name * @throws UserAccessException if the user does not exist in * the list of known users for this manager + * @throws UserAccessException if the currentUser does not have + * ability to create a repository * @throws IOException if there was an error creating the repository */ public synchronized Repository createRepository(String currentUser, String name) - throws IOException, DuplicateFileException { + throws UserAccessException, IOException, DuplicateFileException { if (isAnonymousUser(currentUser)) { throw new UserAccessException("Anonymous user not permitted to create repository"); @@ -143,7 +145,6 @@ public class RepositoryManager { } Repository rep = new Repository(this, currentUser, f, name); - log(name, null, "repository created", currentUser); repositoryMap.put(name, rep); return rep; } @@ -183,9 +184,11 @@ public class RepositoryManager { * Delete a specified repository. * @param currentUser current user * @param name repository name + * @throws UserAccessException if currentUser does not have Admin priviledge * @throws IOException if error occurs while removing repository */ - public synchronized void deleteRepository(String currentUser, String name) throws IOException { + public synchronized void deleteRepository(String currentUser, String name) + throws UserAccessException, IOException { if (isAnonymousUser(currentUser)) { throw new UserAccessException("Anonymous user not permitted to delete repository"); @@ -429,23 +432,13 @@ public class RepositoryManager { return host; } - public static void log(String repositoryName, String path, String msg, String user) { + static void log(String repositoryName, String path, String msg) { StringBuffer buf = new StringBuffer(); if (repositoryName != null) { buf.append("["); buf.append(repositoryName); buf.append("]"); } - String host = RepositoryManager.getRMIClient(); - String userStr = user; - if (userStr != null) { - if (host != null) { - userStr += "@" + host; - } - } - else { - userStr = host; - } if (path != null) { buf.append(path); } @@ -453,11 +446,6 @@ public class RepositoryManager { buf.append(": "); } buf.append(msg); - if (userStr != null) { - buf.append(" ("); - buf.append(userStr); - buf.append(")"); - } log.info(buf.toString()); } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java index 90421f4b4b..927caf738e 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/GhidraServer.java @@ -70,9 +70,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan private static final String TLS_SERVER_PROTOCOLS_PROPERTY = "ghidra.tls.server.protocols"; private static final String TLS_ENABLED_CIPHERS_PROPERTY = "jdk.tls.server.cipherSuites"; - private static final String SERIALIZATION_FILTER_DISABLED_PROPERTY = - "ghidra.server.serialization.filter.disabled"; - private static SslRMIServerSocketFactory serverSocketFactory; private static SslRMIClientSocketFactory clientSocketFactory; private static InetAddress bindAddress; @@ -210,9 +207,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan GhidraServer.server = this; - // Establish serialization filter to address deserialization vulnerabity concerns. - setGlobalSerializationFilter(); - // Start block stream server - use RMI serverSocketFactory blockStreamServer = BlockStreamServer.getBlockStreamServer(); ServerSocket streamServerSocket; @@ -244,7 +238,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan } catch (Throwable t) { log.error("Failed to generate authentication callbacks", t); - throw new RemoteException("Failed to generate authentication callbacks", t); + throw new RemoteException("Failed to generate authentication callbacks"); } } @@ -263,7 +257,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan @Override public RemoteRepositoryServerHandle getRepositoryServer(Subject user, Callback[] authCallbacks) - throws LoginException, RemoteException { + throws FailedLoginException, RemoteException { System.gc(); @@ -278,29 +272,29 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan anonymousAuthModule.anonymousAccessRequested(authCallbacks)) { username = UserManager.ANONYMOUS_USERNAME; anonymousAccess = true; - RepositoryManager.log(null, null, "Anonymous access allowed", principal.getName()); + RemoteLoggingUtil.log("Anonymous access allowed", principal.getName()); } else if (authModule != null) { NameCallback nameCb = AuthenticationModule.getFirstCallbackOfType(NameCallback.class, authCallbacks); if (nameCb != null) { if (!authModule.isNameCallbackAllowed()) { - RepositoryManager.log(null, null, + RemoteLoggingUtil.log( "Illegal authentication callback: NameCallback not permitted", username); - throw new LoginException("Illegal authentication callback"); + throw new FailedLoginException("Illegal authentication callback"); } String name = nameCb.getName(); if (name == null) { - RepositoryManager.log(null, null, + RemoteLoggingUtil.log( "Illegal authentication callback: NameCallback must specify login name", username); - throw new LoginException("Illegal authentication callback"); + throw new FailedLoginException("Illegal authentication callback"); } username = name; } } - RepositoryManager.log(null, null, "Repository server handle requested", username); + RemoteLoggingUtil.log("Repository server handle requested", username); boolean supportPasswordChange = false; if (!anonymousAccess) { @@ -310,10 +304,11 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan username = sshAuthModule.authenticate(mgr.getUserManager(), user, authCallbacks); } - catch (LoginException e) { - RepositoryManager.log(null, null, - "SSH Authentication failed (" + e.getMessage() + ")", username); - throw e; + catch (FailedLoginException e) { + RemoteLoggingUtil.log("SSH Authentication failed (" + e.getMessage() + ")", + username); + // Create new exceptions so we don't leak config info to the client. + throw new FailedLoginException("SSH authentication failed"); } } else if (authModule != null) { @@ -325,14 +320,13 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan if (autoProvisionAuthedUsers) { try { mgr.getUserManager().addUser(username); - RepositoryManager.log(null, null, + RemoteLoggingUtil.log( "User '" + username + "' successful auto provision", username); } catch (DuplicateNameException | IOException e) { - RepositoryManager.log( - null, null, "User '" + username + - "' auto provision failed. Cause: " + e.getMessage(), + RemoteLoggingUtil.log("User '" + username + + "' auto provision failed. Cause: " + e.getMessage(), username); throw new LoginException( "Error when trying to auto provision successfully authenticated user: " + @@ -340,7 +334,7 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan } } else { - RepositoryManager.log(null, null, + RemoteLoggingUtil.log( "User successfully authenticated, but does not exist in Ghidra user list: " + username, null); @@ -351,36 +345,42 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan throw new LoginException("Unknown user: " + username); } } - RepositoryManager.log(null, null, "User '" + username + "' authenticated", + RemoteLoggingUtil.log("User '" + username + "' authenticated", principal.getName()); } } catch (LoginException e) { - RepositoryManager.log(null, null, "Login failed (" + e.getMessage() + ")", + RemoteLoggingUtil.log("Login failed (" + e.getMessage() + ")", username); // Create new exceptions so we don't leak config info to the client. - if (e instanceof FailedLoginException) { - throw new FailedLoginException("User authentication failed"); - } - throw new LoginException("User login system failure"); + throw new FailedLoginException("Authentication failed"); } if (authModule instanceof PasswordFileAuthenticationModule) { supportPasswordChange = true; } } else if (!mgr.getUserManager().isValidUser(username)) { - FailedLoginException e = new FailedLoginException("Unknown user: " + username); - RepositoryManager.log(null, null, "Login failed (" + e.getMessage() + ")", + RemoteLoggingUtil.log("Login failed (Unknown user: " + username + ")", username); - throw e; + // Create new exceptions so we don't leak config info to the client. + throw new FailedLoginException("Authentication failed"); } } if (anonymousAccess) { - RepositoryManager.log(null, null, "Anonymous server access granted", null); + RemoteLoggingUtil.log("Anonymous server access granted", null); } - return new RepositoryServerHandleImpl(username, anonymousAccess, mgr, - supportPasswordChange); + try { + return new RepositoryServerHandleImpl(username, anonymousAccess, mgr, + supportPasswordChange); + } + catch (RemoteException e) { + RemoteLoggingUtil.log( + "Failed to instantiate RepositoryServerHandleImpl: " + e.getMessage(), + username); + e.printStackTrace(); + throw new RemoteException("Remote server handle error (see server log)"); + } } /** @@ -550,8 +550,9 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan configuration.setInitializeLogging(false); Application.initializeApplication(layout, configuration); } - catch (IOException e) { + catch (Throwable t) { System.err.println("Failed to initialize the application!"); + t.printStackTrace(); System.exit(-1); } @@ -729,11 +730,22 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan File serverLogFile = new File(serverRoot, "server.log"); Application.initializeLogging(serverLogFile, serverLogFile); + log = LogManager.getLogger(GhidraServer.class); // init log *after* initializing log system + + // Establish serialization filter to address deserialization vulnerabity concerns + try { + ResourceFile serialFilterFile = Application.getModuleDataFile(SERIAL_FILTER_FILE); + GhidraObjectInputFilter.configureServerSerialFilter(serialFilterFile, + () -> RepositoryManager.getRMIClient()); + } + catch (Throwable t) { + log.fatal("Failed to initialize serialization filter", t); + System.exit(-1); + } + // In the absence of module initialization - we must invoke directly DefaultSSLContextInitializer.initialize(); - log = LogManager.getLogger(GhidraServer.class); // init log *after* initializing log system - ServerPortFactory.setBasePort(basePort); Runtime.getRuntime().addShutdownHook(new Thread((Runnable) () -> { @@ -806,7 +818,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan // localhost.getCanonicalHostName() + ":" + classSvrPort + "/"; // System.setProperty(RMI_CODEBASE_PROPERTY, codeBaseProp); - log.info(" RMI Registry port: " + ServerPortFactory.getRMIRegistryPort()); log.info(" RMI SSL port: " + ServerPortFactory.getRMISSLPort()); log.info(" Block Stream port: " + ServerPortFactory.getStreamPort()); @@ -865,11 +876,6 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan log.info("Registered Ghidra Server."); } - catch (IOException e) { - e.printStackTrace(); - log.error(e.getMessage()); - System.exit(-1); - } catch (Throwable t) { log.fatal("Server error: " + t.getMessage(), t); System.exit(-1); @@ -898,130 +904,12 @@ public class GhidraServer extends UnicastRemoteObject implements GhidraServerHan server.dispose(); } - public static RMIServerSocketFactory getRMIServerSocketFactory() { + static RMIServerSocketFactory getRMIServerSocketFactory() { return serverSocketFactory; } - public static RMIClientSocketFactory getRMIClientSocketFactory() { + static RMIClientSocketFactory getRMIClientSocketFactory() { return clientSocketFactory; } - private static void setGlobalSerializationFilter() throws IOException { - - // NOTE: Serialization filter may need to be disabled when profiling with VisualVM - String disabledStr = System.getProperty(SERIALIZATION_FILTER_DISABLED_PROPERTY); - if (Boolean.valueOf(disabledStr)) { - return; - } - - ObjectInputFilter patternFilter = readSerialFilterPatternFile(); - - ObjectInputFilter filter = new ObjectInputFilter() { - - @Override - public Status checkInput(FilterInfo info) { - - Class clazz = info.serialClass(); - - // Give serial filter patterns first shot - Status status = patternFilter.checkInput(info); - if (status != Status.UNDECIDED) { - if (status == Status.REJECTED) { - return serialReject(info, "failed by serial.filter pattern"); - } - return status; - } - - if (clazz == null) { - return Status.ALLOWED; - } - - Class componentType = clazz.getComponentType(); - if (componentType != null && componentType.isPrimitive()) { - return Status.ALLOWED; // allow all primitive arrays - } - - return serialReject(info, "not allowed"); - } - - private Status serialReject(FilterInfo info, String reason) { - String clientHost = RepositoryManager.getRMIClient(); - StringBuilder buf = new StringBuilder(); - buf.append("Rejected class serialization"); - if (clientHost != null) { - buf.append(" from "); - buf.append(clientHost); - } - buf.append("("); - buf.append(reason); - buf.append(")"); - - Class serialClass = info.serialClass(); - if (serialClass != null) { - buf.append(": "); - buf.append(serialClass.getCanonicalName()); - buf.append(" "); - if (serialClass.getComponentType() != null) { - buf.append("("); - buf.append("array-length="); - buf.append(info.arrayLength()); - buf.append(")"); - } - } - - log.error(buf.toString()); - return Status.REJECTED; - } - - }; - - // Install global serial class filter - ObjectInputFilter.Config.setSerialFilter(filter); - } - - /** - * Read serial.filter file content removing any comments and newlines and generate - * corresponding {@link ObjectInputFilter}. See {@link java.io.ObjectInputFilter.Config#createFilter(String)} - * for filter syntax. - * @return serial filter content - * @throws IOException if file error occurs - */ - private static ObjectInputFilter readSerialFilterPatternFile() throws IOException { - - File serialFilterFile = Application.getModuleDataFile(SERIAL_FILTER_FILE).getFile(false); - if (serialFilterFile == null) { - // jar mode not supported - throw new FileNotFoundException(SERIAL_FILTER_FILE + " not found"); - } - try { - StringBuilder buf = new StringBuilder(); - try (FileReader fr = new FileReader(serialFilterFile); - BufferedReader r = new BufferedReader(fr)) { - - for (String line = r.readLine(); line != null; line = r.readLine()) { - int ix = line.indexOf('#'); - if (ix >= 0) { - // strip comment - line = line.substring(0, ix); - } - line = line.trim(); - if (line.length() == 0) { - continue; - } - if (!line.endsWith(";")) { - throw new IllegalArgumentException( - "all filter statements must end with `;`"); - } - if (line.length() != 0) { - buf.append(line); - } - } - } - return ObjectInputFilter.Config.createFilter(buf.toString()); - } - catch (Exception e) { - throw new IOException("Failed to parse " + SERIAL_FILTER_FILE, e); - } - } - } diff --git a/Ghidra/Features/GhidraServer/src/main/java/db/buffers/RemoteBufferFileImpl.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteBufferFileImpl.java similarity index 74% rename from Ghidra/Features/GhidraServer/src/main/java/db/buffers/RemoteBufferFileImpl.java rename to Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteBufferFileImpl.java index 1f15f7d840..cc1ce44ba8 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/db/buffers/RemoteBufferFileImpl.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteBufferFileImpl.java @@ -4,16 +4,16 @@ * Licensed 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 db.buffers; +package ghidra.server.remote; import java.io.IOException; import java.rmi.NoSuchObjectException; @@ -22,9 +22,9 @@ import java.rmi.server.UnicastRemoteObject; import java.rmi.server.Unreferenced; import java.util.*; +import db.buffers.*; import ghidra.framework.remote.RemoteRepositoryHandle; import ghidra.server.RepositoryManager; -import ghidra.server.remote.*; import ghidra.server.stream.*; /** @@ -44,6 +44,7 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject new HashMap<>(); protected final RepositoryHandleImpl owner; + protected final String user; protected final String associatedFilePath; private final String clientHost; @@ -56,7 +57,7 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject * @param bufferFile buffer file * @param owner owner object to which this instance should be associated. * @param associatedFilePath repository path of file item associated with this buffer file - * @throws RemoteException + * @throws RemoteException if failed to instantiate remote object */ public RemoteBufferFileImpl(LocalBufferFile bufferFile, RepositoryHandleImpl owner, String associatedFilePath) throws RemoteException { @@ -68,6 +69,7 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject if (owner == null || associatedFilePath == null) { throw new IllegalArgumentException("Missing one or more required arguments"); } + this.user = owner.getUser().getName(); this.clientHost = RepositoryManager.getRMIClient(); addInstance(this); } @@ -155,7 +157,7 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject } /** - * Return user name@host associated with open file handle. + * {@return username@host associated with open file handle} */ public String getUserClient() { if (clientHost != null) { @@ -166,7 +168,9 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject /** * Returns list of users with open handles associated with the specified filePath. - * @param filePath file path + * @param repoName repository name + * @param filePath repository file path + * @return users with open file handles */ public static String[] getOpenFileUsers(String repoName, String filePath) { String filePathKey = getFilePathKey(repoName, filePath); @@ -218,63 +222,75 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject } @Override - public int getParameter(String name) throws NoSuchElementException, IOException { + public int getParameter(String name) throws NoSuchElementException { + // NOTE: NoSuchElementException will get encapsulated within a RemoteException return bufferFile.getParameter(name); } @Override - public void setParameter(String name, int value) throws IOException { + public void setParameter(String name, int value) { bufferFile.setParameter(name, value); } @Override - public void clearParameters() throws IOException { + public void clearParameters() { bufferFile.clearParameters(); } @Override - public String[] getParameterNames() throws IOException { + public String[] getParameterNames() { return bufferFile.getParameterNames(); } @Override - public int getBufferSize() throws IOException { + public int getBufferSize() { return bufferFile.getBufferSize(); } @Override - public int getIndexCount() throws IOException { + public int getIndexCount() { return bufferFile.getIndexCount(); } @Override - public int[] getFreeIndexes() throws IOException { + public int[] getFreeIndexes() { return bufferFile.getFreeIndexes(); } @Override - public void setFreeIndexes(int[] indexes) throws IOException { + public void setFreeIndexes(int[] indexes) { bufferFile.setFreeIndexes(indexes); } @Override - public boolean isReadOnly() throws IOException { + public boolean isReadOnly() { return bufferFile.isReadOnly(); } @Override public boolean setReadOnly() throws IOException { - return bufferFile.setReadOnly(); + try { + return bufferFile.setReadOnly(); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, "setReadOnly: " + associatedFilePath, + user); + } } @Override public void close() throws IOException { - bufferFile.close(); - dispose(); + try { + bufferFile.close(); + dispose(); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, "close: " + associatedFilePath, user); + } } @Override - public boolean delete() throws IOException { + public boolean delete() { boolean rc = false; try { rc = bufferFile.delete(); @@ -287,12 +303,24 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject @Override public DataBuffer get(int index) throws IOException { - return bufferFile.get(new DataBuffer(), index); + try { + return bufferFile.get(new DataBuffer(), index); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "get(" + index + "): " + associatedFilePath, user); + } } @Override public void put(DataBuffer buf, int index) throws IOException { - bufferFile.put(buf, index); + try { + bufferFile.put(buf, index); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "put(" + index + "): " + associatedFilePath, user); + } } @Override @@ -307,27 +335,39 @@ public class RemoteBufferFileImpl extends UnicastRemoteObject @Override public BlockStreamHandle getInputBlockStreamHandle() throws IOException { - BlockStreamServer blockStreamServer = BlockStreamServer.getBlockStreamServer(); - InputBlockStream inputBlockStream = bufferFile.getInputBlockStream(); - RemoteInputBlockStreamHandle streamHandle = - new RemoteInputBlockStreamHandle(blockStreamServer, inputBlockStream); - if (!blockStreamServer.registerBlockStream(streamHandle, inputBlockStream)) { - throw new IOException("request failed: block stream server not running"); + try { + BlockStreamServer blockStreamServer = BlockStreamServer.getBlockStreamServer(); + InputBlockStream inputBlockStream = bufferFile.getInputBlockStream(); + RemoteInputBlockStreamHandle streamHandle = + new RemoteInputBlockStreamHandle(blockStreamServer, inputBlockStream); + if (!blockStreamServer.registerBlockStream(streamHandle, inputBlockStream)) { + throw new IOException("request failed: block stream server not running"); + } + return streamHandle; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "getInputBlockStreamHandle: " + associatedFilePath, user); } - return streamHandle; } @Override public BlockStreamHandle getOutputBlockStreamHandle(int blockCount) throws IOException { - BlockStreamServer blockStreamServer = BlockStreamServer.getBlockStreamServer(); - OutputBlockStream outputBlockStream = bufferFile.getOutputBlockStream(blockCount); - RemoteOutputBlockStreamHandle streamHandle = new RemoteOutputBlockStreamHandle( - blockStreamServer, blockCount, outputBlockStream.getBlockSize()); - if (!blockStreamServer.registerBlockStream(streamHandle, outputBlockStream)) { - throw new IOException("request failed: block stream server not running"); + try { + BlockStreamServer blockStreamServer = BlockStreamServer.getBlockStreamServer(); + OutputBlockStream outputBlockStream = bufferFile.getOutputBlockStream(blockCount); + RemoteOutputBlockStreamHandle streamHandle = new RemoteOutputBlockStreamHandle( + blockStreamServer, blockCount, outputBlockStream.getBlockSize()); + if (!blockStreamServer.registerBlockStream(streamHandle, outputBlockStream)) { + throw new IOException("request failed: block stream server not running"); + } + return streamHandle; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "getOutputBlockStreamHandle: " + associatedFilePath, user); } - return streamHandle; } } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteExceptionUtil.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteExceptionUtil.java new file mode 100644 index 0000000000..ab498f946d --- /dev/null +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteExceptionUtil.java @@ -0,0 +1,118 @@ +/* ### + * IP: GHIDRA + * + * Licensed 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 ghidra.server.remote; + +import java.io.IOException; +import java.rmi.RemoteException; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class RemoteExceptionUtil { + + private static final Logger log = LogManager.getLogger(RemoteExceptionUtil.class); + + /** + * Allowed IOExceptions (without cause) that are also defined by + * {@code ghidra/Ghidra/Framework/FileSystem/data/client.rmi.serial.filter}. + */ + private static final Set> allowedIOExceptionClassSet = Set.of( + java.io.IOException.class, + java.io.FileNotFoundException.class, + ghidra.framework.store.ExclusiveCheckoutException.class, + ghidra.util.exception.UserAccessException.class, + ghidra.util.exception.DuplicateFileException.class, + ghidra.util.exception.FileInUseException.class, + ghidra.util.ReadOnlyException.class); + + /** + * Sanitize, log and dispatch exceptions to client and comply with client serialization + * requirements. Any IOException with a cause will be simplified to an IOException without + * a cause. Any other checked exception, {@link RuntimeException}, {@link Throwable} or + * {@link Error} will be logged and produce a simplified {@link RemoteException} without cause. + * + * @param t original exception/error (expected non-IOExceptions which are explicitly thrown should + * be caught and conveyed by called instead of passing to this method). + * @param logDetail operation descipription (required) + * @param user user if known else null + * @return IOException to be thrown + */ + static IOException dispatchIOException(Throwable t, String logDetail, String user) { + return dispatchIOException(t, null, null, logDetail, user); + } + + /** + * Sanitize, log and dispatch exceptions to client and comply with client serialization + * requirements. Any IOException with a cause will be simplified to an IOException without + * a cause. Any other checked exception, {@link RuntimeException}, {@link Throwable} or + * {@link Error} will be logged and produce a simplified {@link RemoteException} without cause. + * + * @param t original exception/error (expected non-IOExceptions which are explicitly thrown should + * be caught and conveyed by called instead of passing to this method). + * @param repositoryName repository name or null + * @param path repository folder/item path or null + * @param logDetail operation descipription (required) + * @param user user if known else null + * @return IOException to be thrown + */ + static IOException dispatchIOException(Throwable t, String repositoryName, String path, + String logDetail, String user) { + + if (t instanceof RemoteException re) { + // Assume this was triggered by a failed remote object instantiation + return re; + } + + Class excClass = t.getClass(); + Throwable cause = t.getCause(); + String excKind; + + if (t instanceof IOException ioe) { + + // Only return allowed IOException class which has no cause + if (cause == null && allowedIOExceptionClassSet.contains(excClass)) { + return ioe; + } + + // Log any IOException which has a cause or is not in the allowed set. + // Return as simple IOException without a cause. + log.error(excClass.getName() + ": " + t.getMessage(), t); + return new IOException(ioe.getMessage()); + } + + if (t instanceof RuntimeException rte) { + excKind = "Runtime Exception"; + } + else if (t instanceof Exception) { + // Unexpected condition: exception should have been caught and handled by caller + excKind = "Checked Exception"; + } + else { + excKind = "Error"; + } + + // Log all non-IOExceptions and return as a RemoteException without cause. + RemoteLoggingUtil.log(repositoryName, path, "ERROR: " + logDetail, user, true); + log.error(excKind + ": " + t, t); + return new RemoteException("Unexpected Server " + excKind); + } + + private RemoteExceptionUtil() { + // No instantiation + } + +} diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteLoggingUtil.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteLoggingUtil.java new file mode 100644 index 0000000000..9b973c0cc5 --- /dev/null +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteLoggingUtil.java @@ -0,0 +1,111 @@ +/* ### + * IP: GHIDRA + * + * Licensed 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 ghidra.server.remote; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import ghidra.server.RepositoryManager; + +public class RemoteLoggingUtil { + + private static Logger log = LogManager.getLogger(GhidraServer.class); + + /** + * Generate log message that contains inforamtion message. + * + * General format where client host may be omitted if unable to determine: + *
+	 *   msg (host)
+	 * 
+ * @param msg log message (required) + * @param user user name or null + */ + public static void log(String msg) { + log(null, null, msg, null, false); + } + + /** + * Generate log message that contains information message and user details. + * + * General format where some portions may be omitted if null: + *
+	 *   msg (user@host)
+	 * 
+ * @param msg log message (required) + * @param user user name or null + */ + public static void log(String msg, String user) { + log(null, null, msg, user, false); + } + + /** + * Generate information or error log message that contains repository, path, message + * and user details. + * + * General format where some portions may be omitted if null: + *
+	 *   [repositoryName]path: msg (user@host)
+	 * 
+ * @param repositoryName repository name or null + * @param path repository file path or null + * @param msg log message (required) + * @param user user name or null + * @param error true if error log else info + */ + public static void log(String repositoryName, String path, String msg, String user, + boolean error) { + StringBuilder buf = new StringBuilder(); + if (repositoryName != null) { + buf.append("["); + buf.append(repositoryName); + buf.append("]"); + } + String host = RepositoryManager.getRMIClient(); + String userStr = user; + if (userStr != null) { + if (host != null) { + userStr += "@" + host; + } + } + else { + userStr = host; + } + if (path != null) { + buf.append(path); + } + if (repositoryName != null || path != null) { + buf.append(": "); + } + buf.append(msg); + if (userStr != null) { + buf.append(" ("); + buf.append(userStr); + buf.append(")"); + } + if (error) { + log.error(buf.toString()); + } + else { + log.info(buf.toString()); + } + } + + private RemoteLoggingUtil() { + // no instantiation + } + +} diff --git a/Ghidra/Features/GhidraServer/src/main/java/db/buffers/RemoteManagedBufferFileImpl.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteManagedBufferFileImpl.java similarity index 53% rename from Ghidra/Features/GhidraServer/src/main/java/db/buffers/RemoteManagedBufferFileImpl.java rename to Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteManagedBufferFileImpl.java index 09546f3e53..84073c5397 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/db/buffers/RemoteManagedBufferFileImpl.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RemoteManagedBufferFileImpl.java @@ -4,21 +4,21 @@ * Licensed 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 db.buffers; +package ghidra.server.remote; import java.io.IOException; import java.rmi.RemoteException; -import ghidra.server.remote.RepositoryHandleImpl; +import db.buffers.*; import ghidra.server.stream.BlockStreamServer; import ghidra.server.stream.RemoteInputBlockStreamHandle; @@ -38,7 +38,7 @@ public class RemoteManagedBufferFileImpl extends RemoteBufferFileImpl * @param managedBufferFile underlying managed buffer file * @param owner associated repository handle instance * @param associatedFilePath associated file path for logging - * @throws RemoteException + * @throws RemoteException if failed to instantiate remote object */ public RemoteManagedBufferFileImpl(LocalManagedBufferFile managedBufferFile, RepositoryHandleImpl owner, String associatedFilePath) throws RemoteException { @@ -48,12 +48,18 @@ public class RemoteManagedBufferFileImpl extends RemoteBufferFileImpl @Override public RemoteManagedBufferFileHandle getSaveFile() throws IOException { - LocalManagedBufferFile sf = (LocalManagedBufferFile) managedBufferFile.getSaveFile(); - return sf != null ? new RemoteManagedBufferFileImpl(sf, owner, associatedFilePath) : null; + try { + LocalManagedBufferFile sf = (LocalManagedBufferFile) managedBufferFile.getSaveFile(); + return sf != null ? new RemoteManagedBufferFileImpl(sf, owner, associatedFilePath) : null; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, "getSaveFile: " + associatedFilePath, + user); + } } @Override - public boolean delete() throws IOException { + public boolean delete() { if (managedBufferFile.getVersion() == 1) { owner.getRepository().log(associatedFilePath, "aborting file creation", owner.getUserName()); @@ -63,44 +69,69 @@ public class RemoteManagedBufferFileImpl extends RemoteBufferFileImpl @Override public void saveCompleted(boolean commit) throws IOException { - if (!commit) { - int version = managedBufferFile.getVersion(); - owner.getRepository().log(associatedFilePath, - "aborting file version " + version + " creation", owner.getUserName()); + try { + if (!commit) { + int version = managedBufferFile.getVersion(); + owner.getRepository().log(associatedFilePath, + "aborting file version " + version + " creation", owner.getUserName()); + } + managedBufferFile.saveCompleted(commit); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, "saveCompleted: " + associatedFilePath, + user); } - managedBufferFile.saveCompleted(commit); } @Override - public boolean canSave() throws IOException { + public boolean canSave() { return managedBufferFile.canSave(); } @Override - public void setVersionComment(String comment) throws IOException { + public void setVersionComment(String comment) { managedBufferFile.setVersionComment(comment); } @Override public RemoteBufferFileHandle getNextChangeDataFile(boolean getFirst) throws IOException { - LocalBufferFile cf = (LocalBufferFile) managedBufferFile.getNextChangeDataFile(getFirst); - return cf != null ? new RemoteBufferFileImpl(cf, owner, associatedFilePath) : null; + try { + LocalBufferFile cf = + (LocalBufferFile) managedBufferFile.getNextChangeDataFile(getFirst); + return cf != null ? new RemoteBufferFileImpl(cf, owner, associatedFilePath) : null; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "getNextChangeDataFile: " + associatedFilePath, user); + } } @Override public RemoteBufferFileHandle getSaveChangeDataFile() throws IOException { - LocalBufferFile cf = (LocalBufferFile) managedBufferFile.getSaveChangeDataFile(); - return cf != null ? new RemoteBufferFileImpl(cf, owner, associatedFilePath) : null; + try { + LocalBufferFile cf = (LocalBufferFile) managedBufferFile.getSaveChangeDataFile(); + return cf != null ? new RemoteBufferFileImpl(cf, owner, associatedFilePath) : null; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "getSaveChangeDataFile: " + associatedFilePath, user); + } } @Override - public long getCheckinID() throws IOException { + public long getCheckinID() { return managedBufferFile.getCheckinID(); } @Override public byte[] getForwardModMapData(int oldVersion) throws IOException { - return managedBufferFile.getForwardModMapData(oldVersion); + try { + return managedBufferFile.getForwardModMapData(oldVersion); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "getForwardModMapData: " + associatedFilePath, user); + } } @Override @@ -111,14 +142,21 @@ public class RemoteManagedBufferFileImpl extends RemoteBufferFileImpl @Override public BlockStreamHandle getInputBlockStreamHandle(byte[] changeMapData) throws IOException { - BlockStreamServer blockStreamServer = BlockStreamServer.getBlockStreamServer(); - InputBlockStream inputBlockStream = managedBufferFile.getInputBlockStream(changeMapData); - RemoteInputBlockStreamHandle streamHandle = - new RemoteInputBlockStreamHandle(blockStreamServer, inputBlockStream); - if (!blockStreamServer.registerBlockStream(streamHandle, inputBlockStream)) { - throw new IOException("request failed: block stream server not running"); + try { + BlockStreamServer blockStreamServer = BlockStreamServer.getBlockStreamServer(); + InputBlockStream inputBlockStream = + managedBufferFile.getInputBlockStream(changeMapData); + RemoteInputBlockStreamHandle streamHandle = + new RemoteInputBlockStreamHandle(blockStreamServer, inputBlockStream); + if (!blockStreamServer.registerBlockStream(streamHandle, inputBlockStream)) { + throw new IOException("request failed: block stream server not running"); + } + return streamHandle; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, + "getInputBlockStream with map: " + associatedFilePath, user); } - return streamHandle; } } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java index d868205915..e0cf51dcb5 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryHandleImpl.java @@ -23,11 +23,11 @@ import java.rmi.server.UnicastRemoteObject; import java.rmi.server.Unreferenced; import java.util.*; -import db.buffers.*; +import db.buffers.LocalManagedBufferFile; +import db.buffers.RemoteManagedBufferFileHandle; import ghidra.framework.remote.*; import ghidra.framework.store.*; import ghidra.server.Repository; -import ghidra.server.RepositoryManager; import ghidra.server.store.RepositoryFile; import ghidra.server.store.RepositoryFolder; import ghidra.util.InvalidNameException; @@ -41,10 +41,8 @@ import ghidra.util.exception.FileInUseException; public class RepositoryHandleImpl extends UnicastRemoteObject implements RemoteRepositoryHandle, Unreferenced { -// private final RepositoryChangeEvent NULL_EVENT = new RepositoryChangeEvent( -// RepositoryChangeEvent.REP_NULL_EVENT, null, null, null, null); - private volatile boolean isValid = true; + private String repoName; private boolean clientActive = true; private String currentUser; private Repository repository; @@ -55,9 +53,9 @@ public class RepositoryHandleImpl extends UnicastRemoteObject /** * Construct a repository handle for a specific user. - * @param user - * @param repository - * @throws RemoteException + * @param user user which corresponds to client + * @param repository repository store to be wrapped + * @throws RemoteException if failed to instantiate remote object */ RepositoryHandleImpl(String user, Repository repository) throws RemoteException { super(ServerPortFactory.getRMISSLPort(), GhidraServer.getRMIClientSocketFactory(), @@ -65,7 +63,8 @@ public class RepositoryHandleImpl extends UnicastRemoteObject this.currentUser = user; this.repository = repository; this.syncObject = repository.getSyncObject(); - RepositoryManager.log(repository.getName(), null, "generated handle", user); + this.repoName = repository.getName(); + repository.log(null, "Generated handle", user); repository.addHandle(this); } @@ -90,7 +89,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject return; } terminateTransientCheckouts(); - RepositoryManager.log(repository.getName(), null, "handle disposed", currentUser); + repository.log(null, "Handle disposed", currentUser); if (eventQueue != null) { synchronized (eventQueue) { eventQueue.clear(); @@ -115,7 +114,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject return; } try { - repository.log(null, "Clearning " + transientCheckouts.size() + " transiet checkouts", + repository.log(null, "Clearing " + transientCheckouts.size() + " transiet checkouts", currentUser); ArrayList pathnames = new ArrayList<>(transientCheckouts.keySet()); @@ -142,7 +141,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject msg = e.toString(); } msg = "Failed to cleanup transient checkouts - server restart may be required: " + msg; - RepositoryManager.log(repository.getName(), null, msg, currentUser); + repository.log(null, msg, currentUser); } } @@ -171,7 +170,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject /** * Post repository change events to the client. - * @param event change event + * @param events change events */ public void dispatchEvents(RepositoryChangeEvent[] events) { synchronized (eventQueue) { @@ -212,7 +211,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject return; } } - RepositoryManager.log(repository.getName(), null, "not listening!", currentUser); + repository.log(null, "Not listening (may be sleeping)!", currentUser); dispose(); } @@ -255,7 +254,13 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public User[] getUserList() throws IOException { synchronized (syncObject) { validate(); - return repository.getUserList(currentUser); + try { + return repository.getUserList(currentUser); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, null, + "Get repository user list", currentUser); + } } } @@ -263,7 +268,12 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public boolean anonymousAccessAllowed() throws IOException { synchronized (syncObject) { validate(); - return repository.anonymousAccessAllowed(); + try { + return repository.anonymousAccessAllowed(); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, null, null, currentUser); + } } } @@ -271,7 +281,13 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public void setUserList(User[] users, boolean anonymousAccessAllowed) throws IOException { synchronized (syncObject) { validate(); - repository.setUserList(currentUser, users, anonymousAccessAllowed); + try { + repository.setUserList(currentUser, users, anonymousAccessAllowed); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, null, + "Set repository user list", currentUser); + } } } @@ -288,7 +304,7 @@ public class RepositoryHandleImpl extends UnicastRemoteObject } @Override - public String[] getServerUserList() throws IOException { + public String[] getServerUserList() throws RemoteException { synchronized (syncObject) { validate(); return repository.getServerUserList(currentUser); @@ -299,30 +315,45 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public String[] getSubfolderList(String folderPath) throws IOException { synchronized (syncObject) { validate(); - RepositoryFolder folder; try { - folder = repository.getFolder(currentUser, folderPath, false); + RepositoryFolder folder; + try { + folder = repository.getFolder(currentUser, folderPath, false); + } + catch (InvalidNameException e) { + throw new AssertException(); + } + if (folder == null) { + return new String[0]; + } + RepositoryFolder[] subfolders = folder.getFolders(); + String[] subfolderNames = new String[subfolders.length]; + for (int i = 0; i < subfolders.length; i++) { + subfolderNames[i] = subfolders[i].getName(); + } + return subfolderNames; } - catch (InvalidNameException e) { - throw new AssertException(); + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, folderPath, + "Get subfolder list", currentUser); } - if (folder == null) { - return new String[0]; - } - RepositoryFolder[] subfolders = folder.getFolders(); - String[] subfolderNames = new String[subfolders.length]; - for (int i = 0; i < subfolders.length; i++) { - subfolderNames[i] = subfolders[i].getName(); - } - return subfolderNames; } } @Override - public int getItemCount() throws IOException { + public int getItemCount() throws UnsupportedOperationException, IOException { synchronized (syncObject) { validate(); - return repository.getItemCount(); + try { + return repository.getItemCount(); + } + catch (UnsupportedOperationException uoe) { + throw uoe; // Not supported by MangledFileSystem + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, null, + "Get item count", currentUser); + } } } @@ -345,6 +376,10 @@ public class RepositoryHandleImpl extends UnicastRemoteObject catch (InvalidNameException e) { throw new AssertException(); } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, folderPath, + "Get item list", currentUser); + } } } @@ -387,13 +422,23 @@ public class RepositoryHandleImpl extends UnicastRemoteObject throws InvalidNameException, IOException { synchronized (syncObject) { validate(); - repository.validateWritePrivilege(currentUser); - RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true); - if (folder == null) { - throw new IOException("Failed to create repository Folder " + parentPath); + try { + repository.validateWritePrivilege(currentUser); + RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true); + if (folder == null) { + throw new IOException("Failed to create repository Folder " + parentPath); + } + folder.createTextDataFile(itemName, fileID, contentType, textData, comment, + currentUser); + } + catch (InvalidNameException e) { + throw e; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Create text item", currentUser); } - folder.createTextDataFile(itemName, fileID, contentType, textData, comment, - currentUser); } } @@ -403,14 +448,24 @@ public class RepositoryHandleImpl extends UnicastRemoteObject throws InvalidNameException, IOException { synchronized (syncObject) { validate(); - repository.validateWritePrivilege(currentUser); - RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true); - if (folder == null) { - throw new IOException("Failed to create repository Folder " + parentPath); + try { + repository.validateWritePrivilege(currentUser); + RepositoryFolder folder = repository.getFolder(currentUser, parentPath, true); + if (folder == null) { + throw new IOException("Failed to create repository Folder " + parentPath); + } + LocalManagedBufferFile bf = folder.createDatabase(itemName, fileID, bufferSize, + contentType, currentUser, projectPath); + return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName)); + } + catch (InvalidNameException e) { + throw e; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Create database item", currentUser); } - LocalManagedBufferFile bf = folder.createDatabase(itemName, fileID, bufferSize, - contentType, currentUser, projectPath); - return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName)); } } @@ -419,12 +474,22 @@ public class RepositoryHandleImpl extends UnicastRemoteObject int minChangeDataVer) throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + LocalManagedBufferFile bf = rf.openDatabase(version, minChangeDataVer, currentUser); + return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName)); + } + catch (FileNotFoundException e) { + throw e; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName) + "@" + version, + "Open database version", currentUser); } - LocalManagedBufferFile bf = rf.openDatabase(version, minChangeDataVer, currentUser); - return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName)); } } @@ -433,12 +498,22 @@ public class RepositoryHandleImpl extends UnicastRemoteObject long checkoutId) throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + LocalManagedBufferFile bf = rf.openDatabase(checkoutId, currentUser); + return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName)); + } + catch (FileNotFoundException e) { + throw e; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName) + ":0x" + Long.toHexString(checkoutId), + "Open database for checkin", currentUser); } - LocalManagedBufferFile bf = rf.openDatabase(checkoutId, currentUser); - return new RemoteManagedBufferFileImpl(bf, this, getPathname(parentPath, itemName)); } } @@ -446,11 +521,21 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public Version[] getVersions(String parentPath, String itemName) throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + return rf.getVersions(currentUser); + } + catch (FileNotFoundException e) { + throw e; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Get versions", currentUser); } - return rf.getVersions(currentUser); } } @@ -458,11 +543,18 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public void deleteItem(String parentPath, String itemName, int version) throws IOException { synchronized (syncObject) { validate(); - repository.validateWritePrivilege(currentUser); - checkFileInUse(parentPath, itemName); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf != null) { - rf.delete(version, currentUser); + try { + repository.validateWritePrivilege(currentUser); + checkFileInUse(parentPath, itemName); + RepositoryFile rf = getFile(parentPath, itemName); + if (rf != null) { + rf.delete(version, currentUser); + } + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName) + "@" + version, + "Delete item version", currentUser); } } } @@ -472,13 +564,23 @@ public class RepositoryHandleImpl extends UnicastRemoteObject String newFolderName) throws InvalidNameException, IOException { synchronized (syncObject) { validate(); - repository.validateWritePrivilege(currentUser); - checkFolderInUse(oldParentPath, oldFolderName); - RepositoryFolder folder = repository.getFolder(currentUser, - oldParentPath + FileSystem.SEPARATOR + oldFolderName, false); - RepositoryFolder newParent = repository.getFolder(currentUser, newParentPath, true); - if (folder != null) { - folder.moveTo(newParent, newFolderName, currentUser); + try { + repository.validateWritePrivilege(currentUser); + checkFolderInUse(oldParentPath, oldFolderName); + RepositoryFolder folder = repository.getFolder(currentUser, + oldParentPath + FileSystem.SEPARATOR + oldFolderName, false); + RepositoryFolder newParent = repository.getFolder(currentUser, newParentPath, true); + if (folder != null) { + folder.moveTo(newParent, newFolderName, currentUser); + } + } + catch (InvalidNameException e) { + throw e; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(oldParentPath, oldFolderName), + "Move folder to: " + getPathname(newParentPath, newFolderName), currentUser); } } } @@ -488,17 +590,27 @@ public class RepositoryHandleImpl extends UnicastRemoteObject String newItemName) throws InvalidNameException, IOException { synchronized (syncObject) { validate(); - repository.validateWritePrivilege(currentUser); - checkFileInUse(oldParentPath, oldItemName); - RepositoryFile rf = getFile(oldParentPath, oldItemName); - if (rf == null) { - throw new FileNotFoundException(oldItemName + " not found in repository"); + try { + repository.validateWritePrivilege(currentUser); + checkFileInUse(oldParentPath, oldItemName); + RepositoryFile rf = getFile(oldParentPath, oldItemName); + if (rf == null) { + throw new FileNotFoundException(oldItemName + " not found in repository"); + } + RepositoryFolder folder = repository.getFolder(currentUser, newParentPath, true); + if (folder == null) { + throw new IOException("Failed to create repository Folder " + newParentPath); + } + rf.moveTo(folder, newItemName, currentUser); } - RepositoryFolder folder = repository.getFolder(currentUser, newParentPath, true); - if (folder == null) { - throw new IOException("Failed to create repository Folder " + newParentPath); + catch (InvalidNameException e) { + throw e; + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(oldParentPath, oldItemName), + "Move item to: " + getPathname(newParentPath, newItemName), currentUser); } - rf.moveTo(folder, newItemName, currentUser); } } @@ -558,17 +670,25 @@ public class RepositoryHandleImpl extends UnicastRemoteObject CheckoutType checkoutType, String projectPath) throws IOException { synchronized (syncObject) { validate(); - repository.validateWritePrivilege(currentUser); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + repository.validateWritePrivilege(currentUser); + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + ItemCheckoutStatus checkoutStatus = + rf.checkout(checkoutType, currentUser, projectPath); + if (checkoutStatus != null && + checkoutStatus.getCheckoutType() == CheckoutType.TRANSIENT) { + addTransientCheckout(rf.getPathname(), checkoutStatus); + } + return checkoutStatus; } - ItemCheckoutStatus checkoutStatus = rf.checkout(checkoutType, currentUser, projectPath); - if (checkoutStatus != null && - checkoutStatus.getCheckoutType() == CheckoutType.TRANSIENT) { - addTransientCheckout(rf.getPathname(), checkoutStatus); + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Checkout item (" + checkoutType + ") to " + projectPath, currentUser); } - return checkoutStatus; } } @@ -577,10 +697,19 @@ public class RepositoryHandleImpl extends UnicastRemoteObject int checkoutVersion) throws IOException { synchronized (syncObject) { validate(); - repository.validateWritePrivilege(currentUser); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf != null) { - rf.updateCheckoutVersion(checkoutId, checkoutVersion, currentUser); + try { + repository.validateWritePrivilege(currentUser); + RepositoryFile rf = getFile(parentPath, itemName); + if (rf != null) { + rf.updateCheckoutVersion(checkoutId, checkoutVersion, currentUser); + } + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Update checkout 0x" + Long.toHexString(checkoutId) + " to version " + + checkoutVersion, + currentUser); } } } @@ -590,10 +719,18 @@ public class RepositoryHandleImpl extends UnicastRemoteObject boolean notify) throws IOException { synchronized (syncObject) { validate(); // relax read-only restriction - RepositoryFile rf = getFile(parentPath, itemName); - if (rf != null) { - rf.terminateCheckout(checkoutId, currentUser, notify); - removeTransientCheckout(rf.getPathname(), checkoutId); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf != null) { + rf.terminateCheckout(checkoutId, currentUser, notify); + removeTransientCheckout(rf.getPathname(), checkoutId); + } + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Terminate checkout 0x" + Long.toHexString(checkoutId), + currentUser); } } } @@ -603,11 +740,19 @@ public class RepositoryHandleImpl extends UnicastRemoteObject throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + return rf.getCheckout(checkoutId, currentUser); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Get checkout status 0x" + Long.toHexString(checkoutId), + currentUser); } - return rf.getCheckout(checkoutId, currentUser); } } @@ -616,11 +761,18 @@ public class RepositoryHandleImpl extends UnicastRemoteObject throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + return rf.getCheckouts(currentUser); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Get checkout list", currentUser); } - return rf.getCheckouts(currentUser); } } @@ -634,6 +786,10 @@ public class RepositoryHandleImpl extends UnicastRemoteObject catch (InvalidNameException e) { throw new AssertException(); } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + folderPath, "Check folder", currentUser); + } } } @@ -641,14 +797,21 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public boolean fileExists(String parentPath, String itemName) throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf; try { - rf = getFile(parentPath, itemName); + RepositoryFile rf; + try { + rf = getFile(parentPath, itemName); + } + catch (FileNotFoundException e) { + return false; + } + return rf != null; } - catch (FileNotFoundException e) { - return false; + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Check item", currentUser); } - return rf != null; } } @@ -667,6 +830,11 @@ public class RepositoryHandleImpl extends UnicastRemoteObject catch (FileNotFoundException e) { return 0; } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Get item length", currentUser); + } } } @@ -674,11 +842,18 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public boolean hasCheckouts(String parentPath, String itemName) throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + return rf.hasCheckouts(); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Has checkouts", currentUser); } - return rf.hasCheckouts(); } } @@ -686,11 +861,18 @@ public class RepositoryHandleImpl extends UnicastRemoteObject public boolean isCheckinActive(String parentPath, String itemName) throws IOException { synchronized (syncObject) { validate(); - RepositoryFile rf = getFile(parentPath, itemName); - if (rf == null) { - throw new FileNotFoundException(itemName + " not found in repository"); + try { + RepositoryFile rf = getFile(parentPath, itemName); + if (rf == null) { + throw new FileNotFoundException(itemName + " not found in repository"); + } + return rf.isCheckinActive(); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, repoName, + getPathname(parentPath, itemName), + "Is checkin active", currentUser); } - return rf.isCheckinActive(); } } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryServerHandleImpl.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryServerHandleImpl.java index c6764281bc..4e185b33f9 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryServerHandleImpl.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/RepositoryServerHandleImpl.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -37,27 +37,13 @@ public class RepositoryServerHandleImpl extends UnicastRemoteObject private final boolean supportPasswordChange; private final boolean readOnly; - /* - * @see ghidra.framework.remote.RepositoryServerHandle#anonymousAccessAllowed() - */ - @Override - public boolean anonymousAccessAllowed() { - return mgr.anonymousAccessAllowed(); - } - - /* - * @see ghidra.framework.remote.RepositoryServerHandle#isReadOnly() - */ - @Override - public boolean isReadOnly() { - return readOnly; - } - /** * Construct a repository server handle for a specific user. * @param user remote user + * @param readOnly true if restricted to read-only use * @param mgr repository manager - * @throws RemoteException + * @param supportPasswordChange true if password change is allowed + * @throws RemoteException if failed to instantiate remote object */ public RepositoryServerHandleImpl(String user, boolean readOnly, RepositoryManager mgr, boolean supportPasswordChange) throws RemoteException { @@ -77,98 +63,103 @@ public class RepositoryServerHandleImpl extends UnicastRemoteObject mgr.dropHandle(this); } - /* - * @see rmitest.RepositoryServerHandle#createRepository(java.lang.String) - */ @Override - public RemoteRepositoryHandle createRepository(String name) throws IOException { - Repository repository = mgr.createRepository(currentUser, name); - return new RepositoryHandleImpl(currentUser, repository); + public boolean anonymousAccessAllowed() { + return mgr.anonymousAccessAllowed(); } - /* - * @see rmitest.RepositoryServerHandle#getRepository(java.lang.String) - */ @Override - public RemoteRepositoryHandle getRepository(String name) throws IOException { + public boolean isReadOnly() { + return readOnly; + } + + @Override + public RemoteRepositoryHandle createRepository(String name) throws IOException { + try { + Repository repository = mgr.createRepository(currentUser, name); + RemoteLoggingUtil.log(name, null, "repository created", currentUser, false); + return new RepositoryHandleImpl(currentUser, repository); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, name, + null, "Create repository", currentUser); + } + } + + @Override + public RemoteRepositoryHandle getRepository(String name) + throws UserAccessException, IOException { System.gc(); - Repository repository = mgr.getRepository(currentUser, name); - if (repository == null) { - return null; + try { + Repository repository = mgr.getRepository(currentUser, name); + if (repository == null) { + return null; + } + return new RepositoryHandleImpl(currentUser, repository); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, name, + null, "Get repository", currentUser); } - return new RepositoryHandleImpl(currentUser, repository); } - /* - * @see ghidra.framework.remote.RepositoryServerHandle#deleteRepository(java.lang.String) - */ @Override public void deleteRepository(String name) throws UserAccessException, IOException { - mgr.deleteRepository(currentUser, name); + try { + mgr.deleteRepository(currentUser, name); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, name, + null, "Delete repository", currentUser); + } } - /* - * @see rmitest.RepositoryServerHandle#getRepositoryNames() - */ @Override public String[] getRepositoryNames() { return mgr.getRepositoryNames(currentUser); } - /* - * @see ghidra.framework.remote.RepositoryServerHandle#getUser() - */ @Override - public String getUser() throws IOException { + public String getUser() { return currentUser; } - /* - * @see ghidra.framework.remote.RepositoryServerHandle#getAllUsers() - */ @Override - public String[] getAllUsers() throws IOException { + public String[] getAllUsers() { if (readOnly) { return new String[0]; } return mgr.getAllUsers(currentUser); } - /* - * @see ghidra.framework.remote.RepositoryServerHandle#canSetPassword() - */ @Override - public boolean canSetPassword() throws RemoteException { + public boolean canSetPassword() { return supportPasswordChange && mgr.getUserManager().canSetPassword(currentUser); } - /* - * @see ghidra.framework.remote.RepositoryServerHandle#getPasswordExpiration() - */ @Override - public long getPasswordExpiration() throws IOException { + public long getPasswordExpiration() { if (canSetPassword()) { return mgr.getUserManager().getPasswordExpiration(currentUser); } return -1; } - /* - * @see ghidra.framework.remote.RepositoryServerHandle#setPassword(char[]) - */ @Override public boolean setPassword(char[] saltedSHA256PasswordHash) throws IOException { - if (!canSetPassword()) { - return false; + try { + if (!canSetPassword()) { + return false; + } + return mgr.getUserManager().setPassword(currentUser, saltedSHA256PasswordHash, false); + } + catch (Throwable t) { + throw RemoteExceptionUtil.dispatchIOException(t, "Set password", currentUser); } - return mgr.getUserManager().setPassword(currentUser, saltedSHA256PasswordHash, false); } - /* - * @see ghidra.framework.remote.RepositoryServerHandle#connected() - */ @Override public void connected() { // do nothing diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt index ce657b70db..767ebe0a74 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/remote/ServerHelp.txt @@ -1,7 +1,8 @@ Ghidra server startup parameters. Command line parameters: [-ip ] [-i #.#.#.#] [-p#] [-n] - [-a#] [-d] [-e] [-jaas ] [-u] [-autoProvision] [-anonymous] [-ssh] + [-a#] [-d] [-e] [-jaas ] [-u] [-autoProvision] + [-anonymous] [-ssh] diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/PKIAuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/PKIAuthenticationModule.java index 4460108900..626610b272 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/PKIAuthenticationModule.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/PKIAuthenticationModule.java @@ -31,8 +31,8 @@ import org.apache.logging.log4j.*; import ghidra.framework.remote.GhidraPrincipal; import ghidra.framework.remote.SignatureCallback; import ghidra.net.*; -import ghidra.server.RepositoryManager; import ghidra.server.UserManager; +import ghidra.server.remote.RemoteLoggingUtil; /** * PKIAuthenticationModule performs client authentication through the @@ -203,7 +203,7 @@ public class PKIAuthenticationModule implements AuthenticationModule { } if (UserManager.ANONYMOUS_USERNAME.equals(username)) { - RepositoryManager.log(null, null, "Anonymous access allowed for: " + + RemoteLoggingUtil.log("Anonymous access allowed for: " + certChain[0].getSubjectX500Principal().toString(), user.getName()); } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java index ab6fab8c31..17eda870b0 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/SSHAuthenticationModule.java @@ -140,10 +140,10 @@ public class SSHAuthenticationModule { * @param subject unauthenticated user ID (must be used if name callback not provided/allowed) * @param callbacks authentication callbacks * @return authenticated user ID (may come from callbacks) - * @throws LoginException if authentication failure occurs + * @throws FailedLoginException if authentication failure occurs */ public String authenticate(UserManager userMgr, Subject subject, Callback[] callbacks) - throws LoginException { + throws FailedLoginException { GhidraPrincipal user = GhidraPrincipal.getGhidraPrincipal(subject); if (user == null) { diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/loginmodule/ExternalProgramLoginModule.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/loginmodule/ExternalProgramLoginModule.java index 5605cdf79e..7b0a789f3d 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/loginmodule/ExternalProgramLoginModule.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/loginmodule/ExternalProgramLoginModule.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -30,7 +30,7 @@ import javax.security.auth.spi.LoginModule; import com.sun.security.auth.UserPrincipal; import generic.concurrent.io.ProcessConsumer; -import ghidra.server.RepositoryManager; +import ghidra.server.remote.RemoteLoggingUtil; import ghidra.util.DateUtils; import ghidra.util.timer.Watchdog; @@ -209,11 +209,11 @@ public class ExternalProgramLoginModule implements LoginModule { process.set(p); ProcessConsumer.consume(p.getInputStream(), stdOutStr -> { - RepositoryManager.log(null, null, extProgramName + " STDOUT: " + stdOutStr, null); + RemoteLoggingUtil.log(extProgramName + " STDOUT: " + stdOutStr); }); ProcessConsumer.consume(p.getErrorStream(), errStr -> { - RepositoryManager.log(null, null, extProgramName + " STDERR: " + errStr, null); + RemoteLoggingUtil.log(extProgramName + " STDERR: " + errStr); }); PrintWriter outputWriter = new PrintWriter(p.getOutputStream()); @@ -230,8 +230,8 @@ public class ExternalProgramLoginModule implements LoginModule { } } catch (IOException | InterruptedException e) { - RepositoryManager.log(null, null, - "Exception when executing " + extProgramName + ":" + e.getMessage(), null); + RemoteLoggingUtil + .log("Exception when executing " + extProgramName + ": " + e.getMessage()); throw new LoginException("Error executing external program"); } finally { diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java index d0b9714bd9..53038ae175 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFile.java @@ -24,7 +24,6 @@ import ghidra.framework.remote.User; import ghidra.framework.store.*; import ghidra.framework.store.local.*; import ghidra.server.Repository; -import ghidra.server.RepositoryManager; import ghidra.util.InvalidNameException; import ghidra.util.exception.UserAccessException; @@ -79,8 +78,7 @@ public class RepositoryFile { pathname += "/"; } pathname += name; - RepositoryManager.log(repository.getName(), pathname, - "file is corrupt or unsupported", null); + repository.log(pathname, "file is corrupt or unsupported", null); throw new FileNotFoundException(pathname + " is corrupt or unsupported"); } } @@ -177,8 +175,7 @@ public class RepositoryFile { "Unsupported operation for " + folderItem.getClass().getSimpleName()); } LocalManagedBufferFile bf = databaseItem.open(version, minChangeDataVer); - repository.log( - getPathname(), "version " + + repository.log(getPathname(), "version " + (version < 0 ? folderItem.getCurrentVersion() : version) + " opened read-only", user); return bf; @@ -316,7 +313,7 @@ public class RepositoryFile { parent.fileMoved(this, oldName, newParent); parent = newParent; pathChanged(); - RepositoryManager.log(repository.getName(), oldPath, "file moved to " + getPathname(), + repository.log(oldPath, "file moved to " + getPathname(), user); } } diff --git a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java index 40bdf83ae2..6bf3163e3c 100644 --- a/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java +++ b/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/store/RepositoryFolder.java @@ -27,7 +27,6 @@ import ghidra.framework.store.*; import ghidra.framework.store.local.LocalFileSystem; import ghidra.framework.store.local.LocalFolderItem; import ghidra.server.Repository; -import ghidra.server.RepositoryManager; import ghidra.util.InvalidNameException; import ghidra.util.exception.DuplicateFileException; import ghidra.util.exception.FileInUseException; @@ -257,7 +256,7 @@ public class RepositoryFolder { // Folder created notification causes RepositoryFolder instance to be added RepositoryFolder rf = getFolder(folderName); - RepositoryManager.log(repository.getName(), rf.getPathname(), "folder created", user); + repository.log(rf.getPathname(), "folder created", user); return rf; } } @@ -289,7 +288,7 @@ public class RepositoryFolder { RepositoryFile rf = new RepositoryFile(repository, fileSystem, this, itemName); fileMap.put(itemName, rf); - RepositoryManager.log(repository.getName(), makePathname(getPathname(), itemName), + repository.log(makePathname(getPathname(), itemName), "file created", user); } } @@ -320,7 +319,7 @@ public class RepositoryFolder { // Buffer file does not yet exist - too early to get folder item needed for RepositoryFile LocalManagedBufferFile bf = fileSystem.createDatabase(getPathname(), itemName, fileID, contentType, bufferSize, user, projectPath); - RepositoryManager.log(repository.getName(), makePathname(getPathname(), itemName), + repository.log(makePathname(getPathname(), itemName), "file created", user); return bf; } @@ -434,8 +433,7 @@ public class RepositoryFolder { throw new IOException("Folder can not be renamed and moved"); } pathChanged(); - RepositoryManager.log(repository.getName(), oldPath, - "folder moved to " + getPathname(), user); + repository.log(oldPath, "folder moved to " + getPathname(), user); } finally { repository.flushChangeEvents(); diff --git a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java index a9f4633155..9cc0ca0e32 100644 --- a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java +++ b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileAdapter.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -27,7 +27,7 @@ import ghidra.util.Msg; */ public class BufferFileAdapter implements BufferFile { - private BufferFileHandle bufferFileHandle; + private final BufferFileHandle bufferFileHandle; /** * Constructor. @@ -39,7 +39,16 @@ public class BufferFileAdapter implements BufferFile { @Override public int getParameter(String name) throws NoSuchElementException, IOException { - return bufferFileHandle.getParameter(name); + try { + return bufferFileHandle.getParameter(name); + } + catch (RemoteException e) { + Throwable cause = e.getCause(); + if (cause instanceof NoSuchElementException nse) { + throw nse; + } + throw e; + } } @Override diff --git a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileHandle.java b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileHandle.java index ad26dff650..f59626632b 100644 --- a/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileHandle.java +++ b/Ghidra/Framework/DB/src/main/java/db/buffers/BufferFileHandle.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -34,6 +34,7 @@ public interface BufferFileHandle { public boolean setReadOnly() throws IOException; /** + * NOTE: NoSuchElementException is runtime so must be handled if wrapped in RemoteException * @see BufferFile#getParameter(java.lang.String) */ public int getParameter(String name) throws NoSuchElementException, IOException; diff --git a/Ghidra/Framework/DB/src/main/java/db/buffers/LocalManagedBufferFile.java b/Ghidra/Framework/DB/src/main/java/db/buffers/LocalManagedBufferFile.java index 7d82990e81..1d1251e2a0 100644 --- a/Ghidra/Framework/DB/src/main/java/db/buffers/LocalManagedBufferFile.java +++ b/Ghidra/Framework/DB/src/main/java/db/buffers/LocalManagedBufferFile.java @@ -274,7 +274,7 @@ public class LocalManagedBufferFile extends LocalBufferFile implements ManagedBu /** * @return version associated with this buffer file */ - int getVersion() { + public int getVersion() { return version; } @@ -284,7 +284,7 @@ public class LocalManagedBufferFile extends LocalBufferFile implements ManagedBu } @Override - public void setVersionComment(String comment) throws IOException { + public void setVersionComment(String comment) { this.comment = comment; } diff --git a/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteBufferFileHandle.java b/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteBufferFileHandle.java index 592ab77d5f..747ae99069 100644 --- a/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteBufferFileHandle.java +++ b/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteBufferFileHandle.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -17,54 +17,55 @@ package db.buffers; import java.io.IOException; import java.rmi.Remote; +import java.rmi.RemoteException; import java.rmi.server.RemoteObjectInvocationHandler; -import java.util.NoSuchElementException; /** * RemoteBufferFileHandle facilitates access to a remote BufferFile * via RMI. *

- * Methods from {@link BufferFileHandle} must be re-declared here + * IMPORTANT: Methods from {@link BufferFileHandle} must be re-declared here * so they may be properly marshalled for remote invocation via RMI. * This became neccessary with an OpenJDK 11.0.6 change made to * {@link RemoteObjectInvocationHandler}. */ public interface RemoteBufferFileHandle extends BufferFileHandle, Remote { @Override - public boolean isReadOnly() throws IOException; + public boolean isReadOnly() throws RemoteException; @Override public boolean setReadOnly() throws IOException; + // NoSuchElementException will get wrapped within RemoteException @Override - public int getParameter(String name) throws NoSuchElementException, IOException; + public int getParameter(String name) throws RemoteException; @Override - public void setParameter(String name, int value) throws IOException; + public void setParameter(String name, int value) throws RemoteException; @Override - public void clearParameters() throws IOException; + public void clearParameters() throws RemoteException; @Override - public String[] getParameterNames() throws IOException; + public String[] getParameterNames() throws RemoteException; @Override - public int getBufferSize() throws IOException; + public int getBufferSize() throws RemoteException; @Override - public int getIndexCount() throws IOException; + public int getIndexCount() throws RemoteException; @Override - public int[] getFreeIndexes() throws IOException; + public int[] getFreeIndexes() throws RemoteException; @Override - public void setFreeIndexes(int[] indexes) throws IOException; + public void setFreeIndexes(int[] indexes) throws RemoteException; @Override public void close() throws IOException; @Override - public boolean delete() throws IOException; + public boolean delete() throws RemoteException; @Override public DataBuffer get(int index) throws IOException; diff --git a/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteManagedBufferFileHandle.java b/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteManagedBufferFileHandle.java index 1bb0b6cd64..42578db311 100644 --- a/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteManagedBufferFileHandle.java +++ b/Ghidra/Framework/DB/src/main/java/db/buffers/RemoteManagedBufferFileHandle.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -17,14 +17,14 @@ package db.buffers; import java.io.IOException; import java.rmi.Remote; +import java.rmi.RemoteException; import java.rmi.server.RemoteObjectInvocationHandler; -import java.util.NoSuchElementException; /** * RemoteManagedBufferFileHandle facilitates access to a ManagedBufferFile * via RMI. *

- * Methods from {@link BufferFileHandle} and {@link ManagedBufferFile} must + * IMPORTANT: Methods from {@link BufferFileHandle} and {@link ManagedBufferFile} must * be re-declared here so they may be properly marshalled for remote invocation via RMI. * This became neccessary with an OpenJDK 11.0.6 change made to * {@link RemoteObjectInvocationHandler}. @@ -35,40 +35,41 @@ public interface RemoteManagedBufferFileHandle extends ManagedBufferFileHandle, // BufferFileHandle methods //-------------------------------------------------------------------------- @Override - public boolean isReadOnly() throws IOException; + public boolean isReadOnly() throws RemoteException; @Override public boolean setReadOnly() throws IOException; + // NoSuchElementException will get wrapped within RemoteException @Override - public int getParameter(String name) throws NoSuchElementException, IOException; + public int getParameter(String name) throws RemoteException; @Override - public void setParameter(String name, int value) throws IOException; + public void setParameter(String name, int value) throws RemoteException; @Override - public void clearParameters() throws IOException; + public void clearParameters() throws RemoteException; @Override - public String[] getParameterNames() throws IOException; + public String[] getParameterNames() throws RemoteException; @Override - public int getBufferSize() throws IOException; + public int getBufferSize() throws RemoteException; @Override - public int getIndexCount() throws IOException; + public int getIndexCount() throws RemoteException; @Override - public int[] getFreeIndexes() throws IOException; + public int[] getFreeIndexes() throws RemoteException; @Override - public void setFreeIndexes(int[] indexes) throws IOException; + public void setFreeIndexes(int[] indexes) throws RemoteException; @Override public void close() throws IOException; @Override - public boolean delete() throws IOException; + public boolean delete() throws RemoteException; @Override public DataBuffer get(int index) throws IOException; @@ -103,10 +104,10 @@ public interface RemoteManagedBufferFileHandle extends ManagedBufferFileHandle, public void saveCompleted(boolean commit) throws IOException; @Override - public boolean canSave() throws IOException; + public boolean canSave() throws RemoteException; @Override - public void setVersionComment(String comment) throws IOException; + public void setVersionComment(String comment) throws RemoteException; @Override public BufferFileHandle getNextChangeDataFile(boolean getFirst) throws IOException; @@ -115,7 +116,7 @@ public interface RemoteManagedBufferFileHandle extends ManagedBufferFileHandle, public BufferFileHandle getSaveChangeDataFile() throws IOException; @Override - public long getCheckinID() throws IOException; + public long getCheckinID() throws RemoteException; @Override public byte[] getForwardModMapData(int oldVersion) throws IOException; diff --git a/Ghidra/Framework/FileSystem/certification.manifest b/Ghidra/Framework/FileSystem/certification.manifest index 049e27ab98..d764205764 100644 --- a/Ghidra/Framework/FileSystem/certification.manifest +++ b/Ghidra/Framework/FileSystem/certification.manifest @@ -1,6 +1,8 @@ ##VERSION: 2.0 Module.manifest||GHIDRA||||END| README.md||GHIDRA||||END| +data/client.rmi.serial.filter||GHIDRA||||END| +data/serialFilterREADME.md||GHIDRA||||END| src/main/java/ghidra/framework/client/package.html||GHIDRA||reviewed||END| src/main/java/ghidra/framework/store/db/package.html||GHIDRA||reviewed||END| src/main/java/ghidra/framework/store/local/package.html||GHIDRA||reviewed||END| diff --git a/Ghidra/Framework/FileSystem/data/client.rmi.serial.filter b/Ghidra/Framework/FileSystem/data/client.rmi.serial.filter new file mode 100644 index 0000000000..1d0d4e90c4 --- /dev/null +++ b/Ghidra/Framework/FileSystem/data/client.rmi.serial.filter @@ -0,0 +1,108 @@ +# Ghidra Server RMI client deserialization filter patterns. +# See GhidraObjectInputFilter javadoc. + +remoteIf=ghidra.framework.remote.GhidraServerHandle; +remoteIf=ghidra.framework.remote.RemoteRepositoryServerHandle; +remoteIf=ghidra.framework.remote.RemoteRepositoryHandle; +remoteIf=db.buffers.RemoteBufferFileHandle; +remoteIf=db.buffers.RemoteManagedBufferFileHandle; + +ghidra.framework.remote.*; +ghidra.framework.store.*; +ghidra.server.stream.*; + +[Lghidra.framework.remote.RepositoryChangeEvent; +[Lghidra.framework.remote.RepositoryItem; +[Lghidra.framework.store.ItemCheckoutStatus; +[Lghidra.framework.store.Version; + +db.buffers.*; + +java.rmi.Remote; +java.rmi.dgc.Lease; +java.rmi.dgc.VMID; +java.rmi.server.ObjID; +java.rmi.server.RemoteObject; +java.rmi.server.RemoteObjectInvocationHandler; +java.rmi.server.UID; + +# RMI exception serialized from server to client +java.rmi.AccessException; +java.rmi.MarshalException; +java.rmi.NoSuchObjectException; +java.rmi.NotBoundException; +java.rmi.RemoteException; +java.rmi.ServerException; +java.rmi.ServerError; +java.rmi.ServerRuntimeException; +java.rmi.server.SocketSecurityException; +java.rmi.UnexpectedException; +java.rmi.UnmarshalException; + +javax.rmi.ssl.SslRMIClientSocketFactory; + +java.lang.reflect.Proxy; + +java.lang.String; +[Ljava.lang.String; + +java.util.Collections$EmptyList; + +javax.security.auth.callback.NameCallback; +javax.security.auth.callback.PasswordCallback; + +javax.security.auth.x500.X500Principal; + +javax.security.auth.callback.Callback; +[Ljavax.security.auth.callback.Callback; +javax.security.auth.x500.X500Principal; +[Ljavax.security.auth.x500.X500Principal; + +# +# Exceptions thrown by Ghidra Server remote objects +# (see above for various RMI remote exception implementations) +# + +java.lang.StackTraceElement; +[Ljava.lang.StackTraceElement; # Throwable stackTrace array field + +java.lang.Throwable; +java.lang.Error; +java.lang.Exception; +java.lang.RuntimeException; + +java.security.GeneralSecurityException; + +# Exceptions explicitly thrown by Ghidra Server remote objects +# as declared by corresponding Remote interface or encapsulated +# by a java.rmi.RemoteException. + +javax.security.auth.login.FailedLoginException; +javax.security.auth.login.LoginException; + +java.lang.UnsupportedOperationException; + +java.util.NoSuchElementException; + +ghidra.util.exception.DuplicateNameException; +ghidra.util.exception.UsrException; +ghidra.util.InvalidNameException; + +# +# IOExceptions +# +# NOTE: Any exception thrown on server with a cause will be logged on server and conveyed back +# to client as a simple IOException without a cause. +# +# NOTE: All IOExceptions added below must be included within RemoteExceptionUtil.allowedIOExceptionClassSet +# + +java.io.IOException; +java.io.FileNotFoundException; + +ghidra.framework.store.ExclusiveCheckoutException; +ghidra.util.exception.UserAccessException; +ghidra.util.exception.DuplicateFileException; +ghidra.util.exception.FileInUseException; +ghidra.util.ReadOnlyException; + diff --git a/Ghidra/Framework/FileSystem/data/serialFilterREADME.md b/Ghidra/Framework/FileSystem/data/serialFilterREADME.md new file mode 100644 index 0000000000..07b59f7033 --- /dev/null +++ b/Ghidra/Framework/FileSystem/data/serialFilterREADME.md @@ -0,0 +1,100 @@ +# Ghidra Serialization Filter + +## Overview +As of version 12.0.5, Ghidra employs serialization input filters to address concerns about potential +serialization vulnerabilities in relation to the use of Java RMI (e.g., Ghidra Server). Filters +are employed by both Ghidra Server and client applications. The consequqnce of this filtering is +that all Java Object deserialization is subject to the filter even when it corresponds to purely +local functionality. This can occur with certain code that relies on serialization to facilitate +object cloning (e.g., `org.apache.commons.collections4.functors.PrototypeFactory`). When such cases occur +it may be neccessary to add allowed classes to a client-side serial input filter. + +The Ghidra application discovers serial input filter specifications (`*.serial.filter`) files within +each Ghidra module's data directory (e.g., `Ghidra/Framework/FileSystem/data`) at startup. The +combined filter set is used to establish a global input serialization filter for Ghidra. + +When adding functionality to Ghidra it may be neccessary to adjust the defined serial filter +specifications. When the filter rejects a class deserialization an `InvalidClassException` will be +thrown and the rejected class name will be logged. The log will need to be consulted since the +exception itself does not convey the name of the offending class. + +## Reference Information +- [Java Serialization Filtering](https://docs.oracle.com/javase/8/docs/technotes/guides/serialization/filters/serialization-filtering.html) + +## Serial Input Filter Format +By default, the filter implementation will allow all primitive types (e.g., `int`, `char`, etc.) +and primitive arrays. Filter files need to specify all other Java classes that should allow +deserialization. It is important to remember that all filter specifications will be combined into +a single global filter. + +**IMPORTANT:** Although supported by Java's serial input filter specification, Ghidra does not +support the class rejection pattern starting with the `!` prefix. This restriction stems from +Ghidra combining all serial filters into a single unordered filter specification. + +The serial input filters (`*.serial.filter`) support the following entry types where each entry +must end with a semicolon `;`. End of line comments may be specified with a leading `#` character. + +- Allowed class name. A class is specified by its full classname including package path: + +``` + java.lang.String; +``` +- Allowed inner class, anonymous class, or compiler‑generated synthetic class: + +``` + ghidra.myplugin.Foo$MyInnerClass; + +``` +- Allowed class array (single dimension). Uses a `[L` prefix before the full classname. +NOTE: Anytime an array is allowed the base class must also be allowed. + +``` + [Ljava.lang.String; +```` +- Allowed class array (two dimension). Uses a `[[L` prefix before the full classname. +NOTE: Anytime an array is allowed the base class must also be allowed. + +``` + [[Ljava.lang.Integer; +```` +- Wildcard class name specification can be used very carefully. The `*` wildcard only spans a single +package, while the `**` wildcard will include all subpackages. Do not allow "Gadget classes" that +can be exploited. + +``` + ghidra.myplugin.*; + [Lghidra.myplugin.*; + ghidra.my*; + [Lghidra.my*; +``` +- Allowed remote interface that employ a dynamic Proxy classes (e.g., Java RMI Remote interface). + +``` + remoteIf=ghidra.remote.MyRemoteIf; +``` +- Maximum number of array elements (default: `32000`). The maximum specified by any filter will be +used. A specified value will be ignore if less than the default. + +``` + maxarray=200000; +``` +- Maximum number of bytes in a serialization stream (default: `33554432` / 32MB). The maximum +specified by any filter will be used. A specified value will be ignore if less than the default. + +``` + maxbytes=100000000; +``` +- Maximum references in a graph between objects (default: `10000`). The maximum specified by any +filter will be used. A specified value will be ignore if less than the default. + +``` + maxrefs=15000; +``` +- Maximum depth of an object graph. (default: `50`). The maximum specified by any filter will be used. +A specified value will be ignore if less than the default. + +``` + maxdepth=75; +``` +**NOTE:** Default values shown above may be adjusted in the future. Please report any filter failures +associated with standard Ghidra features. \ No newline at end of file diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java index be41582f44..72adcb53ae 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/ClientUtil.java @@ -31,7 +31,8 @@ import ghidra.framework.model.ServerInfo; import ghidra.framework.remote.*; import ghidra.framework.remote.security.SSHKeyManager; import ghidra.net.*; -import ghidra.util.*; +import ghidra.util.Msg; +import ghidra.util.SystemUtilities; import ghidra.util.exception.*; import ghidra.util.task.TaskLauncher; import ghidra.util.task.TaskMonitor; @@ -207,6 +208,9 @@ public class ClientUtil { Msg.showError(ClientUtil.class, parent, title, "Access denied: " + repository + "\n" + exc.getMessage()); } + // FIXME: Verify presence and source of ServerException and ServerError which + // both originate from UnicastServerRef.dispatch method and are both forms + // of RemoteException else if ((exc instanceof ServerException) || (exc instanceof ServerError)) { Msg.showError(ClientUtil.class, parent, title, "Exception occurred on the Ghidra Server.", exc.getCause()); diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java index 711bee3bb1..fdffc4072e 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/client/RepositoryAdapter.java @@ -576,6 +576,9 @@ public class RepositoryAdapter implements RemoteAdapterListener { if (t instanceof UnmarshalException) { throw new UnsupportedOperationException(operation); } + if (t instanceof UnsupportedOperationException uoe) { + throw uoe; + } } /* diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraObjectInputFilter.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraObjectInputFilter.java new file mode 100644 index 0000000000..f747073e12 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraObjectInputFilter.java @@ -0,0 +1,542 @@ +/* ### + * IP: GHIDRA + * + * Licensed 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 ghidra.framework.remote; + +import java.io.*; +import java.lang.reflect.Proxy; +import java.rmi.Remote; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import generic.jar.ResourceFile; +import ghidra.framework.Application; + +/** + * {@link GhidraObjectInputFilter} provides global serial input filter for use with Ghidra server + * and client applications. This filter primarily targets RMI deserialization, however as a + * global filter it impacts all deserialization cases which may need to be considered + * when specifying filters. + *

+ * Filter files use syntax as supported by {@link java.io.ObjectInputFilter.Config#createFilter(String)} + * with the addition of {@code remoteIf=<remote-classname>} entries for client filters to + * specify those {@link Remote} interfaces which require the use of dynamic {@link Proxy} class + * implementations for RMI stubs. + *

+ * RMI Server applications should invoke {@link #configureServerSerialFilter(ResourceFile, Supplier)} + * during early initialization with a suitable filter file, while Ghidra client applications should + * invoke {@link #configureClientSerialFilter()} to use all Module data {@code *.serial.filter} files + * to define client deserialization restrictions. + * + * See {@link java.io.ObjectInputFilter.Config#createFilter(String)} for filter file syntax. + * See {@link java.io.ObjectInputFilter.Config#setSerialFilterFactory(java.util.function.BinaryOperator)}. + */ +public class GhidraObjectInputFilter implements ObjectInputFilter { + + private static final String README_PATH = + "Ghidra/Framework/FileSystem/data/serialFilterREADME.md"; + + private static final Logger log = LogManager.getLogger(GhidraObjectInputFilter.class); + + private static final String FILTER_SEARCH_EXTENSION = ".serial.filter"; + + private static final String REMOTE_INTERFACE = "remoteIf"; // RMI Remote Interface + + private static final String MAXARRAY = "maxarray"; + private static final String MAXREFS = "maxrefs"; + private static final String MAXDEPTH = "maxdepth"; + private static final String MAXBYTES = "maxbytes"; + + // NOTE: Be sure to update serialFilterREADME.md if values are updated. + private int MAXARRAY_DEFAULT = 32_000; + private int MAXREFS_DEFAULT = 10_000; + private int MAXDEPTH_DEFAULT = 50; + private int MAXBYTES_DEFAULT = 32 * 1024 * 1024; // 32MB + + private long maxArray; + private long maxRefs; + private long maxDepth; + private long maxBytes; + + private final Set> allowedRemoteInterfaces = new HashSet<>(); + private final AtomicReference patternFilterRef = new AtomicReference<>(); + private final AtomicReference> sourceSupplierRef = new AtomicReference<>(); + + // NOTE: When class tracking is enabled all class deserializations will be permitted and + // all filters will be ignored. An normal process shutdown will dump the list of deserialized + // classes to the file TRACKER_LOG_FILE. + + // IMPORTANT: TRACKER_ENABLED must be set to 'false' when comitted to source control !! + + private static final boolean TRACKER_ENABLED = false; // class tracking enablement + private Map classTracker = new TreeMap<>(); // classnames -> ClassInfo + private File trackerLogFile; + + /** + * Construct global serial input filter. + * + * NOTE: Caller is responsible for installing this instance. + * @throws IllegalStateException if this filter has previously been instantiated. + */ + GhidraObjectInputFilter() throws IllegalStateException { + // Lazy initialization will occur during application initialization. + // See configure methods. + } + + private void initializeFilter(List filterFiles, + Supplier sourceNameSupplier) + throws IllegalStateException { + + if (TRACKER_ENABLED) { + log.warn( + "Object deserialization filter tracking enabled! All deserializations will be ALLOWED."); + } + + ObjectInputFilter filter; + try { + String filterText = readSerialFilterFiles(filterFiles); + + // Include limits if missing from filter text + if (maxArray <= 0) { + maxArray = MAXARRAY_DEFAULT; + filterText += MAXARRAY + "=" + MAXARRAY_DEFAULT + ";"; + } + if (maxRefs <= 0) { + maxRefs = MAXREFS_DEFAULT; + filterText += MAXREFS + "=" + MAXREFS_DEFAULT + ";"; + } + if (maxDepth <= 0) { + maxDepth = MAXDEPTH_DEFAULT; + filterText += MAXDEPTH + "=" + MAXDEPTH_DEFAULT + ";"; + } + if (maxBytes <= 0) { + maxBytes = MAXBYTES_DEFAULT; + filterText += MAXBYTES + "=" + MAXBYTES_DEFAULT + ";"; + } + + // Generate serial filter + filter = ObjectInputFilter.Config.createFilter(filterText); + } + catch (Exception e) { + throw new IllegalStateException("Failed to build serial input filter", e); + } + + if (!sourceSupplierRef.compareAndSet(null, sourceNameSupplier) || + !patternFilterRef.compareAndSet(null, filter)) { + throw new IllegalStateException("Serial input filter previously initialized"); + } + } + + + @Override + public Status checkInput(FilterInfo info) { + + if (TRACKER_ENABLED) { + trackClassDeserialization(info); + return Status.UNDECIDED; + } + + ObjectInputFilter patternFilter = patternFilterRef.get(); + if (patternFilter == null) { + // Uninitialized filter state. + // NOTE: This mode is required to facilitate lazy initialization due to + // Gradle testing frameworks use of serialization. + return Status.UNDECIDED; + } + + if (info.references() > maxRefs) { + return serialReject(info, "maxrefs exceeded: " + info.references()); + } + + if (info.depth() > maxDepth) { + return serialReject(info, "maxdepth exceeded: " + info.depth()); + } + + if (info.streamBytes() > maxBytes) { + return serialReject(info, "maxbytes exceeded: " + info.streamBytes()); + } + + Class clazz = info.serialClass(); + if (clazz != null) { + + // Allow all primitive arrays + if (clazz.isArray()) { + + if (info.arrayLength() > maxArray) { + return serialReject(info, "maxarray exceeded: " + info.arrayLength()); + } + + Class componentType = clazz.getComponentType(); + if (componentType != null && componentType.isPrimitive()) { + return Status.ALLOWED; // allow all primitive arrays + } + } + + // Check for allowed RMI Remote Proxies + else if (clazz.getPackageName().startsWith("jdk.proxy") && Proxy.isProxyClass(clazz)) { + Class[] interfaces = clazz.getInterfaces(); + for (Class iface : interfaces) { + if (allowedRemoteInterfaces.contains(iface)) { + return Status.ALLOWED; + } + } + return serialReject(info, "unknown proxy"); + } + } + + // Give serial filter patterns first shot + Status status = patternFilter.checkInput(info); + if (status == Status.ALLOWED) { + return status; + } + + if (clazz == null) { + return Status.UNDECIDED; + } + + + return serialReject(info, "not allowed"); + } + + private static class ClassInfo { + private final String classname; + private final String module; + private long maxArrayLength; + private long maxDepth; + private long maxRefs; + private long maxBytes; + + ClassInfo(FilterInfo info) { + Class clazz = info.serialClass(); + classname = clazz.getName(); + module = clazz.getModule().getName(); + maxArrayLength = info.arrayLength(); + maxDepth = info.depth(); + maxRefs = info.references(); + maxBytes = info.streamBytes(); + } + + ClassInfo(String csv) { + String[] csvValues = csv.split(","); + if (csvValues.length != 6) { + throw new IllegalArgumentException("Invalid ClassInfo csv"); + } + classname = csvValues[0].trim(); + module = csvValues[1].trim(); + maxArrayLength = Long.parseLong(csvValues[2]); + maxDepth = Long.parseLong(csvValues[3]); + maxRefs = Long.parseLong(csvValues[4]); + maxBytes = Long.parseLong(csvValues[5]); + } + + void update(FilterInfo info) { + maxArrayLength = Math.max(maxArrayLength, info.arrayLength()); + maxDepth = Math.max(maxDepth, info.depth()); + maxRefs = Math.max(maxRefs, info.references()); + maxBytes = Math.max(maxBytes, info.streamBytes()); + } + + @Override + public String toString() { + return String.format("%s,%s,%d,%d,%d,%d", classname, module, maxArrayLength, maxDepth, + maxRefs, maxBytes); + } + + static String getCSVHeader() { + return "Classname, module, max-array-length, max-depth, max-Refs"; + } + } + + private void trackClassDeserialization(FilterInfo info) { + Class clazz = info.serialClass(); + if (clazz == null) { + return; + } + + if (classTracker.isEmpty()) { + log.info( + "Installing deserialization class tracking: " + trackerLogFile); + Thread hook = new Thread(() -> { + List list = classTracker.keySet().stream().collect(Collectors.toList()); + Collections.sort(list); + try (FileWriter w = new FileWriter(trackerLogFile)) { + w.append("# " + ClassInfo.getCSVHeader() + "\n"); + for (String s : list) { + w.append(classTracker.get(s) + "\n"); + } + } + catch (IOException e) { + // ignore - logging may be limited + } + }); + Runtime.getRuntime().addShutdownHook(hook); + + // Consume previously recorded class names + if (trackerLogFile.isFile()) { + try (BufferedReader r = + new BufferedReader(new FileReader(trackerLogFile))) { + for (String line = r.readLine(); line != null; line = r.readLine()) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + ClassInfo classInfo = new ClassInfo(line); + classTracker.put(classInfo.classname, classInfo); + } + } + } + catch (IOException e) { + log.error("Error when consuming file: " + trackerLogFile + "\n", e); + } + } + } + + String classname = clazz.getName(); + ClassInfo classInfo = classTracker.get(classname); + if (classInfo != null) { + classInfo.update(info); + } + else { + classTracker.put(classname, new ClassInfo(info)); + } + } + + /** + * Get the class serialization source. + * If a {@link #sourceSupplierRef} has been set it will be used, otherwise null will be returned. + * + * @return serialized data source name or null + */ + protected String getSourceName() { + Supplier supplier = sourceSupplierRef.get(); + return supplier != null ? supplier.get() : null; + } + + private Status serialReject(FilterInfo info, String reason) { + String dataSourceName = getSourceName(); + StringBuilder buf = new StringBuilder(); + buf.append("Rejected class serialization"); + if (dataSourceName != null) { + buf.append(" from "); + buf.append(dataSourceName); + } + buf.append("("); + buf.append(reason); + buf.append(")"); + + Class serialClass = info.serialClass(); + if (serialClass != null) { + buf.append(": "); + buf.append(serialClass.getCanonicalName()); + buf.append(" "); + if (serialClass.getComponentType() != null) { + buf.append("("); + buf.append("array-length="); + buf.append(info.arrayLength()); + buf.append(")"); + } + } + + buf.append(" (see " + README_PATH + ")"); + + log.error(buf.toString()); + return Status.REJECTED; + } + + private String readSerialFilterFiles(List filterFiles) throws IOException { + + LinkedHashSet filterSet = new LinkedHashSet<>(); // preserves order while preventing duplicates + for (ResourceFile filterFile : filterFiles) { + if (!filterFile.exists()) { + throw new FileNotFoundException("Serialization filter not found: " + filterFile); + } + + ResourceFile p1 = filterFile.getParentFile(); + ResourceFile p2 = p1.getParentFile(); + String path = p2.getName() + "/" + p1.getName() + "/" + filterFile.getName(); + log.debug("Including serial input filter: " + path); + + try (InputStream in = filterFile.getInputStream()) { + try { + readFilterEntries(in, filterSet); + } + catch (IllegalArgumentException | IOException e) { + throw new IOException( + "Failed to parse serialization filter file: " + filterFile, e); + } + } + } + return filterSet.stream().collect(Collectors.joining()); + } + + /** + * Read specified serialization filter file content removing any comments and newlines and generate + * corresponding {@link ObjectInputFilter}. + * @param in filter file input stream + * @param filterSet filter text accumulator set (eliminates duplicate entries). + * @throws IOException if file error occurs + */ + private void readFilterEntries(InputStream in, Set filterSet) + throws IOException { + try (BufferedReader r = new BufferedReader(new InputStreamReader(in))) { + for (String line = r.readLine(); line != null; line = r.readLine()) { + int ix = line.indexOf('#'); + if (ix >= 0) { + // strip comment + line = line.substring(0, ix); + } + line = line.trim(); + if (line.length() == 0) { + continue; + } + if (!line.endsWith(";")) { + throw new IllegalArgumentException( + "All filter statements must end with `;`"); + } + if (line.startsWith("!")) { + throw new IllegalArgumentException( + "The class rejection prefix '!' is not supported"); + } + if (line.indexOf('=') > 0 && consumeSpecialValue(line)) { + continue; + } + if (line.length() != 0) { + filterSet.add(line); + } + } + } + } + + /** + * Check for special filter assignment values. + * @param line filter line + * @return true if entry fully consumed and should be excluded from + * {@link java.io.ObjectInputFilter.Config#createFilter(String)} filter text. + */ + private boolean consumeSpecialValue(String line) { + + int equalIx = line.indexOf('='); + String name = line.substring(0, equalIx); + String valueStr = line.substring(equalIx + 1, line.length() - 1); + + if (REMOTE_INTERFACE.equals(name)) { + // Add allowed Remote interface for proxies + try { + Class ifClass = Class.forName(valueStr); + if (Remote.class.isAssignableFrom(ifClass)) { + allowedRemoteInterfaces.add(ifClass); + return true; + } + } + catch (ClassNotFoundException e) { + // ignore + } + throw new IllegalArgumentException( + "Invalid remote interface '" + valueStr + "'"); + } + + try { + if (MAXARRAY.equals(name)) { + maxArray = Math.max(maxArray, parseLong(name, valueStr, MAXARRAY_DEFAULT)); + } + else if (MAXREFS.equals(name)) { + maxRefs = Math.max(maxRefs, parseLong(name, valueStr, MAXREFS_DEFAULT)); + } + else if (MAXDEPTH.equals(name)) { + maxDepth = Math.max(maxDepth, parseLong(name, valueStr, MAXDEPTH_DEFAULT)); + } + else if (MAXBYTES.equals(name)) { + maxBytes = Math.max(maxBytes, parseLong(name, valueStr, MAXBYTES_DEFAULT)); + } + } + catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid '" + name + "' filter value: " + valueStr); + } + return false; // include in filter + } + + private long parseLong(String name, String valueStr, long defaultMin) { + long value = Long.parseLong(valueStr); + if (value <= 0) { + throw new NumberFormatException("Positive value required"); + } + if (value < defaultMin) { + log.warn("Ignoring '" + name + "=" + valueStr + + "' serial filter entry which is less than " + defaultMin); + return -1; // ignore entry + } + return value; + } + + /** + * Install global deserialization filter factory for a server. This will handle all + * deserialization filtering including SignedObject payloads. + *

+ * This filter will make use of the {@link GhidraSerialFilterFactory} and ensure that it is + * properly installed. + * + * @param filterFile serial filter file + * @param sourceNameSupplier source name supplied for use during logging, or null. It + * is assumed that a the current thread may be used to differentiate a client connection + * over which the serialization is occuring. + * @throws IllegalStateException if error occured building or installing serial input filter + * and related filter factory. + */ + public static void configureServerSerialFilter(ResourceFile filterFile, + Supplier sourceNameSupplier) throws IllegalStateException { + + Objects.requireNonNull(filterFile, "Serial filter resource file is required"); + + GhidraObjectInputFilter serialFilter = + GhidraSerialFilterFactory.getOrInstallInstance().getSerialFilter(); + serialFilter.trackerLogFile = + new File(Application.getUserTempDirectory(), "SerialLogServer.txt"); + serialFilter.initializeFilter(List.of(filterFile), sourceNameSupplier); + } + + /** + * Configure global serial input filter for a client. This will handle all + * deserialization filtering including SignedObject payloads. The object deserialization + * filter will be based on the accumulation of all {@code data/*.serial.filter} files found + * within all Application modules. + *

+ * This filter will make use of the {@link GhidraSerialFilterFactory} and ensure that it is + * properly installed. + * + * @throws IllegalStateException if error occured building or installing serial input filter + * and related filter factory. + */ + public static synchronized void configureClientSerialFilter() + throws IllegalStateException { + + List filterFiles = + Application.findFilesByExtensionInApplication(FILTER_SEARCH_EXTENSION); + if (filterFiles.isEmpty()) { + log.warn("No serial input filter files were found (*" + + FILTER_SEARCH_EXTENSION + ")"); + } + + GhidraObjectInputFilter serialFilter = + GhidraSerialFilterFactory.getOrInstallInstance().getSerialFilter(); + serialFilter.trackerLogFile = + new File(Application.getUserTempDirectory(), "SerialLogClient.txt"); + serialFilter.initializeFilter(filterFiles, null); + } + +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraSerialFilterFactory.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraSerialFilterFactory.java new file mode 100644 index 0000000000..e4b550da41 --- /dev/null +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraSerialFilterFactory.java @@ -0,0 +1,93 @@ +/* ### + * IP: GHIDRA + * + * Licensed 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 ghidra.framework.remote; + +import java.io.ObjectInputFilter; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BinaryOperator; + +/** + * {@link GhidraSerialFilterFactory} provides the serial filter factory which imposes + * {@link GhidraObjectInputFilter} as a global serial input filter. + *

+ * NOTE: With the use of Gradle test JVM instances it may be neccessary for those instances + * to specify this class as the serial filter factory and rely on lazy initialization of the + * {@link GhidraObjectInputFilter global serial filter}. + *

+ * 		-Djdk.serialFilterFactory=ghidra.framework.remote.GhidraSerialFilterFactory
+ * 
+ */ +public class GhidraSerialFilterFactory implements BinaryOperator { + + private static final AtomicReference filterFactoryRef = + new AtomicReference<>(); + + private final GhidraObjectInputFilter globalFilter; + + /** + * Constructor. Caller is reposponsible for installation. + * See {@link ObjectInputFilter.Config#setSerialFilterFactory}. + */ + public GhidraSerialFilterFactory() { + if (!filterFactoryRef.compareAndSet(null, this)) { + throw new IllegalStateException( + "Serial filter factory has previously been instantiated"); + } + globalFilter = new GhidraObjectInputFilter(); + } + + GhidraObjectInputFilter getSerialFilter() { + return globalFilter; + } + + @Override + public ObjectInputFilter apply(ObjectInputFilter current, ObjectInputFilter requested) { + // Merge any existing/requested filter with our strict global filter. + // Our filter is always applied. + if (current == null && requested == null) { + return globalFilter; + } + if (current == null) { + return ObjectInputFilter.merge(requested, globalFilter); + } + if (requested == null) { + return ObjectInputFilter.merge(current, globalFilter); + } + return ObjectInputFilter.merge(ObjectInputFilter.merge(current, requested), + globalFilter); + } + + /** + * Get, and install if neccessary, the serial filter factory instance. If a new factory is + * installed it will have an uninitialized {@link GhidraObjectInputFilter} instance. + *

+ * See {@link java.io.ObjectInputFilter.Config#setSerialFilterFactory(java.util.function.BinaryOperator)}. + * + * @return serial filter factory singleton instance + * @throws IllegalStateException if the serial input factory has already been established and + * cannot be updated. + */ + static synchronized GhidraSerialFilterFactory getOrInstallInstance() + throws IllegalStateException { + GhidraSerialFilterFactory factory = filterFactoryRef.get(); + if (factory != null) { + return factory; + } + GhidraSerialFilterFactory newFactory = new GhidraSerialFilterFactory(); + ObjectInputFilter.Config.setSerialFilterFactory(newFactory); + return newFactory; + } +} diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java index 6fcc450366..f1e6e0c10b 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/GhidraServerHandle.java @@ -20,7 +20,7 @@ import java.rmi.RemoteException; import javax.security.auth.Subject; import javax.security.auth.callback.Callback; -import javax.security.auth.login.LoginException; +import javax.security.auth.login.FailedLoginException; /** * GhidraServerHandle provides access to a remote server. @@ -51,7 +51,7 @@ public interface GhidraServerHandle extends Remote { * older clients the ability to connect to the server. Remote interface remained * unchanged allowing 9.1 clients to connect to 9.0 server. * 12: Revised RepositoryFile serialization to facilitate support for text-data used - * for link-file storage. + * for link-file storage (12.0). */ /** @@ -99,17 +99,17 @@ public interface GhidraServerHandle extends Remote { * @param authCallbacks valid authentication callback objects which have been satisfied, or * null if server does not require authentication. * @return repository server handle. - * @throws LoginException if user authentication fails - * @throws RemoteException + * @throws FailedLoginException if user authentication fails + * @throws RemoteException failed to create remote handle * @see #getAuthenticationCallbacks() */ RemoteRepositoryServerHandle getRepositoryServer(Subject user, Callback[] authCallbacks) - throws LoginException, RemoteException; + throws FailedLoginException, RemoteException; /** * Check server interface compatibility * @param serverInterfaceVersion client/server interface version - * @throws RemoteException + * @throws RemoteException if requested server interface version not available * @see #INTERFACE_VERSION */ void checkCompatibility(int serverInterfaceVersion) throws RemoteException; diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java index 6a291821f5..4c1c4d7a43 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryHandle.java @@ -17,6 +17,7 @@ package ghidra.framework.remote; import java.io.IOException; import java.rmi.Remote; +import java.rmi.RemoteException; import java.rmi.server.RemoteObjectInvocationHandler; import db.buffers.ManagedBufferFileHandle; @@ -33,10 +34,10 @@ import ghidra.util.InvalidNameException; */ public interface RemoteRepositoryHandle extends RepositoryHandle, Remote { @Override - String getName() throws IOException; + String getName() throws RemoteException; @Override - User getUser() throws IOException; + User getUser() throws RemoteException; @Override User[] getUserList() throws IOException; @@ -45,7 +46,7 @@ public interface RemoteRepositoryHandle extends RepositoryHandle, Remote { boolean anonymousAccessAllowed() throws IOException; @Override - String[] getServerUserList() throws IOException; + String[] getServerUserList() throws RemoteException; @Override void setUserList(User[] users, boolean anonymousAccessAllowed) throws IOException; diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryServerHandle.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryServerHandle.java index d62c2676cd..3cb9dc97fd 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryServerHandle.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/RemoteRepositoryServerHandle.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -17,12 +17,13 @@ package ghidra.framework.remote; import java.io.IOException; import java.rmi.Remote; +import java.rmi.RemoteException; import java.rmi.server.RemoteObjectInvocationHandler; /** * RepositoryServerHandle provides access to a remote repository server via RMI. *

- * Methods from {@link RepositoryServerHandle} must be re-declared here + * IMPORTANT: Methods from {@link RepositoryServerHandle} must be re-declared here * so they may be properly marshalled for remote invocation via RMI. * This became neccessary with an OpenJDK 11.0.6 change made to * {@link RemoteObjectInvocationHandler}. @@ -30,10 +31,10 @@ import java.rmi.server.RemoteObjectInvocationHandler; public interface RemoteRepositoryServerHandle extends RepositoryServerHandle, Remote { @Override - boolean anonymousAccessAllowed() throws IOException; + boolean anonymousAccessAllowed() throws RemoteException; @Override - boolean isReadOnly() throws IOException; + boolean isReadOnly() throws RemoteException; @Override RepositoryHandle createRepository(String name) throws IOException; @@ -45,24 +46,24 @@ public interface RemoteRepositoryServerHandle extends RepositoryServerHandle, Re void deleteRepository(String name) throws IOException; @Override - String[] getRepositoryNames() throws IOException; + String[] getRepositoryNames() throws RemoteException; @Override - String getUser() throws IOException; + String getUser() throws RemoteException; @Override - String[] getAllUsers() throws IOException; + String[] getAllUsers() throws RemoteException; @Override - boolean canSetPassword() throws IOException; + boolean canSetPassword() throws RemoteException; @Override - long getPasswordExpiration() throws IOException; + long getPasswordExpiration() throws RemoteException; @Override boolean setPassword(char[] saltedSHA256PasswordHash) throws IOException; @Override - void connected() throws IOException; + void connected() throws RemoteException; } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SignatureCallback.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SignatureCallback.java index 0bc10aa5f5..22d947feb4 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SignatureCallback.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/remote/SignatureCallback.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed 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. @@ -109,39 +108,4 @@ public class SignatureCallback implements Callback, Serializable { return null; } -// private void writeObject(java.io.ObjectOutputStream out) throws IOException { -// -// out.defaultWriteObject(); -// -// try { -// out.writeInt(certChain == null ? -1 : certChain.length); -// if (certChain != null) { -// for (int i = 0; i < certChain.length; i++) { -// out.writeObject(certChain[i].getEncoded()); -// } -// } -// } catch (CertificateEncodingException e) { -// throw new IOException("Can not serialize certificate chain"); -// } -// } -// -// private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { -// -// in.defaultReadObject(); -// -// try { -// int cnt = in.readInt(); -// if (cnt >= 0) { -// CertificateFactory cf = CertificateFactory.getInstance("X509"); -// certChain = new X509Certificate[cnt]; -// for (int i = 0; i < cnt; i++) { -// byte[] bytes = (byte[]) in.readObject(); -// certChain[i] = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes)); -// } -// } -// } catch (CertificateException e) { -// throw new IOException("Can not de-serialize certificate chain"); -// } -// } - } diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/CheckoutManager.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/CheckoutManager.java index bca80e809b..509a1f9f2a 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/CheckoutManager.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/CheckoutManager.java @@ -4,9 +4,9 @@ * Licensed 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. @@ -23,6 +23,7 @@ import org.jdom.input.SAXBuilder; import org.jdom.output.XMLOutputter; import ghidra.framework.store.*; +import ghidra.util.Msg; import ghidra.util.xml.GenericXMLOutputter; import ghidra.util.xml.XmlUtilities; @@ -47,7 +48,7 @@ class CheckoutManager { * @param item folder item * @param create if true an empty checkout data file is written, else the * initial data is read from the file. - * @throws IOException + * @throws IOException if create is true and checkouts data file creation failed */ CheckoutManager(LocalFolderItem item, boolean create) throws IOException { this.item = item; @@ -70,6 +71,7 @@ class CheckoutManager { * @param checkoutType type of checkout * @param user name of user requesting checkout * @param version item version to be checked-out + * @param projectPath client-side project and path * @return checkout data or null if exclusive checkout denied due to * existing checkouts. * @throws IOException if checkout fails @@ -109,6 +111,7 @@ class CheckoutManager { * * @param checkoutId checkout ID to be updated * @param version item version to be associated with checkout + * @throws IOException if item validation fails */ synchronized void updateCheckout(long checkoutId, int version) throws IOException { validate(); @@ -133,7 +136,7 @@ class CheckoutManager { * Terminate the specified checkout * * @param checkoutId checkout ID - * @throws IOException + * @throws IOException @throws IOException if item validation fails or data update fails */ synchronized void endCheckout(long checkoutId) throws IOException { validate(); @@ -156,10 +159,11 @@ class CheckoutManager { } /** - * Returns true if the specified version of the associated item is - * checked-out. + * {@return true if the specified version of the associated item has + * one or more checkedouts} * * @param version the specific version to check for checkouts. + * @throws IOException if item validation fails */ synchronized boolean isCheckedOut(int version) throws IOException { validate(); @@ -173,7 +177,8 @@ class CheckoutManager { } /** - * Returns true if the any version of the associated item is checked-out. + * {@return true if one or more checkouts exist for the associated item} + * @throws IOException if item validation fails */ synchronized boolean isCheckedOut() throws IOException { validate(); @@ -181,10 +186,11 @@ class CheckoutManager { } /** - * Returns the checkout data corresponding to the specified checkout ID. - * Null is returned if checkout ID is not found. + * {@return the checkout data corresponding to the specified checkout ID. + * Null is returned if checkout ID is not found.} * * @param checkoutId checkout ID + * @throws IOException if item validation fails */ synchronized ItemCheckoutStatus getCheckout(long checkoutId) throws IOException { validate(); @@ -192,8 +198,10 @@ class CheckoutManager { } /** - * Returns the checkout data for all existing checkouts of the associated - * item. + * {@return the checkout data for all existing checkouts of the associated + * item.} + * + * @throws IOException if item validation fails */ synchronized ItemCheckoutStatus[] getAllCheckouts() throws IOException { validate(); @@ -207,6 +215,8 @@ class CheckoutManager { * updated, the checkout data will be re-initialized from the file. This is * undesirable and is only required when multiple instances of a * LocalFolderItem are used for a specific item path (e.g., unit testing). + * + * @throws IOException if failed to read checkouts file */ private void validate() throws IOException { if (LocalFileSystem.isRefreshRequired()) { @@ -219,6 +229,11 @@ class CheckoutManager { readCheckoutsFile(); success = true; } + catch (IOException e) { + String msg = "Item validation failed: " + item.getPathName(); + Msg.error(this, msg, e); + throw new IOException(msg); + } finally { if (!success) { nextCheckoutId = oldNextCheckoutId; @@ -231,7 +246,7 @@ class CheckoutManager { /** * Read data from checkout file. * - * @throws IOException + * @throws IOException if reading checkout data fails indicating storage file path */ @SuppressWarnings("unchecked") private void readCheckoutsFile() throws IOException { @@ -265,8 +280,8 @@ class CheckoutManager { checkouts.put(coStatus.getCheckoutId(), coStatus); } } - catch (org.jdom.JDOMException je) { - throw new InvalidObjectException("Invalid checkouts file: " + checkoutsFile); + catch (JDOMException je) { + throw new IOException("Invalid checkouts file: " + checkoutsFile, je); } finally { istream.close(); @@ -278,7 +293,7 @@ class CheckoutManager { * * @param coElement checkout data element * @return checkout data for specified element - * @throws JDOMException + * @throws JDOMException if checkout data parse fails */ ItemCheckoutStatus parseCheckoutElement(Element coElement) throws JDOMException { try { @@ -302,7 +317,7 @@ class CheckoutManager { /** * Write checkout data file. * - * @throws IOException + * @throws IOException if error writing checkout data file occurs */ private void writeCheckoutsFile() throws IOException { @@ -341,14 +356,14 @@ class CheckoutManager { oldFile = new File(checkoutsFile.getParentFile(), checkoutsFile.getName() + ".bak"); oldFile.delete(); if (!checkoutsFile.renameTo(oldFile)) { - throw new IOException("Failed to update checkouts: " + item.getPathName()); + throw new IOException("Failed to update checkout file: " + checkoutsFile); } } if (!tmpFile.renameTo(checkoutsFile)) { if (oldFile != null) { oldFile.renameTo(checkoutsFile); } - throw new IOException("Failed to update checkouts: " + item.getPathName()); + throw new IOException("Failed to update checkout file: " + checkoutsFile); } if (oldFile != null) { oldFile.delete(); diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java index 86849914e6..c8e539e5fd 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/LocalFolderItem.java @@ -21,7 +21,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import ghidra.framework.store.*; -import ghidra.util.Msg; import ghidra.util.ReadOnlyException; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; @@ -155,7 +154,7 @@ public abstract class LocalFolderItem implements FolderItem { } /** - * Returns hidden data directory. + * {@return data storage directory} * NOTE: Even if a data directory is not required this method will still return one to * allow removal of an unknown item type that may or may not use it. */ @@ -170,7 +169,7 @@ public abstract class LocalFolderItem implements FolderItem { } /** - * Return the oldest/minimum version. + * {@return the oldest/minimum version} * @throws IOException thrown if an IO error occurs. */ abstract int getMinimumVersion() throws IOException; @@ -178,7 +177,7 @@ public abstract class LocalFolderItem implements FolderItem { /** * Verify that the specified version of this item is not in use. * @param version the specific version to check for versioned items. - * @throws FileInUseException + * @throws FileInUseException if specified item version is in use or unable to determine */ void checkInUse(int version) throws FileInUseException { synchronized (fileSystem) { @@ -188,7 +187,7 @@ public abstract class LocalFolderItem implements FolderItem { isCheckedOut = checkoutMgr.isCheckedOut(version); } catch (IOException e) { - throw new FileInUseException(getName() + " versioning error", e); + throw new FileInUseException(getName() + " versioning error"); } if (isCheckedOut) { throw new FileInUseException( @@ -203,7 +202,7 @@ public abstract class LocalFolderItem implements FolderItem { /** * Verify that this item is not in use. - * @throws FileInUseException + * @throws FileInUseException if item is in use or unable to determine */ void checkInUse() throws FileInUseException { synchronized (fileSystem) { @@ -216,7 +215,8 @@ public abstract class LocalFolderItem implements FolderItem { isCheckedOut = checkoutMgr.isCheckedOut(); } catch (IOException e) { - throw new FileInUseException(getName() + " versioning error", e); + // A bad checkouts file can cause this (check log) + throw new FileInUseException(getName() + " versioning error"); } if (isCheckedOut) { throw new FileInUseException(getName() + " is checked out"); @@ -231,7 +231,7 @@ public abstract class LocalFolderItem implements FolderItem { /** * Begin the check-in process for a versioned item. * @param checkoutId assigned at time of checkout, becomes the check-in ID. - * @throws FileInUseException + * @throws FileInUseException if specified item version is in use or unable to determine */ void beginCheckin(long checkoutId) throws FileInUseException { synchronized (fileSystem) { @@ -244,7 +244,7 @@ public abstract class LocalFolderItem implements FolderItem { status = checkoutMgr.getCheckout(checkinId); } catch (IOException e) { - throw new FileInUseException(getName() + " versioning error", e); + throw new FileInUseException(getName() + " versioning error"); } String byMsg = status != null ? (" by: " + status.getUser()) : ""; throw new FileInUseException("Another checkin is in progress" + byMsg); @@ -401,7 +401,7 @@ public abstract class LocalFolderItem implements FolderItem { * never be the only version (i.e., minVersion will always be less * than the currentVersion). * @param user user name - * @throws IOException + * @throws IOException if item update failure occurs */ abstract void deleteMinimumVersion(String user) throws IOException; @@ -411,7 +411,7 @@ public abstract class LocalFolderItem implements FolderItem { * never be the only version (i.e., minVersion will always be less * than the currentVersion). * @param user user name - * @throws IOException + * @throws IOException if item update failure occurs */ abstract void deleteCurrentVersion(String user) throws IOException; @@ -419,10 +419,11 @@ public abstract class LocalFolderItem implements FolderItem { * Move this item into a newFolder which has a path of newPath. * @param newFolder new parent directory/folder * @param newStorageName new storage name - * @param newPath new parent path - * @throws DuplicateFileException - * @throws FileInUseException - * @throws IOException + * @param newFolderPath new parent path + * @param newName new item name + * @throws DuplicateFileException if detsination item already exists + * @throws FileInUseException if items appears to be in use + * @throws IOException if item update failure occurs * @see ghidra.framework.store.FileSystem#moveItem */ void moveTo(File newFolder, String newStorageName, String newFolderPath, String newName) @@ -831,8 +832,7 @@ public abstract class LocalFolderItem implements FolderItem { return checkoutMgr != null && checkoutMgr.isCheckedOut(); } catch (IOException e) { - Msg.error(getName() + " versioning error", e); - return true; + return true; // error already logged } } return false; @@ -879,8 +879,8 @@ public abstract class LocalFolderItem implements FolderItem { * Update this non-versioned item with the contents of the specified item which must be * within the same non-versioned fileSystem. If successful, the specified item will be * removed after its content has been moved into this item. - * @param item - * @param checkoutVersion + * @param item source item for update of this item + * @param checkoutVersion version of current checkout * @throws IOException if this file is not a checked-out non-versioned file * or an IO error occurs. */ diff --git a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/RepositoryLogger.java b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/RepositoryLogger.java index bd24648ad8..62bfa50bf5 100644 --- a/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/RepositoryLogger.java +++ b/Ghidra/Framework/FileSystem/src/main/java/ghidra/framework/store/local/RepositoryLogger.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed 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. @@ -18,6 +17,12 @@ package ghidra.framework.store.local; public interface RepositoryLogger { + /** + * Append log with information related to specified folder/item path. + * @param path folder or item path + * @param msg message + * @param user associated user or null + */ void log(String path, String msg, String user); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java b/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java index b14b5af2db..2fc3472107 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/net/ApplicationKeyManagerFactory.java @@ -93,7 +93,7 @@ public class ApplicationKeyManagerFactory { } } catch (IOException e) { - throw new KeyStoreException("Failed to examine keystore: " + keystorePath, e); + throw new KeyStoreException("Failed to open keystore: " + keystorePath, e); } int tryCount = 0; diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/FileInUseException.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/FileInUseException.java index da0269272d..2af0b952b4 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/FileInUseException.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/exception/FileInUseException.java @@ -1,13 +1,12 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed 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. @@ -34,13 +33,4 @@ public class FileInUseException extends IOException { super(msg); } - /** - * Create a new FileInUseException with the given message and cause. - * - * @param msg the exception message. - */ - public FileInUseException(String msg, Throwable cause) { - super(msg, cause); - } - } diff --git a/Ghidra/RuntimeScripts/Common/server/server.conf b/Ghidra/RuntimeScripts/Common/server/server.conf index c1fd8d77fa..89f8cee6b8 100644 --- a/Ghidra/RuntimeScripts/Common/server/server.conf +++ b/Ghidra/RuntimeScripts/Common/server/server.conf @@ -87,17 +87,15 @@ wrapper.java.additional.11=-Ddb.buffers.DataBuffer.compressedOutput=true # timeouts to their maximum values. #wrapper.java.debug.port=18200 -# Uncomment to allow VisualVM Profiling to avoid "Rejected class serialization..." errors -# NOTE: A Java class serialization filter is added for RMI security assurance and should remain -# enabled during normal use. -#wrapper.java.additional.16=-Dghidra.server.serialization.filter.disabled=true - -# Uncomment to enable remote use of VisualVM for profiling -# See JMX documentation for more information: http://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html -#wrapper.java.additional.17=-Dcom.sun.management.jmxremote.port=9010 -#wrapper.java.additional.18=-Dcom.sun.management.jmxremote.local.only=false -#wrapper.java.additional.19=-Dcom.sun.management.jmxremote.authenticate=false -#wrapper.java.additional.20=-Dcom.sun.management.jmxremote.ssl=false +# Uncomment additional java properties below to enable remote use of VisualVM for profiling. +# See JMX documentation for more information: +# http://docs.oracle.com/javase/8/docs/technotes/guides/management/agent.html +# When JMX over RMI is in use the Ghidra Server serialization filters defined by +# file Ghidra/GhidraServer/data/serial.filter may need adjustment. +#wrapper.java.additional.16=-Dcom.sun.management.jmxremote.port=9010 +#wrapper.java.additional.17=-Dcom.sun.management.jmxremote.local.only=false +#wrapper.java.additional.18=-Dcom.sun.management.jmxremote.authenticate=false +#wrapper.java.additional.19=-Dcom.sun.management.jmxremote.ssl=false # YAJSW will by default assume a POSIX spawn for Linux and Mac OS X systems, unfortunately it has # not yet been implemented for Mac OS X. The default process support within YAJSW for Mac OS X is diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java index 0542367773..aa43d38226 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/framework/client/GhidraServerSerialFilterFailureTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.*; import java.io.File; import java.io.InvalidClassException; -import java.rmi.RemoteException; import java.rmi.UnmarshalException; import java.security.Principal; import java.util.HashSet; @@ -117,11 +116,10 @@ public class GhidraServerSerialFilterFailureTest extends AbstractGhidraHeadlessI serverHandle.getRepositoryServer(getBogusUserSubject(), new Callback[0]); fail("serial filter rejection failed to perform"); } - catch (RemoteException e) { + catch (Exception e) { Throwable cause = e.getCause(); - assertTrue("expected remote unmarshall exception", cause instanceof UnmarshalException); - cause = cause.getCause(); - assertTrue("expected remote invalid class exceptionn", + assertTrue("Expected remote unmarshall exception", e instanceof UnmarshalException); + assertTrue("Expected remote invalid class exceptionn", cause instanceof InvalidClassException); } diff --git a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java index 5aebdaa0d7..4f723f7d6c 100644 --- a/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java +++ b/Ghidra/Test/IntegrationTest/src/test.slow/java/ghidra/server/remote/ServerTestUtil.java @@ -528,8 +528,6 @@ public class ServerTestUtil { Thread.sleep(200); if (isServerRegistered(portFactory.getRMIRegistryPort()) && canConnect(portFactory.getRMISSLPort())) { - Msg.info(ServerTestUtil.class, - "Successfully verified Ghidra Server registration and SSL port availability"); success = true; return; } diff --git a/build.gradle b/build.gradle index da978cca88..a47afdd415 100644 --- a/build.gradle +++ b/build.gradle @@ -319,6 +319,33 @@ def getCurrentDateTimeLong() { return formattedDate } +/********************************************************************************* + * Returns true if 'project' has a direct or transitive API project dependency + * on the project with path 'targetPath'. The 'targetPath' should be specified + * in the form ":" + *********************************************************************************/ +boolean hasApiProjectDependency(Project project, String targetPath) { + def visited = [] as Set + + def walk + walk = { Project p -> + if (!visited.add(p)) return false + + def apiDeps = p.configurations.api + .allDependencies + .withType(org.gradle.api.artifacts.ProjectDependency) + + if (apiDeps.any { it.dependencyProject.path == targetPath }) { + return true + } + + return apiDeps.any { dep -> walk(dep.dependencyProject) } + } + + walk(project) +} + + /********************************************************************************* * Returns a list of all the external library paths declared as dependencies for the * given project diff --git a/gradle/javaTestProject.gradle b/gradle/javaTestProject.gradle index bfe455092f..c99348710a 100644 --- a/gradle/javaTestProject.gradle +++ b/gradle/javaTestProject.gradle @@ -142,8 +142,10 @@ def initTestJVM(Task task, String rootDirName) { task.minHeapSize = xms task.maxHeapSize = xmx - task.doFirst { - task.jvmArgs '-DupgradeProgramErrorMessage=' + upgradeProgramErrorMessage, + task.doFirst { + + def args = [ + '-DupgradeProgramErrorMessage=' + upgradeProgramErrorMessage, '-DupgradeTimeErrorMessage=' + upgradeTimeErrorMessage, '-Dlog4j.configurationFile=' + logPropertiesUrl, '-Dghidra.test.property.batch.mode=true', @@ -165,7 +167,10 @@ def initTestJVM(Task task, String rootDirName) { '-Duser.language=en', '-Djdk.attach.allowAttachSelf', '-XX:TieredStopAtLevel=1', - '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=' + debugPort, + + // Note: this following arg is needed to enable remote debug. + + //'-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:' + debugPort, // Allow illegal reflective accesses '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', @@ -175,19 +180,9 @@ def initTestJVM(Task task, String rootDirName) { '--add-opens=java.desktop/javax.swing=ALL-UNNAMED', '--add-opens=java.desktop/javax.swing.text=ALL-UNNAMED' - // Note: this args are used to speed-up the tests, but are not safe for production code + // Note: these args are used to speed-up the tests, but are not safe for production code // -noverify and -XX:TieredStopAtLevel=1 - // Note: modern remote debug invocation; - // -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 - - // - // We removed these lines, which should not be needed in modern JVMs - // -Xdebug - // -Xnoagent - // -Djava.compiler=NONE - // -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 - // // TODO Future Updates: // The test configuration should be updated to support all known modes of operation: @@ -197,6 +192,17 @@ def initTestJVM(Task task, String rootDirName) { // For better command-line usage we will need to update tests such that they can // share a VM, enabling us to elimnate the use of 'forEver 1' in this file. // + ] + + if (hasApiProjectDependency(project, ":FileSystem")) { + // If project has a FileSystem dependency we must assume there may be a + // Ghidra ApplicationConfiguration that will attempt to configure a + // serial filter factory which must be done in a lazy fashion when gradle + // test workers are used. + args << '-Djdk.serialFilterFactory=ghidra.framework.remote.GhidraSerialFilterFactory' + } + + task.jvmArgs args } } /********************************************************************************* diff --git a/gradle/root/test.gradle b/gradle/root/test.gradle index 6eb7ce0b2d..93b6162015 100644 --- a/gradle/root/test.gradle +++ b/gradle/root/test.gradle @@ -310,8 +310,10 @@ def initTestJVM(Task task, String rootDirName) { task.minHeapSize xms task.maxHeapSize xmx - task.doFirst { - task.jvmArgs '-DupgradeProgramErrorMessage=' + upgradeProgramErrorMessage, + task.doFirst { + + def args = [ + '-DupgradeProgramErrorMessage=' + upgradeProgramErrorMessage, '-DupgradeTimeErrorMessage=' + upgradeTimeErrorMessage, '-Dlog4j.configurationFile=' + logPropertiesUrl, '-Dghidra.test.property.batch.mode=true', @@ -332,8 +334,10 @@ def initTestJVM(Task task, String rootDirName) { '-Duser.language=en', '-Djdk.attach.allowAttachSelf', '-XX:TieredStopAtLevel=1', - '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=' + debugPort, - + + // Note: this following arg is needed to enable remote debug. + //'-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:' + debugPort, + // Allow illegal reflective accesses '--add-opens=java.base/java.util.concurrent=ALL-UNNAMED', '--add-opens=java.desktop/sun.awt=ALL-UNNAMED', @@ -342,18 +346,19 @@ def initTestJVM(Task task, String rootDirName) { '--add-opens=java.desktop/javax.swing=ALL-UNNAMED', '--add-opens=java.desktop/javax.swing.text=ALL-UNNAMED' - // Note: this args are used to speed-up the tests, but are not safe for production code - // -noverify and -XX:TieredStopAtLevel=1 + // Note: these args are used to speed-up the tests, but are not safe for production code + // -noverify and -XX:TieredStopAtLevel=1 + ] + + if (hasApiProjectDependency(project, ":FileSystem")) { + // If project has a FileSystem dependency we must assume there may be a + // Ghidra ApplicationConfiguration that will attempt to configure a + // serial filter factory which must be done in a lazy fashion when gradle + // test workers are used. + args << '-Djdk.serialFilterFactory=ghidra.framework.remote.GhidraSerialFilterFactory' + } - // Note: modern remote debug invocation; - // -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 - - // - // We removed these lines, which should not be needed in modern JVMs - // -Xdebug - // -Xnoagent - // -Djava.compiler=NONE - // -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 + task.jvmArgs args } }