Skip to content

Commit c34eeaa

Browse files
jeremiahjstaceykwwall
authored andcommitted
AbstractAccessReferenceMap Issues #37 #329 (#469)
* Concurrency Test for AbstractAccessReferenceMap SThis test case consistently illustrates the race condition resulting from concurrent invocations of addDirectReference. * AbstractAccessReferenceMap Sync Updates Synchronizing all public access methods to this wrapper class. * AbstractAccessReferenceMap Class Documentation Updating class documentation to provide an example of multi-interaction synchronization syntax. * AbstractAccessMap field updates to use HashMap With primary synchronization being pushed to the class API level, there really is no benefit to maintaining the overhead of ConcurrentHashMap fields. The thread-safety guaranteed by the ConcurrentHashMap is superceeded by the public API of the abstract class, and should never actually be leveraged. This assumes that all sub-classes with extended public API continue the use of class-level synchronization. * AbstractAccessReferenceMapTest Extension Adding method which shows that it is possible to create a corrupted direct -> Indirect to Indirect -> Direct Object relationship when using the public update(Set) API. * AbstractAccessReferenceMap.update Logic Update Altering the logic of update behavior to avoid the possibility of the same indirect object being associated with multiple direct instances.
1 parent 5e0fcf7 commit c34eeaa

File tree

2 files changed

+171
-35
lines changed

2 files changed

+171
-35
lines changed

src/main/java/org/owasp/esapi/reference/AbstractAccessReferenceMap.java

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,37 @@
1515
*/
1616
package org.owasp.esapi.reference;
1717

18-
import org.owasp.esapi.AccessReferenceMap;
19-
import org.owasp.esapi.errors.AccessControlException;
20-
18+
import java.util.HashMap;
19+
import java.util.HashSet;
2120
import java.util.Iterator;
2221
import java.util.Map;
2322
import java.util.Set;
2423
import java.util.TreeSet;
25-
import java.util.concurrent.ConcurrentHashMap;
24+
25+
import org.owasp.esapi.AccessReferenceMap;
26+
import org.owasp.esapi.errors.AccessControlException;
2627

2728
/**
28-
* Abstract Implementation of the AccessReferenceMap that is backed by ConcurrentHashMaps to
29-
* provide a thread-safe implementation of the AccessReferenceMap. Implementations of this
30-
* abstract class should implement the #getUniqueReference() method.
31-
*
29+
* Abstract Implementation of the AccessReferenceMap.
30+
* <br>
31+
* Implementation offers default synchronization on all public API
32+
* to assist with thread safety.
33+
* <br>
34+
* For complex interactions spanning multiple calls, it is recommended
35+
* to add a synchronized block around all invocations to maintain intended data integrity.
36+
*
37+
* <pre>
38+
* public MyClassUsingAARM {
39+
* private AbstractAccessReferenceMap<Object> aarm;
40+
*
41+
* public void replaceAARMDirect(Object oldDirect, Object newDirect) {
42+
* synchronized (aarm) {
43+
* aarm.removeDirectReference(oldDirect);
44+
* aarm.addDirectReference(newDirect);
45+
* }
46+
* }
47+
* }
48+
* </pre>
3249
* @author Chris Schmidt ([email protected])
3350
* @since July 21, 2009
3451
*/
@@ -43,15 +60,15 @@ public abstract class AbstractAccessReferenceMap<K> implements AccessReferenceMa
4360

4461
/**
4562
* Instantiates a new access reference map. Note that this will create the underlying Maps with an initialSize
46-
* of {@link ConcurrentHashMap#DEFAULT_INITIAL_CAPACITY} and that resizing a Map is an expensive process. Consider
63+
* of {@link HashMap#DEFAULT_INITIAL_CAPACITY} and that resizing a Map is an expensive process. Consider
4764
* using a constructor where the initialSize is passed in to maximize performance of the AccessReferenceMap.
4865
*
4966
* @see #AbstractAccessReferenceMap(java.util.Set, int)
5067
* @see #AbstractAccessReferenceMap(int)
5168
*/
5269
public AbstractAccessReferenceMap() {
53-
itod = new ConcurrentHashMap<K, Object>();
54-
dtoi = new ConcurrentHashMap<Object,K>();
70+
itod = new HashMap<K, Object>();
71+
dtoi = new HashMap<Object,K>();
5572
}
5673

