diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/SymbolicInstructionVisitor.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/SymbolicInstructionVisitor.java
index 771f7759..2df5873c 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/SymbolicInstructionVisitor.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/SymbolicInstructionVisitor.java
@@ -21,6 +21,8 @@
import de.uzl.its.swat.symbolic.shadow.Frame;
import de.uzl.its.swat.symbolic.shadow.ShadowContext;
import de.uzl.its.swat.symbolic.trace.SymbolicTraceHandler;
+import de.uzl.its.swat.symbolic.value.ContainerPlaceHolder;
+import de.uzl.its.swat.symbolic.invoke.java.util.CollectionInvocation;
import de.uzl.its.swat.symbolic.value.PlaceHolder;
import de.uzl.its.swat.symbolic.value.Value;
import de.uzl.its.swat.symbolic.value.ValueFactory;
@@ -1350,7 +1352,11 @@ public void visitGETVALUE_Object(GETVALUE_Object> inst) throws SymbolicInstruc
stack.pushOperand(ValueFactory.getLambdaObjectValue(inst.v, parentAddress, key));
} else */
- if (peek instanceof PlaceHolder placeHolder) {
+ if (peek instanceof ContainerPlaceHolder containerPlaceHolder) {
+ stack.popOperand();
+ stack.pushOperand(CollectionInvocation.resolve(
+ containerPlaceHolder, inst.val, inst.address, stack, symbolicTraceHandler));
+ } else if (peek instanceof PlaceHolder placeHolder) {
// remove the placeholder value
stack.popOperand();
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java
index d89f7cc2..c755833d 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java
@@ -7,6 +7,7 @@
import de.uzl.its.swat.common.exceptions.ValueConversionException;
import de.uzl.its.swat.common.logging.GlobalLogger;
import de.uzl.its.swat.common.logging.records.InvocationEntry;
+import de.uzl.its.swat.symbolic.invoke.java.util.CollectionInvocation;
import de.uzl.its.swat.symbolic.trace.SymbolicTraceHandler;
import de.uzl.its.swat.symbolic.value.PlaceHolder;
import de.uzl.its.swat.symbolic.value.Value;
@@ -67,6 +68,13 @@ private static boolean hasSymbolicElement(Value, ?> v) throws NotImplementedEx
Type[] types = Type.getArgumentTypes(desc);
logger.trace("Invoking method {} in class {} with arguments {}", name, owner, arguments);
+ // JDK containers: modelled calls only move references, see CollectionInvocation
+ Value, ?> containerResult = CollectionInvocation.invoke(
+ owner, name, desc, arguments.toArray(new Value[0]), isInstance, instance, invokeId);
+ if (containerResult != null) {
+ return containerResult;
+ }
+
// We do that first to include the pointer in instance invocation cases
boolean containsSymbolicArgument = arguments.stream().anyMatch(v -> {
try {
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java
index 21b2849f..a644382b 100644
--- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java
@@ -15,6 +15,7 @@
import de.uzl.its.swat.symbolic.invoke.java.lang.MathInvocation;
import de.uzl.its.swat.symbolic.invoke.java.lang.ShortInvocation;
import de.uzl.its.swat.symbolic.invoke.java.lang.StringInvocation;
+import de.uzl.its.swat.symbolic.invoke.java.lang.SystemInvocation;
import de.uzl.its.swat.symbolic.invoke.java.util.ObjectsInvocation;
import de.uzl.its.swat.symbolic.trace.SymbolicTraceHandler;
import de.uzl.its.swat.symbolic.value.PlaceHolder;
@@ -53,6 +54,7 @@ public final class StaticInvocation {
name, args, desc, symbolicStateHandler);
case "java/lang/Double" -> DoubleInvocation.invokeStaticMethod(
name, args, desc, symbolicStateHandler);
+ case "java/lang/System" -> SystemInvocation.invokeStaticMethod(name, args, desc);
case "java/util/Objects" -> ObjectsInvocation.invokeStaticMethod(
name, args, desc, symbolicStateHandler);
default -> PlaceHolder.instance;
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/lang/SystemInvocation.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/lang/SystemInvocation.java
new file mode 100644
index 00000000..112089f3
--- /dev/null
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/lang/SystemInvocation.java
@@ -0,0 +1,87 @@
+package de.uzl.its.swat.symbolic.invoke.java.lang;
+
+import de.uzl.its.swat.common.exceptions.NotImplementedException;
+import de.uzl.its.swat.symbolic.value.PlaceHolder;
+import de.uzl.its.swat.symbolic.value.Value;
+import de.uzl.its.swat.symbolic.value.VoidValue;
+import de.uzl.its.swat.symbolic.value.primitive.numeric.integral.IntValue;
+import de.uzl.its.swat.symbolic.value.reference.array.AbstractArrayValue;
+import de.uzl.its.swat.symbolic.value.reference.array.ArrayArrayValue;
+import de.uzl.its.swat.symbolic.value.reference.array.ObjectArrayValue;
+import java.lang.reflect.Array;
+import org.objectweb.asm.Type;
+
+public final class SystemInvocation {
+
+ /** Longer copies are not modelled: each element adds a store to the array formula. */
+ static final int MAX_ARRAYCOPY_LENGTH = 4096;
+
+ private SystemInvocation() {}
+
+ public static Value, ?> invokeStaticMethod(String name, Value, ?>[] args, Type[] desc)
+ throws NotImplementedException {
+ if (name.equals("arraycopy") && args.length == 5) {
+ return invokeArraycopy(args);
+ }
+ return PlaceHolder.instance;
+ }
+
+ /**
+ * {@code System.arraycopy(src, srcPos, dest, destPos, length)} with concrete positions and
+ * length: the shadow elements are copied as the JVM copies the elements, as if through a
+ * temporary array. The model runs after the concrete call returned, so the arguments were
+ * valid.
+ */
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static Value, ?> invokeArraycopy(Value, ?>[] args) throws NotImplementedException {
+ for (int i : new int[] {1, 3, 4}) {
+ if (!(args[i] instanceof IntValue) || args[i].isSymbolic()) return PlaceHolder.instance;
+ }
+ int srcPos = ((IntValue) args[1]).concrete;
+ int destPos = ((IntValue) args[3]).concrete;
+ int length = ((IntValue) args[4]).concrete;
+ if (length < 0 || length > MAX_ARRAYCOPY_LENGTH) {
+ return PlaceHolder.instance;
+ }
+ Value, ?> src = args[0];
+ Value, ?> dest = args[2];
+
+ if (src instanceof ObjectArrayValue s && dest instanceof ObjectArrayValue d) {
+ Value, ?>[] from = s.getFields();
+ Value, ?>[] to = d.getFields();
+ if (from == null || to == null || srcPos + length > from.length || destPos + length > to.length) {
+ return PlaceHolder.instance;
+ }
+ System.arraycopy(from, srcPos, to, destPos, length);
+ return VoidValue.instance;
+ }
+
+ if (!(src instanceof AbstractArrayValue s) || !(dest instanceof AbstractArrayValue d)
+ || src.getClass() != dest.getClass() || src instanceof ArrayArrayValue>) {
+ return PlaceHolder.instance;
+ }
+ if (s.concrete == null || d.concrete == null
+ || srcPos + length > Array.getLength(s.concrete) || destPos + length > Array.getLength(d.concrete)) {
+ return PlaceHolder.instance;
+ }
+ // The shadow of an array seen through GETVALUE keeps the real array as its concrete value,
+ // which the call has already updated; its concrete values must not be written again.
+ boolean destIsLive = System.identityHashCode(d.concrete) == d.getAddress();
+ Object liveValues = null;
+ if (destIsLive) {
+ liveValues = Array.newInstance(d.concrete.getClass().getComponentType(), length);
+ System.arraycopy(d.concrete, destPos, liveValues, 0, length);
+ }
+ Value, ?>[] elements = new Value[length];
+ for (int k = 0; k < length; k++) {
+ elements[k] = s.getElement(new IntValue(s.getContext(), srcPos + k));
+ }
+ for (int k = 0; k < length; k++) {
+ d.storeElement(new IntValue(d.getContext(), destPos + k), elements[k]);
+ }
+ if (destIsLive) {
+ System.arraycopy(liveValues, 0, d.concrete, destPos, length);
+ }
+ return VoidValue.instance;
+ }
+}
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/util/CollectionInvocation.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/util/CollectionInvocation.java
new file mode 100644
index 00000000..9647eb0d
--- /dev/null
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/util/CollectionInvocation.java
@@ -0,0 +1,416 @@
+package de.uzl.its.swat.symbolic.invoke.java.util;
+
+import ch.qos.logback.classic.Logger;
+import de.uzl.its.swat.common.exceptions.NoThreadContextException;
+import de.uzl.its.swat.common.exceptions.NotImplementedException;
+import de.uzl.its.swat.common.logging.GlobalLogger;
+import de.uzl.its.swat.common.logging.records.InvocationEntry;
+import de.uzl.its.swat.symbolic.shadow.ShadowContext;
+import de.uzl.its.swat.symbolic.trace.SymbolicTraceHandler;
+import de.uzl.its.swat.symbolic.value.ContainerPlaceHolder;
+import de.uzl.its.swat.symbolic.value.ContainerPlaceHolder.Result;
+import de.uzl.its.swat.symbolic.value.PlaceHolder;
+import de.uzl.its.swat.symbolic.value.Value;
+import de.uzl.its.swat.symbolic.value.ValueFactory;
+import de.uzl.its.swat.symbolic.value.primitive.numeric.integral.IntValue;
+import de.uzl.its.swat.symbolic.value.reference.ContainerContents;
+import de.uzl.its.swat.symbolic.value.reference.ContainerContents.Kind;
+import de.uzl.its.swat.symbolic.value.reference.ObjectValue;
+import de.uzl.its.swat.symbolic.value.reference.array.AbstractArrayValue;
+import de.uzl.its.swat.symbolic.value.reference.array.ObjectArrayValue;
+import de.uzl.its.swat.symbolic.value.reference.lang.BoxedValue;
+import de.uzl.its.swat.symbolic.value.reference.lang.StringValue;
+import de.uzl.its.swat.thread.ThreadHandler;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Models of JDK containers ({@code java.util} lists, deques and hash maps, their views and
+ * iterators, and a few {@code Collections}/{@code Arrays} factories) that only move references:
+ * storing an element, copying a container, returning an element, a view or an iterator.
+ *
+ *
The concrete call still runs; the model records the shadow of what goes in (the {@link
+ * ContainerContents} of the container's shadow) and, for a call that returns a reference, picks
+ * the shadow of what comes out when the concrete result is known ({@link #resolve}). A call that
+ * inspects the elements ({@code contains}, {@code indexOf}, {@code remove(Object)}, sorting,
+ * {@code equals}, {@code hashCode}, {@code toString}, streams, ...) is not modelled: a container
+ * holding a symbolic value counts as symbolic, so such a call records context loss as before.
+ *
+ *
Hashed parts are modelled only while they are concrete: a map key must not be symbolic, and a
+ * map whose stored keys have become symbolic is not modelled any more. Index arguments must be
+ * concrete too. Anything else falls back to the generic handling of a missing invocation.
+ */
+public final class CollectionInvocation {
+ private static final Logger logger = GlobalLogger.getSymbolicExecutionLogger();
+
+ /** The JDK classes whose shadow tracks contents, with the calls modelled on them. */
+ private static final Map KIND_BY_CLASS = new HashMap<>();
+
+ static {
+ for (String c : List.of("ArrayList", "LinkedList", "Vector", "Stack", "ArrayDeque")) {
+ KIND_BY_CLASS.put("java/util/" + c, Kind.LIST);
+ }
+ for (String c : List.of("HashMap", "LinkedHashMap", "Hashtable")) {
+ KIND_BY_CLASS.put("java/util/" + c, Kind.MAP);
+ }
+ }
+
+ private enum Op {
+ INIT, // a new, empty container
+ STORE, // stores argument `arg` (lists) or the value `arg` under key `key` (maps)
+ COPY, // stores what the container in argument `arg` holds
+ ELEMENT, // returns an element
+ STORE_ELEMENT, // STORE, then returns an element (set, put, Stack.push)
+ VIEW, // returns a view or iterator of the container
+ ARRAY, // returns a new Object[] with the elements
+ PLAIN // returns a primitive or nothing and only depends on concrete state
+ }
+
+ /**
+ * @param op what the call does
+ * @param arg the argument stored or copied, -1 for none
+ * @param key the map-key argument, which must be concrete, -1 for none
+ * @param initFirst whether the call is a constructor that starts with empty contents
+ */
+ private record Model(Op op, int arg, int key, boolean initFirst) {}
+
+ private static final Map LIST = new HashMap<>();
+ private static final Map MAP = new HashMap<>();
+ private static final Map VIEW = new HashMap<>();
+
+ private static void put(Map table, Op op, int arg, int key, String... sigs) {
+ for (String sig : sigs) table.put(sig, new Model(op, arg, key, sig.startsWith("")));
+ }
+
+ static {
+ put(LIST, Op.INIT, -1, -1, "()V", "(I)V");
+ put(LIST, Op.COPY, 0, -1, "(Ljava/util/Collection;)V", "addAll(Ljava/util/Collection;)Z");
+ put(LIST, Op.COPY, 1, -1, "addAll(ILjava/util/Collection;)Z");
+ put(LIST, Op.STORE, 0, -1,
+ "add(Ljava/lang/Object;)Z", "addLast(Ljava/lang/Object;)V", "addFirst(Ljava/lang/Object;)V",
+ "offer(Ljava/lang/Object;)Z", "offerLast(Ljava/lang/Object;)Z", "offerFirst(Ljava/lang/Object;)Z",
+ "push(Ljava/lang/Object;)V", "addElement(Ljava/lang/Object;)V",
+ "insertElementAt(Ljava/lang/Object;I)V", "setElementAt(Ljava/lang/Object;I)V");
+ put(LIST, Op.STORE, 1, -1, "add(ILjava/lang/Object;)V");
+ put(LIST, Op.STORE_ELEMENT, 1, -1, "set(ILjava/lang/Object;)Ljava/lang/Object;");
+ put(LIST, Op.STORE_ELEMENT, 0, -1, "push(Ljava/lang/Object;)Ljava/lang/Object;");
+ put(LIST, Op.ELEMENT, -1, -1,
+ "get(I)Ljava/lang/Object;", "getFirst()Ljava/lang/Object;", "getLast()Ljava/lang/Object;",
+ "element()Ljava/lang/Object;", "peek()Ljava/lang/Object;", "peekFirst()Ljava/lang/Object;",
+ "peekLast()Ljava/lang/Object;", "poll()Ljava/lang/Object;", "pollFirst()Ljava/lang/Object;",
+ "pollLast()Ljava/lang/Object;", "pop()Ljava/lang/Object;", "remove()Ljava/lang/Object;",
+ "remove(I)Ljava/lang/Object;", "removeFirst()Ljava/lang/Object;", "removeLast()Ljava/lang/Object;",
+ "firstElement()Ljava/lang/Object;", "lastElement()Ljava/lang/Object;",
+ "elementAt(I)Ljava/lang/Object;");
+ put(LIST, Op.VIEW, -1, -1,
+ "iterator()Ljava/util/Iterator;", "listIterator()Ljava/util/ListIterator;",
+ "listIterator(I)Ljava/util/ListIterator;", "descendingIterator()Ljava/util/Iterator;",
+ "elements()Ljava/util/Enumeration;", "subList(II)Ljava/util/List;");
+ put(LIST, Op.ARRAY, -1, -1, "toArray()[Ljava/lang/Object;");
+ put(LIST, Op.PLAIN, -1, -1,
+ "size()I", "isEmpty()Z", "clear()V", "trimToSize()V", "ensureCapacity(I)V");
+
+ put(MAP, Op.INIT, -1, -1, "()V", "(I)V", "(IF)V");
+ put(MAP, Op.COPY, 0, -1, "(Ljava/util/Map;)V", "putAll(Ljava/util/Map;)V");
+ put(MAP, Op.STORE_ELEMENT, 1, 0,
+ "put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;",
+ "putIfAbsent(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+ put(MAP, Op.ELEMENT, -1, 0,
+ "get(Ljava/lang/Object;)Ljava/lang/Object;", "remove(Ljava/lang/Object;)Ljava/lang/Object;",
+ "getOrDefault(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+ put(MAP, Op.PLAIN, -1, 0, "containsKey(Ljava/lang/Object;)Z");
+ put(MAP, Op.VIEW, -1, -1,
+ "keySet()Ljava/util/Set;", "values()Ljava/util/Collection;", "entrySet()Ljava/util/Set;",
+ "keys()Ljava/util/Enumeration;", "elements()Ljava/util/Enumeration;");
+ put(MAP, Op.PLAIN, -1, -1, "size()I", "isEmpty()Z", "clear()V");
+
+ put(VIEW, Op.PLAIN, -1, -1,
+ "hasNext()Z", "hasMoreElements()Z", "hasPrevious()Z", "nextIndex()I", "previousIndex()I",
+ "size()I", "isEmpty()Z", "remove()V");
+ put(VIEW, Op.ELEMENT, -1, -1,
+ "next()Ljava/lang/Object;", "nextElement()Ljava/lang/Object;", "previous()Ljava/lang/Object;",
+ "getKey()Ljava/lang/Object;", "getValue()Ljava/lang/Object;", "get(I)Ljava/lang/Object;");
+ put(VIEW, Op.VIEW, -1, -1, "iterator()Ljava/util/Iterator;", "listIterator()Ljava/util/ListIterator;");
+ put(VIEW, Op.ARRAY, -1, -1, "toArray()[Ljava/lang/Object;");
+ }
+
+ private CollectionInvocation() {}
+
+ /**
+ * Applies the model of a call that was not instrumented.
+ *
+ * @return the return value of the call, or {@code null} if the call is not modelled
+ */
+ public static Value, ?> invoke(
+ String owner,
+ String name,
+ String desc,
+ Value, ?>[] args,
+ boolean isInstance,
+ ObjectValue, ?> instance,
+ long invokeId) throws NotImplementedException {
+ if (isInstance) {
+ if (instance == null || instance.isNullValue()) {
+ return null;
+ }
+ ContainerContents contents = instance.getContents();
+ Kind kind = contents != null ? contents.kind : kindOf(instance);
+ if (kind == null) {
+ return null;
+ }
+ Model model = (switch (kind) {
+ case LIST -> LIST;
+ case MAP -> MAP;
+ case VIEW -> VIEW;
+ }).get(name + desc);
+ if (model == null) {
+ return null;
+ }
+ return apply(model, kind, owner, name, desc, args, instance, invokeId);
+ }
+ if (owner.equals("java/util/Collections") || owner.equals("java/util/Arrays")) {
+ return invokeStatic(owner, name, desc, args, invokeId);
+ }
+ return null;
+ }
+
+ private static Kind kindOf(ObjectValue, ?> instance) {
+ if (instance.className == null || instance instanceof AbstractArrayValue, ?, ?, ?, ?>) {
+ return null;
+ }
+ return KIND_BY_CLASS.get(instance.className.replace('.', '/'));
+ }
+
+ private static Value, ?> apply(
+ Model model,
+ Kind kind,
+ String owner,
+ String name,
+ String desc,
+ Value, ?>[] args,
+ ObjectValue, ?> instance,
+ long invokeId) throws NotImplementedException {
+ // Every argument the model does not store must be concrete: indices, sizes, keys
+ for (int i = 0; i < args.length; i++) {
+ if (i != model.arg() && args[i].isSymbolic()) {
+ return null;
+ }
+ }
+ ContainerContents contents = instance.getContents();
+ if (model.initFirst() || contents == null) {
+ contents = new ContainerContents(kind);
+ }
+ if (kind == Kind.MAP && contents.keysSymbolic(new HashSet<>())) {
+ return null;
+ }
+ Value, ?> stored = model.arg() >= 0 ? args[model.arg()] : null;
+ if (model.op() == Op.COPY) {
+ ContainerContents source = sourceOf(stored, kind == Kind.MAP);
+ if (source == null) {
+ return null;
+ }
+ contents.addSource(source);
+ }
+ if (model.op() == Op.STORE || model.op() == Op.STORE_ELEMENT) {
+ if (model.key() >= 0) {
+ contents.addKey(args[model.key()]);
+ }
+ contents.addValue(stored);
+ }
+ instance.setContents(contents);
+ InvocationEntry call = new InvocationEntry(owner, name, desc, true, invokeId, true);
+ logger.debug("Modelled container call {}.{}{} on {}", owner, name, desc, instance);
+ return switch (model.op()) {
+ case INIT, STORE, COPY, PLAIN -> PlaceHolder.instance;
+ case ELEMENT, STORE_ELEMENT -> {
+ if (contents.kind == Kind.VIEW && contents.yieldsEntries && !name.startsWith("get")) {
+ yield new ContainerPlaceHolder(Result.VIEW, contents, false, List.of(), call);
+ }
+ List> candidates =
+ name.equals("getOrDefault") || name.equals("push") ? List.of(args[args.length - 1]) : List.of();
+ yield new ContainerPlaceHolder(Result.ELEMENT, contents, false, candidates, call);
+ }
+ case VIEW -> new ContainerPlaceHolder(
+ Result.VIEW, contents, name.equals("entrySet") || contents.yieldsEntries, List.of(), call);
+ case ARRAY -> new ContainerPlaceHolder(Result.ARRAY, contents, false, List.of(), call);
+ };
+ }
+
+ private static Value, ?> invokeStatic(String owner, String name, String desc, Value, ?>[] args, long invokeId)
+ throws NotImplementedException {
+ InvocationEntry call = new InvocationEntry(owner, name, desc, false, invokeId, true);
+ if (owner.equals("java/util/Arrays")) {
+ if (name.equals("asList") && desc.equals("([Ljava/lang/Object;)Ljava/util/List;")
+ && args[0] instanceof ObjectArrayValue array) {
+ ContainerContents view = new ContainerContents(Kind.VIEW);
+ view.addSource(array);
+ return new ContainerPlaceHolder(Result.VIEW, view, false, List.of(), call);
+ }
+ return null;
+ }
+ switch (name + desc) {
+ case "singleton(Ljava/lang/Object;)Ljava/util/Set;",
+ "singletonList(Ljava/lang/Object;)Ljava/util/List;" -> {
+ ContainerContents view = new ContainerContents(Kind.VIEW);
+ view.addValue(args[0]);
+ return new ContainerPlaceHolder(Result.VIEW, view, false, List.of(), call);
+ }
+ case "enumeration(Ljava/util/Collection;)Ljava/util/Enumeration;",
+ "unmodifiableCollection(Ljava/util/Collection;)Ljava/util/Collection;",
+ "unmodifiableList(Ljava/util/List;)Ljava/util/List;",
+ "unmodifiableSet(Ljava/util/Set;)Ljava/util/Set;",
+ "unmodifiableMap(Ljava/util/Map;)Ljava/util/Map;" -> {
+ ContainerContents source = sourceOf(args[0], false);
+ if (source == null) {
+ return null;
+ }
+ return new ContainerPlaceHolder(Result.VIEW, source, source.yieldsEntries, List.of(), call);
+ }
+ default -> {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * The contents of a container argument that is copied or viewed: its recorded contents, or
+ * empty contents for a container that holds nothing symbolic. {@code null} (not modelled) for
+ * an untracked container that is symbolic, or, when the copy goes into a map, for a source that
+ * is not a map or whose keys may be symbolic.
+ */
+ private static ContainerContents sourceOf(Value, ?> arg, boolean intoMap) throws NotImplementedException {
+ if (!(arg instanceof ObjectValue, ?> ov) || ov.isNullValue()) {
+ return null;
+ }
+ ContainerContents source = ov.getContents();
+ if (source == null) {
+ return ov.isSymbolic() ? null : new ContainerContents(Kind.VIEW);
+ }
+ if (intoMap && (source.kind != Kind.MAP || source.keysSymbolic(new HashSet<>()))) {
+ return null;
+ }
+ return source;
+ }
+
+ /**
+ * Picks the shadow of the object a modelled call returned, once the concrete object is known.
+ *
+ *
+ *
An element is the shadow stored under the object's address. If there is none, the
+ * shadow is recovered as for any unmodelled call; if the container may hold a symbolic
+ * value, that recovery may have lost it, which is recorded as context loss.
+ *
A view or iterator gets contents backed by the container's.
+ *
An array returned by {@code toArray()} gets the stored shadows as its elements.
+ *
+ */
+ public static Value, ?> resolve(
+ ContainerPlaceHolder ph,
+ Object concrete,
+ int address,
+ ShadowContext stack,
+ SymbolicTraceHandler symbolicTraceHandler) throws Exception {
+ if (address == ObjectValue.ADDRESS_NULL || concrete == null) {
+ return ValueFactory.createNULLValue();
+ }
+ switch (ph.result) {
+ case ELEMENT -> {
+ Value, ?> match = ph.contents.lookup(address);
+ if (match == null) {
+ for (Value, ?> c : ph.candidates) {
+ if (c instanceof ObjectValue, ?> ov && ov.getAddress() == address) match = c;
+ }
+ }
+ if (match != null && matches(match, concrete)) {
+ return match;
+ }
+ Value, ?> recovered = recover(concrete, address, stack);
+ if (ph.contents.isSymbolic(new HashSet<>())) {
+ recordContextLoss(ph.call, symbolicTraceHandler);
+ }
+ return recovered;
+ }
+ case VIEW -> {
+ Value, ?> recovered = recover(concrete, address, stack);
+ if (recovered instanceof ObjectValue, ?> ov
+ && !(ov instanceof StringValue)
+ && !(ov instanceof BoxedValue>)
+ && !(ov instanceof AbstractArrayValue, ?, ?, ?, ?>)
+ && !(ov instanceof ObjectArrayValue)) {
+ if (ov.getContents() == null) {
+ ov.setContents(ContainerContents.viewOf(ph.contents, ph.yieldsEntries));
+ } else {
+ ov.getContents().addSource(ph.contents);
+ }
+ } else if (ph.contents.isSymbolic(new HashSet<>())) {
+ recordContextLoss(ph.call, symbolicTraceHandler);
+ }
+ return recovered;
+ }
+ case ARRAY -> {
+ if (!(concrete instanceof Object[] elements)) {
+ return recover(concrete, address, stack);
+ }
+ long threadId = Thread.currentThread().getId();
+ IntValue size = new IntValue(ThreadHandler.getSolverContext(threadId), elements.length);
+ ObjectArrayValue array = ValueFactory.createObjectArrayValue("java/lang/Object", size);
+ array.setAddress(address);
+ boolean unmatched = false;
+ for (int i = 0; i < elements.length; i++) {
+ if (elements[i] == null) {
+ array.storeElement(new IntValue(size.context, i), ValueFactory.createNULLValue());
+ continue;
+ }
+ Value, ?> match = ph.contents.lookup(System.identityHashCode(elements[i]));
+ if (match != null && matches(match, elements[i])) {
+ array.storeElement(new IntValue(size.context, i), match);
+ } else {
+ // left unset: read back like any element of an untracked array
+ unmatched = true;
+ }
+ }
+ if (unmatched && ph.contents.isSymbolic(new HashSet<>())) {
+ recordContextLoss(ph.call, symbolicTraceHandler);
+ }
+ stack.putToHeap(address, array);
+ return array;
+ }
+ }
+ return recover(concrete, address, stack);
+ }
+
+ /** Whether a stored shadow can stand for the concrete object (guards against address reuse). */
+ private static boolean matches(Value, ?> shadow, Object concrete) {
+ if (shadow instanceof StringValue s) {
+ return concrete instanceof String && concrete.equals(s.concrete);
+ }
+ if (shadow instanceof ObjectValue, ?> ov && ov.className != null && !(ov instanceof BoxedValue>)
+ && !(ov instanceof AbstractArrayValue, ?, ?, ?, ?>) && !(ov instanceof ObjectArrayValue)) {
+ return ov.className.replace('.', '/').equals(concrete.getClass().getName().replace('.', '/'));
+ }
+ return true;
+ }
+
+ /** What {@code GETVALUE_Object} does for the result of an unmodelled call. */
+ private static Value, ?> recover(Object concrete, int address, ShadowContext stack) throws Exception {
+ Value, ?> heap = stack.getFromHeap(address);
+ if (heap != null) {
+ return heap;
+ }
+ Value, ?> created = ValueFactory.createObjectValue(concrete, address);
+ stack.putToHeap(address, created);
+ return created;
+ }
+
+ private static void recordContextLoss(InvocationEntry call, SymbolicTraceHandler symbolicTraceHandler)
+ throws NoThreadContextException {
+ logger.warn("Result of {}.{}{} could not be matched to a stored shadow, causes context loss",
+ call.owner(), call.name(), call.desc());
+ symbolicTraceHandler.recordSymbolicContextLoss();
+ long threadId = Thread.currentThread().getId();
+ ThreadHandler.recordMissingInvocation(threadId, call);
+ ThreadHandler.recordContextLossInvocation(threadId, call);
+ }
+}
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/value/ContainerPlaceHolder.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/value/ContainerPlaceHolder.java
new file mode 100644
index 00000000..258d66f9
--- /dev/null
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/value/ContainerPlaceHolder.java
@@ -0,0 +1,50 @@
+package de.uzl.its.swat.symbolic.value;
+
+import de.uzl.its.swat.common.logging.records.InvocationEntry;
+import de.uzl.its.swat.symbolic.value.reference.ContainerContents;
+import java.util.List;
+
+/**
+ * The return value of a modelled call on a JDK container that returns a reference. The concrete
+ * object is only known when the value is read back ({@code GETVALUE_Object}); the shadow is chosen
+ * there, see {@code CollectionInvocation.resolve}.
+ */
+public class ContainerPlaceHolder extends PlaceHolder {
+
+ public enum Result {
+ /** An element (or map key/value) of {@link #contents}. */
+ ELEMENT,
+ /** A view, iterator, enumeration or entry backed by {@link #contents}. */
+ VIEW,
+ /** A new object array holding elements of {@link #contents} ({@code toArray()}). */
+ ARRAY
+ }
+
+ public final Result result;
+ public final ContainerContents contents;
+ /** Whether a returned view yields map entries. */
+ public final boolean yieldsEntries;
+ /** Arguments the call may return as they are (e.g. the default of {@code getOrDefault}). */
+ public final List> candidates;
+ /** The call, recorded as the culprit if the result cannot be matched to a shadow. */
+ public final InvocationEntry call;
+
+ public ContainerPlaceHolder(
+ Result result,
+ ContainerContents contents,
+ boolean yieldsEntries,
+ List> candidates,
+ InvocationEntry call) {
+ super(false);
+ this.result = result;
+ this.contents = contents;
+ this.yieldsEntries = yieldsEntries;
+ this.candidates = candidates;
+ this.call = call;
+ }
+
+ @Override
+ public String toString() {
+ return "ContainerPlaceHolder (" + result + ")";
+ }
+}
diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/value/reference/ContainerContents.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/value/reference/ContainerContents.java
new file mode 100644
index 00000000..0a042e90
--- /dev/null
+++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/value/reference/ContainerContents.java
@@ -0,0 +1,223 @@
+package de.uzl.its.swat.symbolic.value.reference;
+
+import de.uzl.its.swat.common.exceptions.NotImplementedException;
+import de.uzl.its.swat.symbolic.value.PlaceHolder;
+import de.uzl.its.swat.symbolic.value.Value;
+import de.uzl.its.swat.symbolic.value.reference.array.ObjectArrayValue;
+import de.uzl.its.swat.symbolic.value.reference.lang.BoxedValue;
+import de.uzl.its.swat.symbolic.value.reference.lang.StringValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The shadow values a JDK container (a {@code java.util} collection or map, or a view, iterator or
+ * entry derived from one) has been given through modelled calls. A container is not instrumented,
+ * so its fields stay empty in the shadow; the shadow of what it holds is kept here instead.
+ *
+ *
The record only grows: removals are not tracked, so it over-approximates the contents. That
+ * is enough for its two uses:
+ *
+ *
+ *
{@link #isSymbolic} makes a container that may hold a symbolic value count as symbolic,
+ * so an unmodelled call that gets it still records context loss;
+ *
{@link #lookup} gives back the shadow of an element a modelled read returned, found by the
+ * address of the concrete object.
+ *
+ *
+ *
Contents can have sources: the contents of another container (a copy, a view) or an object
+ * array (the array behind {@code Arrays.asList}). Their values count as values of this container.
+ */
+public class ContainerContents {
+
+ /** What the container is, which decides the calls that are modelled on it. */
+ public enum Kind {
+ /** A list, queue or deque: elements are only stored and returned, never compared. */
+ LIST,
+ /** A hash map: the keys are hashed and compared, the values are only stored and returned. */
+ MAP,
+ /** A view, iterator, enumeration or map entry, or an immutable container: only read. */
+ VIEW
+ }
+
+ /** Marks an address under which different shadows were stored. */
+ private static final Value, ?> AMBIGUOUS = new PlaceHolder(false);
+
+ public final Kind kind;
+ /** Whether the elements of this view are map entries (an entry set and its iterators). */
+ public final boolean yieldsEntries;
+
+ /** Mutable shadows (objects, arrays, containers), whose symbolic state can change later. */
+ private final List> mutables = new ArrayList<>();
+ /** Whether a value-typed (String, boxed) element was symbolic when it was stored. */
+ private boolean symbolicImmutable = false;
+ /** Mutable map keys; a modelled store only accepts keys that are not symbolic. */
+ private final List> keys = new ArrayList<>();
+
+ private final Map> byAddress = new HashMap<>();
+ private final List> addressUnknown = new ArrayList<>();
+ private final List