1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.net.module.util;
17 
18 import android.os.Build;
19 import android.system.ErrnoException;
20 import android.util.Pair;
21 
22 import androidx.annotation.GuardedBy;
23 import androidx.annotation.NonNull;
24 import androidx.annotation.RequiresApi;
25 
26 import java.util.HashMap;
27 import java.util.NoSuchElementException;
28 
29 /**
30  * Subclass of BpfMap for maps that are only ever written by one userspace writer.
31  *
32  * This class stores all map data in a userspace HashMap in addition to in the BPF map. This makes
33  * reads (but not iterations) much faster because they do not require a system call or converting
34  * the raw map read to the Value struct. See, e.g., b/343166906 .
35  *
36  * Users of this class must ensure that no BPF program ever writes to the map, and that all
37  * userspace writes to the map occur through this object. Other userspace code may still read from
38  * the map; only writes are required to go through this object.
39  *
40  * Reads and writes to this object are thread-safe and internally synchronized. The read and write
41  * methods are synchronized to ensure that current writers always result in a consistent internal
42  * state (without synchronization, two concurrent writes might update the underlying map and the
43  * cache in the opposite order, resulting in the cache being out of sync with the map).
44  *
45  * getNextKey and iteration over the map are not synchronized or cached and always access the
46  * isunderlying map. The values returned by these calls may be temporarily out of sync with the
47  * values read and written through this object.
48  *
49  * TODO: consider caching reads on iterations as well. This is not trivial because the semantics for
50  * iterating BPF maps require passing in the previously-returned key, and Java iterators only
51  * support iterating from the beginning. It could be done by implementing forEach and possibly by
52  * making getFirstKey and getNextKey private (if no callers are using them). Because HashMap is not
53  * thread-safe, implementing forEach would require either making that method synchronized (and
54  * block reads and updates from other threads until iteration is complete) or switching the
55  * internal HashMap to ConcurrentHashMap.
56  *
57  * @param <K> the key of the map.
58  * @param <V> the value of the map.
59  */
60 @RequiresApi(Build.VERSION_CODES.S)
61 public class SingleWriterBpfMap<K extends Struct, V extends Struct> extends BpfMap<K, V> {
62     // HashMap instead of ArrayMap because it performs better on larger maps, and many maps used in
63     // our code can contain hundreds of items.
64     @GuardedBy("this")
65     private final HashMap<K, V> mCache = new HashMap<>();
66 
67     // This should only ever be called (hence private) once for a given 'path'.
68     // Java-wise what matters is the entire {path, key, value} triplet,
69     // but of course the kernel exclusive lock is just on the path (fd),
70     // and any BpfMap has (or should have...) well defined key/value types
71     // (or at least their sizes) so in practice it doesn't really matter.
SingleWriterBpfMap(@onNull final String path, final Class<K> key, final Class<V> value)72     private SingleWriterBpfMap(@NonNull final String path, final Class<K> key,
73             final Class<V> value) throws ErrnoException, NullPointerException {
74         super(path, BPF_F_RDWR_EXCLUSIVE, key, value);
75 
76         // Populate cache with the current map contents.
77         synchronized (this) {
78             K currentKey = super.getFirstKey();
79             while (currentKey != null) {
80                 mCache.put(currentKey, super.getValue(currentKey));
81                 currentKey = super.getNextKey(currentKey);
82             }
83         }
84     }
85 
86     // This allows reuse of SingleWriterBpfMap objects for the same {path, keyClass, valueClass}.
87     // These are never destroyed, so once created the lock is (effectively) held till process death
88     // (even if fixed, there would still be a write-only fd cache in underlying BpfMap base class).
89     private static final HashMap<Pair<String, Pair<Class, Class>>, SingleWriterBpfMap>
90             singletonCache = new HashMap<>();
91 
92     // This is the public 'factory method' that (creates if needed and) returns a singleton instance
93     // for a given map.  This holds an exclusive lock and has a permanent write-through cache.
94     // It will not be released until process death (or at least unload of the relevant class loader)
95     public synchronized static <KK extends Struct, VV extends Struct> SingleWriterBpfMap<KK,VV>
getSingleton(@onNull final String path, final Class<KK> key, final Class<VV> value)96             getSingleton(@NonNull final String path, final Class<KK> key, final Class<VV> value)
97             throws ErrnoException, NullPointerException {
98         var cacheKey = new Pair<>(path, new Pair<Class,Class>(key, value));
99         if (!singletonCache.containsKey(cacheKey))
100             singletonCache.put(cacheKey, new SingleWriterBpfMap(path, key, value));
101         return singletonCache.get(cacheKey);
102     }
103 
104     @Override
updateEntry(K key, V value)105     public synchronized void updateEntry(K key, V value) throws ErrnoException {
106         super.updateEntry(key, value);
107         mCache.put(key, value);
108     }
109 
110     @Override
insertEntry(K key, V value)111     public synchronized void insertEntry(K key, V value)
112             throws ErrnoException, IllegalStateException {
113         super.insertEntry(key, value);
114         mCache.put(key, value);
115     }
116 
117     @Override
replaceEntry(K key, V value)118     public synchronized void replaceEntry(K key, V value)
119             throws ErrnoException, NoSuchElementException {
120         super.replaceEntry(key, value);
121         mCache.put(key, value);
122     }
123 
124     @Override
insertOrReplaceEntry(K key, V value)125     public synchronized boolean insertOrReplaceEntry(K key, V value) throws ErrnoException {
126         final boolean ret = super.insertOrReplaceEntry(key, value);
127         mCache.put(key, value);
128         return ret;
129     }
130 
131     @Override
deleteEntry(K key)132     public synchronized boolean deleteEntry(K key) throws ErrnoException {
133         final boolean ret = super.deleteEntry(key);
134         mCache.remove(key);
135         return ret;
136     }
137 
138     @Override
containsKey(@onNull K key)139     public synchronized boolean containsKey(@NonNull K key) throws ErrnoException {
140         return mCache.containsKey(key);
141     }
142 
143     @Override
getValue(@onNull K key)144     public synchronized V getValue(@NonNull K key) throws ErrnoException {
145         return mCache.get(key);
146     }
147 }
148