5774
/**
@@ -62,8 +79,8 @@ public AbstractAccessReferenceMap() {
6279
* The initial size of the underlying maps
6380
*/
6481
public AbstractAccessReferenceMap( int initialSize ) {
65-
itod = new ConcurrentHashMap<K, Object>(initialSize);
66-
dtoi = new ConcurrentHashMap<Object,K>(initialSize);
82+
itod = new HashMap<K, Object>(initialSize);
83+
dtoi = new HashMap<Object,K>(initialSize);
6784
}
6885

6986
/**
@@ -82,8 +99,8 @@ public AbstractAccessReferenceMap( int initialSize ) {
8299
*/
83100
@Deprecated
84101
public AbstractAccessReferenceMap( Set<Object> directReferences ) {
85-
itod = new ConcurrentHashMap<K, Object>(directReferences.size());
86-
dtoi = new ConcurrentHashMap<Object,K>(directReferences.size());
102+
itod = new HashMap<K, Object>(directReferences.size());
103+
dtoi = new HashMap<Object,K>(directReferences.size());
87104
update(directReferences);
88105
}
89106

@@ -111,8 +128,8 @@ public AbstractAccessReferenceMap( Set<Object> directReferences ) {
111128
*/
112129
@Deprecated
113130
public AbstractAccessReferenceMap( Set<Object> directReferences, int initialSize ) {
114-
itod = new ConcurrentHashMap<K, Object>(initialSize);
115-
dtoi = new ConcurrentHashMap<Object,K>(initialSize);
131+
itod = new HashMap<K, Object>(initialSize);
132+
dtoi = new HashMap<Object,K>(initialSize);
116133
update(directReferences);
117134
}
118135

@@ -135,7 +152,7 @@ public synchronized Iterator iterator() {
135152
/**
136153
* {@inheritDoc}
137154
*/
138-
public <T> K addDirectReference(T direct) {
155+
public synchronized <T> K addDirectReference(T direct) {
139156
if ( dtoi.keySet().contains( direct ) ) {
140157
return dtoi.get( direct );
141158
}
@@ -148,7 +165,7 @@ public <T> K addDirectReference(T direct) {
148165
/**
149166
* {@inheritDoc}
150167
*/
151-
public <T> K removeDirectReference(T direct) throws AccessControlException
168+
public synchronized <T> K removeDirectReference(T direct) throws AccessControlException
152169
{
153170
K indirect = dtoi.get(direct);
154171
if ( indirect != null ) {
@@ -162,33 +179,52 @@ public <T> K removeDirectReference(T direct) throws AccessControlException
162179
* {@inheritDoc}
163180
*/
164181
public final synchronized void update(Set directReferences) {
165-
Map<Object,K> new_dtoi = new ConcurrentHashMap<Object,K>( directReferences.size() );
166-
Map<K,Object> new_itod = new ConcurrentHashMap<K,Object>( directReferences.size() );
167-
168-
for ( Object o : directReferences ) {
169-
K indirect = dtoi.get( o );
170-
171-
if ( indirect == null ) {
172-
indirect = getUniqueReference();
173-
}
174-
new_dtoi.put( o, indirect );
175-
new_itod.put( indirect, o );
176-
}
177-
dtoi = new_dtoi;
178-
itod = new_itod;
182+
Map<Object,K> new_dtoi = new HashMap<Object,K>( directReferences.size() );
183+
Map<K,Object> new_itod = new HashMap<K,Object>( directReferences.size() );
184+
185+
Set<Object> newDirect = new HashSet<>(directReferences);
186+
Set<Object> dtoiCurrent = new HashSet<>(dtoi.keySet());
187+
188+
//Preserve all keys that are in the new set
189+
dtoiCurrent.retainAll(newDirect);
190+
191+
//Transfer existing values into the new map
192+
for (Object current: dtoiCurrent) {
193+
K idCurrent = dtoi.get(current);
194+
new_dtoi.put(current, idCurrent);
195+
new_itod.put(idCurrent, current);
196+
}
197+
198+
//Trim the new map to only new values
199+
newDirect.removeAll(dtoiCurrent);
200+
201+
//Add new values with new indirect keys to the new map
202+
for (Object newD : newDirect) {
203+
K idCurrent;
204+
do {
205+
idCurrent = getUniqueReference();
206+
//Unlikey, but just in case we generate the exact same key multiple times...
207+
} while (dtoi.containsValue(idCurrent));
208+
209+
new_dtoi.put(newD, idCurrent);
210+
new_itod.put(idCurrent, newD);
211+
}
212+
213+
dtoi = new_dtoi;
214+
itod = new_itod;
179215
}
180216

181217
/**
182218
* {@inheritDoc}
183219
*/
184-
public <T> K getIndirectReference(T directReference) {
220+
public synchronized <T> K getIndirectReference(T directReference) {
185221
return dtoi.get(directReference);
186222
}
187223

188224
/**
189225
* {@inheritDoc}
190226
*/
191-
public <T> T getDirectReference(K indirectReference) throws AccessControlException {
227+
public synchronized <T> T getDirectReference(K indirectReference) throws AccessControlException {
192228
if (itod.containsKey(indirectReference) ) {
193229
try
194230
{
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.owasp.esapi.reference;
2+
3+
import java.util.HashMap;
4+
import java.util.HashSet;
5+
import java.util.Set;
6+
7+
import org.junit.Assert;
8+
import org.junit.Test;
9+
import org.mockito.Mockito;
10+
import org.powermock.reflect.Whitebox;
11+
12+
13+
public class AbstractAccessReferenceMapTest {
14+
15+
@Test
16+
public void testConcurrentAddDirectReference() throws Exception {
17+
@SuppressWarnings("unchecked")
18+
final AbstractAccessReferenceMap<Object> map = Mockito.mock(AbstractAccessReferenceMap.class, Mockito.withSettings().useConstructor()
19+
.defaultAnswer(Mockito.CALLS_REAL_METHODS)
20+
);
21+
Object indirectObj = new Object();
22+
Mockito.when(map.getUniqueReference()).thenReturn(indirectObj);
23+
24+
final HashMap<?,?> itod= Whitebox.getInternalState(map, "itod");
25+
26+
final Object toAdd = new Object();
27+
28+
Runnable addReference1 = new Runnable() {
29+
@Override
30+
public void run() {
31+
map.addDirectReference(toAdd);
32+
}
33+
};
34+
Runnable addReference2 = new Runnable() {
35+
@Override
36+
public void run() {
37+
map.addDirectReference(toAdd);
38+
}
39+
};
40+
41+
Runnable lockItod = new Runnable() {
42+
public void run() {
43+
synchronized (itod) {
44+
try {
45+
Thread.sleep(2000);
46+
} catch (InterruptedException e) {
47+
// TODO Auto-generated catch block
48+
e.printStackTrace();
49+
}
50+
51+
}
52+
}
53+
};
54+
55+
Thread lockIndirectRefs = new Thread(lockItod, "Lock Indirect Refs");
56+
Thread addRef1Thread = new Thread(addReference1, "Add Ref 1");
57+
Thread addRef2Thread = new Thread(addReference2, "Add Ref 2");
58+
lockIndirectRefs.start();
59+
addRef1Thread.start();
60+
addRef2Thread.start();
61+
62+
addRef1Thread.join();
63+
addRef2Thread.join();
64+
lockIndirectRefs.join();
65+
66+
Mockito.verify(map,Mockito.times(1)).getUniqueReference();
67+
}
68+
69+
@Test
70+
public void verifyNoDuplicateKeysOnUpdateReplace() {
71+
@SuppressWarnings("unchecked")
72+
final AbstractAccessReferenceMap<Object> map = Mockito.mock(AbstractAccessReferenceMap.class, Mockito.withSettings().useConstructor()
73+
.defaultAnswer(Mockito.CALLS_REAL_METHODS)
74+
);
75+
Object indirectObj1 = new Object();
76+
Object indirectObj2 = new Object();
77+
Mockito.when(map.getUniqueReference()).thenReturn(indirectObj1);
78+
79+
Object direct1 = new Object();
80+
Object direct2 = new Object();
81+
82+
map.addDirectReference(direct1);
83+
84+
Mockito.reset(map);
85+
86+
Set<Object> newDirectElements = new HashSet<>();
87+
newDirectElements.add(direct2);
88+
newDirectElements.add(direct1);
89+
90+
Mockito.when(map.getUniqueReference()).thenReturn(indirectObj1).thenReturn(indirectObj2);
91+
92+
map.update(newDirectElements);
93+
94+
//Needs to be called 2 times to get past the first duplicate key. This verifies that we're inserting unique pairs.
95+
Mockito.verify(map, Mockito.times(2)).getUniqueReference();
96+
97+
Assert.assertEquals(indirectObj1, map.getIndirectReference(direct1));
98+
Assert.assertEquals(indirectObj2, map.getIndirectReference(direct2));
99+
}
100+
}

0 commit comments

Comments
 (0)