Fix CVE-2016-5384 (double-free resulting in arbitrary code execution):

<https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5384>

Copied from upstream code repository:

<https://cgit.freedesktop.org/fontconfig/commit/?id=7a4a5bd7897d216f0794ca9dbce0a4a5c9d14940>

From 7a4a5bd7897d216f0794ca9dbce0a4a5c9d14940 Mon Sep 17 00:00:00 2001
From: Tobias Stoeckmann <tobias@stoeckmann.org>
Date: Sat, 25 Jun 2016 19:18:53 +0200
Subject: Properly validate offsets in cache files.

The cache files are insufficiently validated. Even though the magic
number at the beginning of the file as well as time stamps are checked,
it is not verified if contained offsets are in legal ranges or are
even pointers.

The lack of validation allows an attacker to trigger arbitrary free()
calls, which in turn allows double free attacks and therefore arbitrary
code execution. Due to the conversion from offsets into pointers through
macros, this even allows to circumvent ASLR protections.

This attack vector allows privilege escalation when used with setuid
binaries like fbterm. A user can create ~/.fonts or any other
system-defined user-private font directory, run fc-cache and adjust
cache files in ~/.cache/fontconfig. The execution of setuid binaries will
scan these files and therefore are prone to attacks.

If it's not about code execution, an endless loop can be created by
letting linked lists become circular linked lists.

This patch verifies that:

- The file is not larger than the maximum addressable space, which
  basically only affects 32 bit systems. This allows out of boundary
  access into unallocated memory.
- Offsets are always positive or zero
- Offsets do not point outside file boundaries
- No pointers are allowed in cache files, every "pointer or offset"
  field must be an offset or NULL
- Iterating linked lists must not take longer than the amount of elements
  specified. A violation of this rule can break a possible endless loop.

If one or more of these points are violated, the cache is recreated.
This is current behaviour.

Even though this patch fixes many issues, the use of mmap() shall be
forbidden in setuid binaries. It is impossible to guarantee with these
checks that a malicious user does not change cache files after
verification. This should be handled in a different patch.

Signed-off-by: Tobias Stoeckmann <tobias@stoeckmann.org>

diff --git a/src/fccache.c b/src/fccache.c
index 71e8f03..02ec301 100644
--- a/src/fccache.c
+++ b/src/fccache.c
@@ -27,6 +27,7 @@
 #include <fcntl.h>
 #include <dirent.h>
 #include <string.h>
+#include <limits.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <assert.h>
@@ -587,6 +588,82 @@ FcCacheTimeValid (FcConfig *config, FcCache *cache, struct stat *dir_stat)
     return cache->checksum == (int) dir_stat->st_mtime && fnano;
 }
 
+static FcBool
+FcCacheOffsetsValid (FcCache *cache)
+{
+    char		*base = (char *)cache;
+    char		*end = base + cache->size;
+    intptr_t		*dirs;
+    FcFontSet		*fs;
+    int			 i, j;
+
+    if (cache->dir < 0 || cache->dir > cache->size - sizeof (intptr_t) ||
+        memchr (base + cache->dir, '\0', cache->size - cache->dir) == NULL)
+        return FcFalse;
+
+    if (cache->dirs < 0 || cache->dirs >= cache->size ||
+        cache->dirs_count < 0 ||
+        cache->dirs_count > (cache->size - cache->dirs) / sizeof (intptr_t))
+        return FcFalse;
+
+    dirs = FcCacheDirs (cache);
+    if (dirs)
+    {
+        for (i = 0; i < cache->dirs_count; i++)
+        {
+            FcChar8	*dir;
+
+            if (dirs[i] < 0 ||
+                dirs[i] > end - (char *) dirs - sizeof (intptr_t))
+                return FcFalse;
+
+            dir = FcOffsetToPtr (dirs, dirs[i], FcChar8);
+            if (memchr (dir, '\0', end - (char *) dir) == NULL)
+                return FcFalse;
+         }
+    }
+
+    if (cache->set < 0 || cache->set > cache->size - sizeof (FcFontSet))
+        return FcFalse;
+
+    fs = FcCacheSet (cache);
+    if (fs)
+    {
+        if (fs->nfont > (end - (char *) fs) / sizeof (FcPattern))
+            return FcFalse;
+
+        if (fs->fonts != 0 && !FcIsEncodedOffset(fs->fonts))
+            return FcFalse;
+
+        for (i = 0; i < fs->nfont; i++)
+        {
+            FcPattern		*font = FcFontSetFont (fs, i);
+            FcPatternElt	*e;
+            FcValueListPtr	 l;
+
+            if ((char *) font < base ||
+                (char *) font > end - sizeof (FcFontSet) ||
+                font->elts_offset < 0 ||
+                font->elts_offset > end - (char *) font ||
+                font->num > (end - (char *) font - font->elts_offset) / sizeof (FcPatternElt))
+                return FcFalse;
+
+
+            e = FcPatternElts(font);
+            if (e->values != 0 && !FcIsEncodedOffset(e->values))
+                return FcFalse;
+
+            for (j = font->num, l = FcPatternEltValues(e); j >= 0 && l; j--, l = FcValueListNext(l))
+                if (l->next != NULL && !FcIsEncodedOffset(l->next))
+                    break;
+            if (j < 0)
+                return FcFalse;
+        }
+    }
+
+    return FcTrue;
+}
+
 /*
  * Map a cache file into memory
  */
@@ -596,7 +673,8 @@ FcDirCacheMapFd (FcConfig *config, int fd, struct stat *fd_stat, struct stat *di
     FcCache	*cache;
     FcBool	allocated = FcFalse;
 
-    if (fd_stat->st_size < (int) sizeof (FcCache))
+    if (fd_stat->st_size > INTPTR_MAX ||
+        fd_stat->st_size < (int) sizeof (FcCache))
 	return NULL;
     cache = FcCacheFindByStat (fd_stat);
     if (cache)
@@ -652,6 +730,7 @@ FcDirCacheMapFd (FcConfig *config, int fd, struct stat *fd_stat, struct stat *di
     if (cache->magic != FC_CACHE_MAGIC_MMAP ||
 	cache->version < FC_CACHE_VERSION_NUMBER ||
 	cache->size != (intptr_t) fd_stat->st_size ||
+        !FcCacheOffsetsValid (cache) ||
 	!FcCacheTimeValid (config, cache, dir_stat) ||
 	!FcCacheInsert (cache, fd_stat))
     {
-- 
cgit v0.10.2

