Subversion HTTP servers with FSFS repositories are vulnerable to a remotely triggerable excessive memory use with certain REPORT requests. Summary: ======== Subversion's mod_dav_svn Apache HTTPD server module may use excessive amounts of memory when processing REPORT requests that require traversing through a large number of FSFS repository nodes (files and directories). This can lead to a DoS. There are no known instances of this problem being observed in the wild, but an exploit has been tested. Known vulnerable: ================= Subversion HTTPD servers 1.8.0 through 1.8.11 (inclusive) Known fixed: ============ Subversion 1.8.13 svnserve (any version) is not vulnerable Subversion 1.8.12 was not publicly released. Details: ======== Subversion FSFS repositories cache different types of data for performance reasons. An FSFS repository filesystem is structured as a direct acyclic graph (DAG), and it has a special cache for the DAG nodes. Subversion 1.8.0 added an additional level of caching for the DAG nodes, and the excessive memory use is a consequence of the cached nodes not being deallocated in a timely manner. HTTPD Server and Subversion use memory pools for allocations. Certain REPORT requests can trigger a state when the cache keeps allocating new elements from a pool, but the previously allocated elements are not being deallocated. This memory will be reclaimed eventually (once the request finishes or when the cache leaves the inappropriate state), but multiple parallel requests might ultimately exhaust all the available memory on the server. Severity: ========= CVSSv2 Base Score: 5.0 CVSSv2 Base Vector: AV:N/AC:L/Au:N/C:N/I:N/A:P We consider this to be a medium risk vulnerability. Repositories which allow for anonymous reads will be vulnerable without authentication. Unfortunately, no special configuration is required and all mod_dav_svn servers with FSFS repositories are vulnerable. Apache HTTPD servers that block potentially expensive requests via mod_dontdothat module have a smaller attack surface, but are still vulnerable. Actual memory consumption (per request) depends on the layout and size of the particular repository, but is potentially unbounded. The impact of using this memory varies wildly based on operating system and httpd configuration. Some operating systems may kill off processes or crash if too much memory is used. The Apache HTTPD configuration option of MaxRequestsPerChild may restart a process after a certain number of requests and limit the impact of accidental exercise of this issue. However, a determined attacker could repeat the requests and mitigate any countermeasures. Recommendations: ================ We recommend all users to upgrade to Subversion 1.8.13. Users of Subversion 1.8.x who are unable to upgrade may apply the included patch. New Subversion packages can be found at: http://subversion.apache.org/packages.html There is no effective configuration that can mitigate the issue entirely however the use of ulimit (or the equivalent) to set memory limits for processes may help prevent the impact affecting other services running on the same machine. References: =========== CVE-2015-0202 (Subversion) Reported by: ============ Evgeny Kotkov, VisualSVN Patches: ======== Patch against 1.8.11: [[[ Index: subversion/libsvn_fs_fs/tree.c =================================================================== --- subversion/libsvn_fs_fs/tree.c (revision 1655679) +++ subversion/libsvn_fs_fs/tree.c (working copy) @@ -127,7 +127,6 @@ typedef struct fs_txn_root_data_t static svn_error_t * get_dag(dag_node_t **dag_node_p, svn_fs_root_t *root, const char *path, - svn_boolean_t needs_lock_cache, apr_pool_t *pool); static svn_fs_root_t *make_revision_root(svn_fs_t *fs, svn_revnum_t rev, @@ -178,34 +177,10 @@ typedef struct cache_entry_t */ enum { BUCKET_COUNT = 256 }; -/* Each pool that has received a DAG node, will hold at least on lock on - our cache to ensure that the node remains valid despite being allocated - in the cache's pool. This is the structure to represent the lock. - */ -typedef struct cache_lock_t -{ - /* pool holding the lock */ - apr_pool_t *pool; - - /* cache being locked */ - fs_fs_dag_cache_t *cache; - - /* next lock. NULL at EOL */ - struct cache_lock_t *next; - - /* previous lock. NULL at list head. Only then this==cache->first_lock */ - struct cache_lock_t *prev; -} cache_lock_t; - /* The actual cache structure. All nodes will be allocated in POOL. When the number of INSERTIONS (i.e. objects created form that pool) exceeds a certain threshold, the pool will be cleared and the cache with it. - - To ensure that nodes returned from this structure remain valid, the - cache will get locked for the lifetime of the _receiving_ pools (i.e. - those in which we would allocate the node if there was no cache.). - The cache will only be cleared FIRST_LOCK is 0. */ struct fs_fs_dag_cache_t { @@ -221,47 +196,8 @@ struct fs_fs_dag_cache_t /* Property lookups etc. have a very high locality (75% re-hit). Thus, remember the last hit location for optimistic lookup. */ apr_size_t last_hit; - - /* List of receiving pools that are still alive. */ - cache_lock_t *first_lock; }; -/* Cleanup function to be called when a receiving pool gets cleared. - Unlocks the cache once. - */ -static apr_status_t -unlock_cache(void *baton_void) -{ - cache_lock_t *lock = baton_void; - - /* remove lock from chain. Update the head */ - if (lock->next) - lock->next->prev = lock->prev; - if (lock->prev) - lock->prev->next = lock->next; - else - lock->cache->first_lock = lock->next; - - return APR_SUCCESS; -} - -/* Cleanup function to be called when the cache itself gets destroyed. - In that case, we must unregister all unlock requests. - */ -static apr_status_t -unregister_locks(void *baton_void) -{ - fs_fs_dag_cache_t *cache = baton_void; - cache_lock_t *lock; - - for (lock = cache->first_lock; lock; lock = lock->next) - apr_pool_cleanup_kill(lock->pool, - lock, - unlock_cache); - - return APR_SUCCESS; -} - fs_fs_dag_cache_t* svn_fs_fs__create_dag_cache(apr_pool_t *pool) { @@ -268,59 +204,15 @@ svn_fs_fs__create_dag_cache(apr_pool_t *pool) fs_fs_dag_cache_t *result = apr_pcalloc(pool, sizeof(*result)); result->pool = svn_pool_create(pool); - apr_pool_cleanup_register(pool, - result, - unregister_locks, - apr_pool_cleanup_null); - return result; } -/* Prevent the entries in CACHE from being destroyed, for as long as the - POOL lives. - */ -static void -lock_cache(fs_fs_dag_cache_t* cache, apr_pool_t *pool) -{ - /* we only need to lock / unlock once per pool. Since we will often ask - for multiple nodes with the same pool, we can reduce the overhead. - However, if e.g. pools are being used in an alternating pattern, - we may lock the cache more than once for the same pool (and register - just as many cleanup actions). - */ - cache_lock_t *lock = cache->first_lock; - - /* try to find an existing lock for POOL. - But limit the time spent on chasing pointers. */ - int limiter = 8; - while (lock && --limiter) - if (lock->pool == pool) - return; - - /* create a new lock and put it at the beginning of the lock chain */ - lock = apr_palloc(pool, sizeof(*lock)); - lock->cache = cache; - lock->pool = pool; - lock->next = cache->first_lock; - lock->prev = NULL; - - if (cache->first_lock) - cache->first_lock->prev = lock; - cache->first_lock = lock; - - /* instruct POOL to remove the look upon cleanup */ - apr_pool_cleanup_register(pool, - lock, - unlock_cache, - apr_pool_cleanup_null); -} - /* Clears the CACHE at regular intervals (destroying all cached nodes) */ static void auto_clear_dag_cache(fs_fs_dag_cache_t* cache) { - if (cache->first_lock == NULL && cache->insertions > BUCKET_COUNT) + if (cache->insertions > BUCKET_COUNT) { svn_pool_clear(cache->pool); @@ -433,18 +325,12 @@ locate_cache(svn_cache__t **cache, } } -/* Return NODE for PATH from ROOT's node cache, or NULL if the node - isn't cached; read it from the FS. *NODE remains valid until either - POOL or the FS gets cleared or destroyed (whichever comes first). - - Since locking can be expensive and POOL may be long-living, for - nodes that will not need to survive the next call to this function, - set NEEDS_LOCK_CACHE to FALSE. */ +/* Return NODE_P for PATH from ROOT's node cache, or NULL if the node + isn't cached; read it from the FS. *NODE_P is allocated in POOL. */ static svn_error_t * dag_node_cache_get(dag_node_t **node_p, svn_fs_root_t *root, const char *path, - svn_boolean_t needs_lock_cache, apr_pool_t *pool) { svn_boolean_t found; @@ -466,25 +352,23 @@ dag_node_cache_get(dag_node_t **node_p, if (bucket->node == NULL) { locate_cache(&cache, &key, root, path, pool); - SVN_ERR(svn_cache__get((void **)&node, &found, cache, key, - ffd->dag_node_cache->pool)); + SVN_ERR(svn_cache__get((void **)&node, &found, cache, key, pool)); if (found && node) { /* Patch up the FS, since this might have come from an old FS * object. */ svn_fs_fs__dag_set_fs(node, root->fs); - bucket->node = node; + + /* Retain the DAG node in L1 cache. */ + bucket->node = svn_fs_fs__dag_dup(node, + ffd->dag_node_cache->pool); } } else { - node = bucket->node; + /* Copy the node from L1 cache into the passed-in POOL. */ + node = svn_fs_fs__dag_dup(bucket->node, pool); } - - /* if we found a node, make sure it remains valid at least as long - as it would when allocated in POOL. */ - if (node && needs_lock_cache) - lock_cache(ffd->dag_node_cache, pool); } else { @@ -822,7 +706,7 @@ get_copy_inheritance(copy_id_inherit_t *inherit_p, SVN_ERR(svn_fs_fs__dag_get_copyroot(©root_rev, ©root_path, child->node)); SVN_ERR(svn_fs_fs__revision_root(©root_root, fs, copyroot_rev, pool)); - SVN_ERR(get_dag(©root_node, copyroot_root, copyroot_path, FALSE, pool)); + SVN_ERR(get_dag(©root_node, copyroot_root, copyroot_path, pool)); copyroot_id = svn_fs_fs__dag_get_id(copyroot_node); if (svn_fs_fs__id_compare(copyroot_id, child_id) == -1) @@ -938,7 +822,7 @@ open_path(parent_path_t **parent_path_p, { directory = svn_dirent_dirname(path, pool); if (directory[1] != 0) /* root nodes are covered anyway */ - SVN_ERR(dag_node_cache_get(&here, root, directory, TRUE, pool)); + SVN_ERR(dag_node_cache_get(&here, root, directory, pool)); } /* did the shortcut work? */ @@ -998,8 +882,8 @@ open_path(parent_path_t **parent_path_p, element if we already know the lookup to fail for the complete path. */ if (next || !(flags & open_path_uncached)) - SVN_ERR(dag_node_cache_get(&cached_node, root, path_so_far, - TRUE, pool)); + SVN_ERR(dag_node_cache_get(&cached_node, root, path_so_far, pool)); + if (cached_node) child = cached_node; else @@ -1136,8 +1020,7 @@ make_path_mutable(svn_fs_root_t *root, parent_path->node)); SVN_ERR(svn_fs_fs__revision_root(©root_root, root->fs, copyroot_rev, pool)); - SVN_ERR(get_dag(©root_node, copyroot_root, copyroot_path, - FALSE, pool)); + SVN_ERR(get_dag(©root_node, copyroot_root, copyroot_path, pool)); child_id = svn_fs_fs__dag_get_id(parent_path->node); copyroot_id = svn_fs_fs__dag_get_id(copyroot_node); @@ -1174,16 +1057,11 @@ make_path_mutable(svn_fs_root_t *root, /* Open the node identified by PATH in ROOT. Set DAG_NODE_P to the node we find, allocated in POOL. Return the error - SVN_ERR_FS_NOT_FOUND if this node doesn't exist. - - Since locking can be expensive and POOL may be long-living, for - nodes that will not need to survive the next call to this function, - set NEEDS_LOCK_CACHE to FALSE. */ + SVN_ERR_FS_NOT_FOUND if this node doesn't exist. */ static svn_error_t * get_dag(dag_node_t **dag_node_p, svn_fs_root_t *root, const char *path, - svn_boolean_t needs_lock_cache, apr_pool_t *pool) { parent_path_t *parent_path; @@ -1192,7 +1070,7 @@ get_dag(dag_node_t **dag_node_p, /* First we look for the DAG in our cache (if the path may be canonical). */ if (*path == '/') - SVN_ERR(dag_node_cache_get(&node, root, path, needs_lock_cache, pool)); + SVN_ERR(dag_node_cache_get(&node, root, path, pool)); if (! node) { @@ -1202,8 +1080,7 @@ get_dag(dag_node_t **dag_node_p, path = svn_fs__canonicalize_abspath(path, pool); /* Try again with the corrected path. */ - SVN_ERR(dag_node_cache_get(&node, root, path, needs_lock_cache, - pool)); + SVN_ERR(dag_node_cache_get(&node, root, path, pool)); } if (! node) @@ -1281,7 +1158,7 @@ svn_fs_fs__node_id(const svn_fs_id_t **id_p, { dag_node_t *node; - SVN_ERR(get_dag(&node, root, path, FALSE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); *id_p = svn_fs_fs__id_copy(svn_fs_fs__dag_get_id(node), pool); } return SVN_NO_ERROR; @@ -1296,7 +1173,7 @@ svn_fs_fs__node_created_rev(svn_revnum_t *revision { dag_node_t *node; - SVN_ERR(get_dag(&node, root, path, FALSE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); return svn_fs_fs__dag_get_revision(revision, node, pool); } @@ -1311,7 +1188,7 @@ fs_node_created_path(const char **created_path, { dag_node_t *node; - SVN_ERR(get_dag(&node, root, path, TRUE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); *created_path = svn_fs_fs__dag_get_created_path(node); return SVN_NO_ERROR; @@ -1375,7 +1252,7 @@ fs_node_prop(svn_string_t **value_p, dag_node_t *node; apr_hash_t *proplist; - SVN_ERR(get_dag(&node, root, path, FALSE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); SVN_ERR(svn_fs_fs__dag_get_proplist(&proplist, node, pool)); *value_p = NULL; if (proplist) @@ -1398,7 +1275,7 @@ fs_node_proplist(apr_hash_t **table_p, apr_hash_t *table; dag_node_t *node; - SVN_ERR(get_dag(&node, root, path, FALSE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); SVN_ERR(svn_fs_fs__dag_get_proplist(&table, node, pool)); *table_p = table ? table : apr_hash_make(pool); @@ -1515,8 +1392,8 @@ fs_props_changed(svn_boolean_t *changed_p, (SVN_ERR_FS_GENERAL, NULL, _("Cannot compare property value between two different filesystems")); - SVN_ERR(get_dag(&node1, root1, path1, TRUE, pool)); - SVN_ERR(get_dag(&node2, root2, path2, TRUE, pool)); + SVN_ERR(get_dag(&node1, root1, path1, pool)); + SVN_ERR(get_dag(&node2, root2, path2, pool)); return svn_fs_fs__dag_things_different(changed_p, NULL, node1, node2); } @@ -1529,7 +1406,7 @@ fs_props_changed(svn_boolean_t *changed_p, static svn_error_t * get_root(dag_node_t **node, svn_fs_root_t *root, apr_pool_t *pool) { - return get_dag(node, root, "/", TRUE, pool); + return get_dag(node, root, "/", pool); } @@ -2193,7 +2070,7 @@ fs_dir_entries(apr_hash_t **table_p, dag_node_t *node; /* Get the entries for this path in the caller's pool. */ - SVN_ERR(get_dag(&node, root, path, FALSE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); return svn_fs_fs__dag_dir_entries(table_p, node, pool); } @@ -2365,7 +2242,7 @@ copy_helper(svn_fs_root_t *from_root, _("Copy from mutable tree not currently supported")); /* Get the NODE for FROM_PATH in FROM_ROOT.*/ - SVN_ERR(get_dag(&from_node, from_root, from_path, TRUE, pool)); + SVN_ERR(get_dag(&from_node, from_root, from_path, pool)); /* Build up the parent path from TO_PATH in TO_ROOT. If the last component does not exist, it's not that big a deal. We'll just @@ -2442,7 +2319,7 @@ copy_helper(svn_fs_root_t *from_root, pool)); /* Make a record of this modification in the changes table. */ - SVN_ERR(get_dag(&new_node, to_root, to_path, TRUE, pool)); + SVN_ERR(get_dag(&new_node, to_root, to_path, pool)); SVN_ERR(add_change(to_root->fs, txn_id, to_path, svn_fs_fs__dag_get_id(new_node), kind, FALSE, FALSE, svn_fs_fs__dag_node_kind(from_node), @@ -2553,7 +2430,7 @@ fs_copied_from(svn_revnum_t *rev_p, { /* There is no cached entry, look it up the old-fashioned way. */ - SVN_ERR(get_dag(&node, root, path, TRUE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); SVN_ERR(svn_fs_fs__dag_get_copyfrom_rev(©from_rev, node)); SVN_ERR(svn_fs_fs__dag_get_copyfrom_path(©from_path, node)); } @@ -2628,7 +2505,7 @@ fs_file_length(svn_filesize_t *length_p, dag_node_t *file; /* First create a dag_node_t from the root/path pair. */ - SVN_ERR(get_dag(&file, root, path, FALSE, pool)); + SVN_ERR(get_dag(&file, root, path, pool)); /* Now fetch its length */ return svn_fs_fs__dag_file_length(length_p, file, pool); @@ -2647,7 +2524,7 @@ fs_file_checksum(svn_checksum_t **checksum, { dag_node_t *file; - SVN_ERR(get_dag(&file, root, path, FALSE, pool)); + SVN_ERR(get_dag(&file, root, path, pool)); return svn_fs_fs__dag_file_checksum(checksum, file, kind, pool); } @@ -2666,7 +2543,7 @@ fs_file_contents(svn_stream_t **contents, svn_stream_t *file_stream; /* First create a dag_node_t from the root/path pair. */ - SVN_ERR(get_dag(&node, root, path, FALSE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); /* Then create a readable stream from the dag_node_t. */ SVN_ERR(svn_fs_fs__dag_get_contents(&file_stream, node, pool)); @@ -2689,7 +2566,7 @@ fs_try_process_file_contents(svn_boolean_t *succes apr_pool_t *pool) { dag_node_t *node; - SVN_ERR(get_dag(&node, root, path, FALSE, pool)); + SVN_ERR(get_dag(&node, root, path, pool)); return svn_fs_fs__dag_try_process_file_contents(success, node, processor, baton, pool); @@ -3071,8 +2948,8 @@ fs_contents_changed(svn_boolean_t *changed_p, (SVN_ERR_FS_GENERAL, NULL, _("'%s' is not a file"), path2); } - SVN_ERR(get_dag(&node1, root1, path1, TRUE, pool)); - SVN_ERR(get_dag(&node2, root2, path2, TRUE, pool)); + SVN_ERR(get_dag(&node1, root1, path1, pool)); + SVN_ERR(get_dag(&node2, root2, path2, pool)); return svn_fs_fs__dag_things_different(NULL, changed_p, node1, node2); } @@ -3092,10 +2969,10 @@ fs_get_file_delta_stream(svn_txdelta_stream_t **st dag_node_t *source_node, *target_node; if (source_root && source_path) - SVN_ERR(get_dag(&source_node, source_root, source_path, TRUE, pool)); + SVN_ERR(get_dag(&source_node, source_root, source_path, pool)); else source_node = NULL; - SVN_ERR(get_dag(&target_node, target_root, target_path, TRUE, pool)); + SVN_ERR(get_dag(&target_node, target_root, target_path, pool)); /* Create a delta stream that turns the source into the target. */ return svn_fs_fs__dag_get_file_delta_stream(stream_p, source_node, @@ -3588,7 +3465,7 @@ history_prev(void *baton, apr_pool_t *pool) SVN_ERR(svn_fs_fs__revision_root(©root_root, fs, copyroot_rev, pool)); - SVN_ERR(get_dag(&node, copyroot_root, copyroot_path, FALSE, pool)); + SVN_ERR(get_dag(&node, copyroot_root, copyroot_path, pool)); copy_dst = svn_fs_fs__dag_get_created_path(node); /* If our current path was the very destination of the copy, @@ -3785,7 +3662,7 @@ crawl_directory_dag_for_mergeinfo(svn_fs_root_t *r svn_pool_clear(iterpool); kid_path = svn_fspath__join(this_path, dirent->name, iterpool); - SVN_ERR(get_dag(&kid_dag, root, kid_path, TRUE, iterpool)); + SVN_ERR(get_dag(&kid_dag, root, kid_path, iterpool)); SVN_ERR(svn_fs_fs__dag_has_mergeinfo(&has_mergeinfo, kid_dag)); SVN_ERR(svn_fs_fs__dag_has_descendants_with_mergeinfo(&go_down, kid_dag)); @@ -4031,7 +3908,7 @@ add_descendant_mergeinfo(svn_mergeinfo_catalog_t r dag_node_t *this_dag; svn_boolean_t go_down; - SVN_ERR(get_dag(&this_dag, root, path, TRUE, scratch_pool)); + SVN_ERR(get_dag(&this_dag, root, path, scratch_pool)); SVN_ERR(svn_fs_fs__dag_has_descendants_with_mergeinfo(&go_down, this_dag)); if (go_down) ]]]