[PATCH 2/2] SELinux: Introduce hash3() as alternative to shift-xor

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



From: Siarhei Liakh <siarhei.liakh@xxxxxxxxxxxxxxxxx>

This change improves performance of hashing functions used in AVC and
AVTab to hash 3x 32-bit values. As compared to original shift-xor function
used in AVC, substantial improvement in quality of hashing is gained by:
1. replacing shifts with rolls, thus preserving all of the incoming
   entropy
2. adjusting shift/roll constants to reduce overlap between the input
   values
3. use of arithmetic addition instead of XOR, as a way to spread out
   collisions
4. use of hash_32() to level out the distribution of hash output values
   throughout the range

Within the scope of particular application, these changes bring hash
quality to that of much more complex MurmurHash3 (which is currently used
in AVTab), while maintaining trivial simplicity of the original code.

The only price paid for the improvements is introduction of a single
32-bit multiplication instruction upon which hash_32() is built, which is
not a problem on almost any modern platform where fast multiplication is
available. For example, 32-bit MULs have had latency of 4 on pretty much
all Intel x86 CPUs for at least a decade or so (while ADD/XOR/SHL/ROL have
latency of 1). Platforms without fast multiplication provide their own
alternative mechanism for hash_32().

New hash function hash3() had been applied to two key parts of SELinux:
Access Vector Cache (AVC) and Access Vector Table (AVTab) with results
that follow.

AVC:

Below is the content of hash_stats, as observed on Fedora 32 in its
default configuration with targeted policy, averaged over 100 samples. The
stats show that new hash function not only cuts the longest chain almost
in half, but also provides a much better distribution throughout the table:
about 39% more buckets are being used, with at least half of all non-zero
buckets consistently staying at length 1, with 75% at 2 or below.

/sys/fs/selinux/avc/hash_stats:
            Old     New
Samples:   100      100
Entries:   504.43   503.67
Buckets:   219.73   304.61
1st Qu.:     1.00     1.00
Median :     1.39     1.00
3rd Qu.:     3.00     2.00
Maximum:     9.32     5.37

Next, performance of avc_lookup() is analyzed by timing it with ftrace on
a system with same configuration as above:

acv_lookup(), latency in us:
            Old     New
 Samples: 261534    244168
 Min.   : 0.1320    0.1310
 1st Qu.: 0.2410    0.2360
 Median : 0.4220    0.4240
 3rd Qu.: 0.5580    0.5600
 Max.   : 9.9300    9.9890

Considering small size of AVC in default configuration, the change does
not show any latency improvements. In fact, median and 75th percentile
timings appear to be in line with addition of extra 4 clock cycles for MUL
(roughly 2ns on a 2.2Ghz CPU), or 0.47%. This appears to be a small price
to pay for substantially better distribution of hash values, reducing a
probability and/or severity of potential pathological behavior on some
larger configurations.

Note that absolute max latency is likely not indicative, as it is
susceptible to one-off events such as interrupts, cache misses, etc.

AVTab:

Unlike AVC, AVTab hash table is much larger and much more densely
populated. In the past, need for better hashing in AVTab was demonstrated,
resulting in transition to a local copy of custom implementation of much
more complicated (and better) MurmurHash3, adapted for hashing of 3x u32s.
This change replaces MurmurHash3 with a much simpler, faster, yet just as
effective (at least, in this particular use case) hash3() function
described above.

As evidenced by debug output produced during the boot process, despite
being much simpler, hash3() yields quite similar (if not just a tiny bit
better) hash distribution quality within AVTab:

Old:
rules:  81014 entries and 29600/32768 buckets used, longest chain length
11 non-zero Q1/Med/Q3 2/2/4 sum of chain length^2 290030

New:
rules:  81014 entries and 29645/32768 buckets used, longest chain length
11 non-zero Q1/Med/Q3 2/2/4 sum of chain length^2 288810

Performance, though, is a different matter: a clear savings of 8ns to
10ns (75th and 50th percentiles respectively) had been measured with
ftrace for the most frequent AVTab lookup method:

avtab_search_node(), latency in us:
          Old       New
 Samples: 5953243   5458099
 Min.   : 0.136     0.129
 1st Qu.: 0.199     0.189
 Median : 0.219     0.209
 3rd Qu.: 0.294     0.286
 Max.   :10.000     9.990

The results are not as clear for much less frequently (that is 1500x call
frequency difference) used avtab_search(), though they appear to lean
towards a positive side:

avtab_search(), latency in us:
            Old     New
 Samples: 3863      3638
 Min.   : 0.165     0.157
 1st Qu.: 0.297     0.293
 Median : 0.510     0.517
 3rd Qu.: 0.803     0.774
 Max.   : 9.343     7.701

