Failure Signature in the QF-Test Terminal
The term SUT here means the system under test: the Java process that QF-Test starts, instruments, and drives. With Ignition, the SUT is the Vision Client Launcher, the Designer Launcher, or Perspective Workstation, plus the client JVM each launcher spawns. A setup that ran for months stops attaching after the launchers are updated. Reinstalling QF-Test in a different location and disabling Windows security on the test machine change nothing, because the block sits inside the SUT's own JVM.
The QF-Test terminal shows the launcher starting normally, then a rejection, then an RMI failure:
| Log line (SUT output) | Meaning | Relevance |
|---|---|---|
[CompositeClassRejectListFilter] Initialization performed successfully |
Ignition's class reject filter loaded in this JVM | Confirms the launcher build includes serialization filtering |
JVM-wide ObjectInputFilter set up successfully |
The filter applies to every deserialization in the process, not only Ignition traffic | This is why QF-Test's own RMI traffic is affected |
Platform serialFilter has 88 pattern(s) |
Number of built-in reject patterns | Same count on Vision and Perspective Workstation launchers |
WARNING: Unsupported JavaFX configuration: classes were loaded from 'unnamed module ...' |
JavaFX loaded from the classpath rather than as a module | Benign; not the cause |
Serial class 'java.rmi.server.RemoteObject' REJECTED by Platform pattern 'java.rmi.server.RemoteObject' for security |
The filter vetoed a class during deserialization | Root cause |
java.rmi.UnmarshalException: error unmarshalling return; nested exception is: java.io.InvalidClassException: filter status: REJECTED |
QF-Test's RMI registry lookup could not rebuild the returned object | Consequence of the rejection |
The stack trace pins the failure location: de.qfs.apps.qftest.client.start.ClientStarter.startClient calls java.rmi.Naming.lookup, which fails in sun.rmi.registry.RegistryImpl_Stub.lookup. The trigger chain starts at com.sun.javafx.tk.Toolkit.<init> → de.qfs.apps.qftest.instrument.InstanceTracer.instanceCreated → de.qfs.apps.qftest.qagent.InstanceWaiter. QF-Test's agent waits for the JavaFX toolkit to come up inside the launcher, then tries to open its RMI link back to QF-Test.
Serialization Filter and RMI Stub Mechanism
The term serialization filter here means a java.io.ObjectInputFilter installed JVM-wide. The JVM consults it for every class descriptor read from an object stream and returns ALLOWED, REJECTED, or UNDECIDED. A REJECTED verdict aborts the read with java.io.InvalidClassException: filter status: REJECTED. Ignition's CompositeClassRejectListFilter combines a platform reject list (the 88 patterns) with an allow-list supplied through the system property ignition.serialFilter.allow. The term allow-list entry means a class name or package pattern in that property that overrides a platform reject.
The term RMI stub here means the client-side proxy object that stands in for a remote object. Naming.lookup asks the RMI registry for a named remote object. The registry sends back a serialized stub, and the caller deserializes it. Every RMI stub descends from java.rmi.server.RemoteObject. Modern stubs are also dynamic proxy classes whose invocation handler is itself a RemoteObject. The filter examines each class in the stream, including superclasses and proxy classes. A reject pattern on java.rmi.server.RemoteObject therefore kills every RMI lookup in that JVM.
Ignition blocks these classes deliberately. Java deserialization of RMI and proxy types is a known gadget-chain vector, and the launchers do not need RMI for their own work. QF-Test does need it, because its agent in the SUT talks to the QF-Test process over RMI. The fix narrows the filter by exactly the classes QF-Test's handshake uses, in exactly the JVMs QF-Test attaches to.
Check 1: CompositeClassRejectListFilter Rejection Line
Reading: search the SUT's stdout block in the QF-Test terminal for the string REJECTED by Platform pattern.
- Present: the serialization filter is the cause. Go to Check 2.
-
Absent, but
InvalidClassException: filter status: REJECTEDis present: the filter rejected a class without logging the pattern at the current level. The warning itself says to set the log level to DEBUG for further events. Go to the section on filter debug logging, enable it, rerun, then return to Check 2. - Absent, and the exception is a different RMI error (connection refused, timeout): the filter is not involved. Investigate QF-Test's RMI port, local firewall, and the QF-Test version's supported Java runtime instead.
OS-level security settings and QF-Test install location do not affect this check. The veto happens inside the launcher's JVM after the network connection has already succeeded. error unmarshalling return means bytes arrived and could not be turned back into objects.
Check 2: Rejected Class and Pattern Pair
Reading: the class name in quotes and the pattern after by Platform pattern. The class determines the allow-list entry.
| Rejected class in log | Matching platform pattern | Allow-list entry to add | Where seen |
|---|---|---|---|
java.rmi.server.RemoteObject |
java.rmi.server.RemoteObject |
java.rmi.server.RemoteObject |
Vision Client Launcher; required for Designer and Perspective Workstation as well |
com.sun.proxy.$Proxy6 (number varies) |
com.sun.proxy.** |
com.sun.proxy.** |
Perspective Workstation |
| Any other class | As logged | The exact class name, or the narrowest pattern that covers it | Surfaced by debug logging |
Pattern syntax follows the JDK filter convention: an exact class name matches one class, and a trailing .** matches the package and all subpackages. The $ProxyN suffix is assigned at runtime and changes between runs, so a package pattern is the only workable entry for proxy classes.
Vision and Perspective Workstation reject different classes because they run different bundled Java runtimes. The java.lang.Thread.run frame sits at line 840 in the Vision trace and line 829 in the Perspective Workstation trace, and the reflection frames differ too. Older runtimes generate dynamic proxies as com.sun.proxy.$ProxyN. Newer runtimes place them in jdk.proxyN packages, which a com.sun.proxy.** pattern never matches. The RMI stub's proxy class trips the reject list on one runtime and passes on the other. Treat each application as its own allow-list, and do not copy one application's list to another without running Check 2 on each.
The filter stops at the first rejected class. Allowing java.rmi.server.RemoteObject alone on Perspective Workstation moves the failure to the proxy class rather than clearing it. Expect to run Check 2 once per newly rejected class until the lookup succeeds.
Check 3: Rejecting Process Identity
Reading: the stream label above the rejection, for example ----- visionclientlauncher:stdout ----- or ----- perspectiveworkstation:stdout -----. This label names the QF-Test client, which is the process that emitted the line.
Two separate JVMs are involved for Vision and Designer. Both carry the Ignition filter, and QF-Test attaches to both:
-
Launcher JVM: started from the launcher
.exe. QF-Test attaches here first to click through the launcher UI and select the gateway and project. Its JVM options come from the Launch4j runtime configuration file beside the executable. - Client JVM: the Vision Client or Designer that the launcher spawns. QF-Test attaches again to drive the actual screens. Its JVM options come from the JVM arguments field in the launcher's settings for that application entry.
Decision: when the rejection appears under the launcher label, the .ini file is missing or not being read. When the launcher attaches but the rejection appears after the client window opens, the client-side JVM arguments are missing. Configure both from the start. Fixing only the launcher gets QF-Test through the launcher and fails at the client, and fixing only the client never gets past the launcher.
Check 4: Argument Path per Application
Reading: open the launcher's settings and look for a JVM arguments field. Its presence decides where the client-side argument goes.
| Application | Launcher runtime file (same folder as the .exe) |
ignition.serialFilter.allow value |
Client-side JVM arguments |
|---|---|---|---|
| Vision | visionclientlauncher.l4j.ini |
java.rmi.server.RemoteObject |
Launcher settings for the application entry (three dots → Manage), plus the project-specific settings |
| Designer | designerlauncher.l4j.ini |
java.rmi.server.RemoteObject |
Designer Launcher settings, JVM arguments field |
| Perspective Workstation | perspectiveworkstation.l4j.ini |
java.rmi.server.RemoteObject,com.sun.proxy.** |
No JVM arguments field; the .ini file carries the whole configuration |
For Vision, the launcher's top-level default settings are not reliable for this. Defaults may not propagate to application entries already added to the launcher. Edit the specific entry through its three-dot menu → Manage, and also set the same argument in the project-specific settings (the three stacked dots next to the project name). Placing the argument only in the defaults is the most common reason the client still rejects after the launcher is fixed.
For Perspective Workstation, the missing JVM arguments field is expected. The .ini file with both allow-list entries is sufficient; the Workstation attaches once that file is in place.
List a class once. A value that repeats java.rmi.server.RemoteObject twice works, but the duplicate adds nothing.
Launch4j Runtime Configuration Files
The Ignition launcher executables are Launch4j wrappers around a bundled JVM. At startup, a Launch4j executable reads a file named <exe-name>.l4j.ini from its own folder and passes each non-comment line to the JVM as an option. Lines beginning with # are comments. No file exists by default, so create one per launcher QF-Test drives.
- Locate the launcher executable. The folder depends on the install scope chosen at install time. For the Designer Launcher:
- Installed for all users:
C:\Program Files\Inductive Automation\Designer Launcher - Installed for the current user only:
C:\Users\<user>\AppData\Roaming\Inductive Automation\Designer Launcher
.exeitself. - Installed for all users:
- In Windows Explorer, turn on File name extensions. Without it, a file saved from Notepad as
visionclientlauncher.l4j.inibecomesvisionclientlauncher.l4j.ini.txt. Launch4j ignores it, and nothing in the log reports the problem. Explorer should list the file's type as a configuration file, not a text document. - Create the file with the exact name for that launcher, in the same folder as its
.exe. Writing toC:\Program Filesrequires an elevated editor. - Add the content for the launcher. Vision Client Launcher, with diagnostic logging enabled during setup:
Designer Launcher (# Launch4j runtime config -Dlauncher.CompositeClassRejectListFilter.debug=true -Dignition.serialFilter.allow=java.rmi.server.RemoteObjectdesignerlauncher.l4j.ini):
Perspective Workstation (# Launch4j runtime config -Dignition.serialFilter.allow=java.rmi.server.RemoteObjectperspectiveworkstation.l4j.ini), debug line commented out:# Launch4j runtime config #-Dlauncher.CompositeClassRejectListFilter.debug=true -Dignition.serialFilter.allow=java.rmi.server.RemoteObject,com.sun.proxy.** - Close every running instance of that launcher, then start it again from QF-Test. Launch4j reads the file only at process start.
Put all allow-list entries in one comma-separated value on one line. The allow list is a single system property. If the file contains two -Dignition.serialFilter.allow= lines, the JVM keeps only the last one, and the first line's classes are silently dropped. The same rule applies when a value in the .ini and a value in the launcher's JVM arguments field would reach the same JVM. Each JVM needs the complete list in one place.
The .ini file needs only the JVM arguments that let QF-Test interact with the launcher. Gateway, project, and client configuration stay in the launcher settings.
Client JVM Arguments in Launcher Settings
This applies to Vision and Designer, whose launchers spawn a separate client JVM.
- Open the Vision Client Launcher (or Designer Launcher) outside QF-Test.
- On the application entry QF-Test uses, click the three dots → Manage. Do not rely on the launcher-wide default settings; they may not apply to entries added before the change.
- In the JVM arguments field, add:
Add any further classes found in Check 2 to the same comma-separated value.-Dignition.serialFilter.allow=java.rmi.server.RemoteObject - For Vision, repeat the same argument in the project-specific settings reached through the three stacked dots next to the project name.
- Save, close the launcher, and let QF-Test start it fresh.
Apply these settings only on test workstations. Each allow-list entry reopens a deserialization path that the platform list closes on purpose. Setting the argument on production operator stations, or widening it to something like java.rmi.** to avoid another debug cycle, removes protection the release added deliberately. Keep a dedicated launcher profile or test machine for QF-Test runs, and allow only the classes the debug log names.
Filter Debug Logging for Iterative Allow-Listing
The default INFO level logs the first rejection and then goes quiet (Set log level to DEBUG to log additional details for future events). The debug system property makes the filter log each class decision. That output shows every class QF-Test's handshake needs, rather than one per rerun.
Two property prefixes are in circulation for this switch: -Dignition.CompositeClassRejectListFilter.debug=true and -Dlauncher.CompositeClassRejectListFilter.debug=true. Both appear in working configurations, one in a Perspective Workstation file and the other in a Vision Client Launcher file. The JVM ignores a system property that no code reads, so the decision path is simple:
- During diagnosis, put both debug lines in the
.inifile (and in the client JVM arguments if the client is under test). - Restart the launcher from QF-Test and inspect the SUT stdout block. DEBUG-level lines from
[CompositeClassRejectListFilter]confirm that the file was read and that at least one prefix is active. - No DEBUG lines at all means the file was not read. Recheck the file name, the
.txtextension trap, and the folder against the actual.exelocation before touching the allow list. - Collect every class logged as REJECTED during the QF-Test attach. Add each one to the single
ignition.serialFilter.allowvalue, using exact class names where possible and.**patterns only for runtime-generated names such as$ProxyN. - Rerun until the attach completes with no REJECTED lines.
- Comment out the debug lines with a leading
#, as in the Perspective Workstation example, so production-length test runs do not fill the log.
Verification Sequence
Run these checks in order after both the .ini files and the client JVM arguments are in place. Each check isolates one JVM, so a failure points to one configuration location.
-
Check 1: file identity. In Explorer with extensions visible, the launcher folder shows the
.exeandvisionclientlauncher.l4j.ini(ordesignerlauncher.l4j.ini,perspectiveworkstation.l4j.ini) side by side. Expect no.txtsuffix and the exact spelling from the table in Check 4. -
Check 2: launcher reads the file. With a debug line active, start the launcher from QF-Test. Expect
Initialization performed successfullyfollowed by DEBUG-level[CompositeClassRejectListFilter]output in the launcher's stdout block. -
Check 3: launcher attach. Expect no
Serial class 'java.rmi.server.RemoteObject' REJECTEDline and nojava.rmi.UnmarshalExceptionunder the launcher's stream label. QF-Test reports the SUT client as connected, and recording or component checks work on the launcher window. -
Check 4: Perspective Workstation proxy class. For Perspective Workstation only, expect no
com.sun.proxy.$Proxyrejection. If one appears,com.sun.proxy.**is missing from the single allow-list line, or a second-Dignition.serialFilter.allow=line later in the file is overriding it. - Check 5: client attach. For Vision and Designer, let QF-Test drive the launcher into opening the client. Expect the client JVM to start without a REJECTED warning and QF-Test to attach a second time and drive client components. A rejection here, after a clean Check 3, means the argument is only in the launcher defaults. Add it to the application entry via Manage and to the project-specific settings.
-
Check 6: clean baseline. Comment out the debug lines, restart, and run one full QF-Test suite. Expect a normal INFO-level startup with no
CompositeClassRejectListFilterWARN lines and nofilter status: REJECTEDanywhere in the terminal.
FAQ
What happens if I add the JVM argument in the Vision Client Launcher settings but skip the .ini file?
The launcher's own JVM still rejects java.rmi.server.RemoteObject, so QF-Test fails at the launcher with filter status: REJECTED and never reaches the client. The settings field feeds only the spawned client; the launcher needs visionclientlauncher.l4j.ini beside its .exe.
What happens if the l4j.ini file is saved as a .txt file?
Launch4j looks only for the exact <exe-name>.l4j.ini name, so it ignores the file and the rejection continues with no error about the file. Turn on file name extensions in Explorer, rename it, and confirm with the debug property that CompositeClassRejectListFilter DEBUG lines appear.
What happens if I put two -Dignition.serialFilter.allow lines in the same file?
The JVM keeps only the last value, so classes on the earlier line are rejected again. Combine them into one comma-separated line, for example -Dignition.serialFilter.allow=java.rmi.server.RemoteObject,com.sun.proxy.**.
What happens if Perspective Workstation still rejects com.sun.proxy.$Proxy6 after allowing RemoteObject?
Its bundled runtime generates the RMI stub proxy in the com.sun.proxy package, which matches the platform pattern com.sun.proxy.**. Add com.sun.proxy.** to the allow value in perspectiveworkstation.l4j.ini; Workstation has no JVM arguments field, so the file is the only place to put it.
How do I see every class the Ignition serial filter rejects during a QF-Test attach?
Add -Dignition.CompositeClassRejectListFilter.debug=true and -Dlauncher.CompositeClassRejectListFilter.debug=true to the launcher's .l4j.ini (and to the client JVM arguments), restart, and read the DEBUG output in the QF-Test terminal. Allow-list each rejected class, rerun until no REJECTED lines remain, then comment the debug lines out with #.