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