Signed-off-by: Siarhei Liakh <siarhei.liakh@xxxxxxxxxxxxxxxxx>
---
Please CC me directly on all replies.

 security/selinux/avc.c           |  8 +++--
 security/selinux/include/hash3.h | 44 ++++++++++++++++++++++++
 security/selinux/ss/avtab.c      | 57 ++++++++++----------------------
 security/selinux/ss/avtab.h      |  1 +
 4 files changed, 67 insertions(+), 43 deletions(-)
 create mode 100644 security/selinux/include/hash3.h

diff --git a/security/selinux/avc.c b/security/selinux/avc.c
index c3bbdb083371..ed092324bef1 100644
--- a/security/selinux/avc.c
+++ b/security/selinux/avc.c
@@ -30,12 +30,14 @@
 #include "avc.h"
 #include "avc_ss.h"
 #include "classmap.h"
+#include "hash3.h"
 
 #ifdef CONFIG_SECURITY_SELINUX_DEBUG_HASHES
 #include "median.h"
 #endif
 
-#define AVC_CACHE_SLOTS			512
+#define AVC_CACHE_BITS			(9)
+#define AVC_CACHE_SLOTS			(1 << AVC_CACHE_BITS)
 #define AVC_DEF_CACHE_THRESHOLD		512
 #define AVC_CACHE_RECLAIM		16
 
@@ -125,9 +127,9 @@ static struct kmem_cache *avc_xperms_data_cachep;
 static struct kmem_cache *avc_xperms_decision_cachep;
 static struct kmem_cache *avc_xperms_cachep;
 
-static inline int avc_hash(u32 ssid, u32 tsid, u16 tclass)
+static inline u32 avc_hash(u32 ssid, u32 tsid, u16 tclass)
 {
-	return (ssid ^ (tsid<<2) ^ (tclass<<4)) & (AVC_CACHE_SLOTS - 1);
+	return hash3(ssid, tsid, tclass, AVC_CACHE_BITS);
 }
 
 /**
diff --git a/security/selinux/include/hash3.h b/security/selinux/include/hash3.h
new file mode 100644
index 000000000000..21e2408f5227
--- /dev/null
+++ b/security/selinux/include/hash3.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* (C) Siarhei Liakh <Siarhei.Liakh@xxxxxxxxxxxxxxxxx>, 2020, GPL 2.0+ */
+
+#ifndef _SELINUX_HASH3_H
+#define _SELINUX_HASH3_H
+
+#include <linux/hash.h>
+#include <linux/bitops.h>
+
+/*
+ * hash3(): Mix and hash 3 x u32's with minimal overhead,
+ * truncate result to requested number of bits.
+ *
+ * This hash function produces very good results for inputs where most of
+ * input entropy is contained within the lower 11 bits of each of the words.
+ *
+ * For example, AVC hash table (in avc.c) is indexed by a 3-tuple (u32 ssid,
+ * u32 tsid, u16 tclass), where (on Fedora 32 Beta, as of March 2020) ssid and
+ * tsid appear to be sequential indexes between 0x0 and 0xA00, while tclass
+ * appears to be confined to the lower 8 bits, resulting in almost perfect
+ * packing of the indexes into a single 32-bit value.
+ *
+ * The function still produces reasonable hash values even when input value
+ * ranges span beyond 11 bits, as long as the placement of entropy within the
+ * input values is roughly the same for each of the componets (a, b, c), and
+ * the address space (a, b, c) is sparsely populated. Such behaviour is the
+ * result of two conscious choices: (1) use of rol32() to preserve all of the
+ * incoming entropy (as opposed to simple shifts which discard some input bits)
+ * and (2) use of arithmetic addition which carries over colliding bits (as
+ * opposed to binary XOR, which does not carry).
+ *
+ * The function will produce horrible collisions if input entropy is distrubuted
+ * within (a, b, c) such that it ends up within the same bit ranges after
+ * rotations, and the address space is densly populated. If that is the case,
+ * then two options are available:
+ * 1. Try switching around some of the inputs. EX: (a, b, c) => (b, c, a)
+ * 2. Use a real hash, such as jhash_3words() from linux/jhash.h
+ */
+static inline u32 hash3(u32 a, u32 b, u32 c, int bits)
+{
+	return hash_32(a + rol32(b, 11) + rol32(c, 22), bits);
+}
+
+#endif /* #ifndef _SELINUX_HASH3_H */
diff --git a/security/selinux/ss/avtab.c b/security/selinux/ss/avtab.c
index 8baaddb01a95..d24f18ab9e4d 100644
--- a/security/selinux/ss/avtab.c
+++ b/security/selinux/ss/avtab.c
@@ -22,6 +22,7 @@
 #include <linux/errno.h>
 #include "avtab.h"
 #include "policydb.h"
+#include "hash3.h"
 
 #ifdef CONFIG_SECURITY_SELINUX_DEBUG_HASHES
 #include "median.h"
@@ -30,43 +31,10 @@
 static struct kmem_cache *avtab_node_cachep;
 static struct kmem_cache *avtab_xperms_cachep;
 
-/* Based on MurmurHash3, written by Austin Appleby and placed in the
- * public domain.
- */
-static inline int avtab_hash(struct avtab_key *keyp, u32 mask)
+static inline u32 avtab_hash(struct avtab_key *keyp, u32 bits)
 {
-	static const u32 c1 = 0xcc9e2d51;
-	static const u32 c2 = 0x1b873593;
-	static const u32 r1 = 15;
-	static const u32 r2 = 13;
-	static const u32 m  = 5;
-	static const u32 n  = 0xe6546b64;
-
-	u32 hash = 0;
-
-#define mix(input) { \
-	u32 v = input; \
-	v *= c1; \
-	v = (v << r1) | (v >> (32 - r1)); \
-	v *= c2; \
-	hash ^= v; \
-	hash = (hash << r2) | (hash >> (32 - r2)); \
-	hash = hash * m + n; \
-}
-
-	mix(keyp->target_class);
-	mix(keyp->target_type);
-	mix(keyp->source_type);
-
-#undef mix
-
-	hash ^= hash >> 16;
-	hash *= 0x85ebca6b;
-	hash ^= hash >> 13;
-	hash *= 0xc2b2ae35;
-	hash ^= hash >> 16;
-
-	return hash & mask;
+	return hash3(keyp->target_class, keyp->target_type,
+		     keyp->source_type, bits);
 }
 
 static struct avtab_node*
@@ -116,7 +84,7 @@ static int avtab_insert(struct avtab *h, struct avtab_key *key, struct avtab_dat
 	if (!h)
 		return -EINVAL;
 
-	hvalue = avtab_hash(key, h->mask);
+	hvalue = avtab_hash(key, h->bits);
 	for (prev = NULL, cur = h->htable[hvalue];
 	     cur;
 	     prev = cur, cur = cur->next) {
@@ -160,7 +128,7 @@ avtab_insert_nonunique(struct avtab *h, struct avtab_key *key, struct avtab_datu
 
 	if (!h)
 		return NULL;
-	hvalue = avtab_hash(key, h->mask);
+	hvalue = avtab_hash(key, h->bits);
 	for (prev = NULL, cur = h->htable[hvalue];
 	     cur;
 	     prev = cur, cur = cur->next) {
@@ -191,7 +159,7 @@ struct avtab_datum *avtab_search(struct avtab *h, struct avtab_key *key)
 	if (!h)
 		return NULL;
 
-	hvalue = avtab_hash(key, h->mask);
+	hvalue = avtab_hash(key, h->bits);
 	for (cur = h->htable[hvalue]; cur;
 	     cur = cur->next) {
 		if (key->source_type == cur->key.source_type &&
@@ -227,7 +195,7 @@ avtab_search_node(struct avtab *h, struct avtab_key *key)
 	if (!h)
 		return NULL;
 
-	hvalue = avtab_hash(key, h->mask);
+	hvalue = avtab_hash(key, h->bits);
 	for (cur = h->htable[hvalue]; cur;
 	     cur = cur->next) {
 		if (key->source_type == cur->key.source_type &&
@@ -301,6 +269,7 @@ void avtab_destroy(struct avtab *h)
 	h->htable = NULL;
 	h->nslot = 0;
 	h->mask = 0;
+	h->bits = 0;
 }
 
 void avtab_init(struct avtab *h)
@@ -313,6 +282,7 @@ void avtab_init(struct avtab *h)
 int avtab_alloc(struct avtab *h, u32 nrules)
 {
 	u32 mask = 0;
+	u32 bits = 0;
 	u32 shift = 0;
 	u32 work = nrules;
 	u32 nslot = 0;
@@ -339,6 +309,13 @@ int avtab_alloc(struct avtab *h, u32 nrules)
 	h->nel = 0;
 	h->nslot = nslot;
 	h->mask = mask;
+
+	while (mask) {
+		mask  = mask >> 1;
+		bits++;
+	}
+
+	h->bits = bits;
 	pr_debug("SELinux: %d avtab hash slots, %d rules.\n",
 	       h->nslot, nrules);
 	return 0;
diff --git a/security/selinux/ss/avtab.h b/security/selinux/ss/avtab.h
index 5fdcb6696bcc..bf24d8094019 100644
--- a/security/selinux/ss/avtab.h
+++ b/security/selinux/ss/avtab.h
@@ -85,6 +85,7 @@ struct avtab {
 	u32 nel;	/* number of elements */
 	u32 nslot;      /* number of hash slots */
 	u32 mask;       /* mask to compute hash func */
+	u32 bits;       /* number of bits in mask */
 };
 
 void avtab_init(struct avtab *h);
-- 
2.17.1




[Index of Archives]     [Selinux Refpolicy]     [Linux SGX]     [Fedora Users]     [Fedora Desktop]     [Yosemite Photos]     [Yosemite Camping]     [Yosemite Campsites]     [KDE Users]     [Gnome Users]

  Powered by Linux