prop224: Directory cache support

This implements the proposal 224 directory descriptor cache store and lookup
functionalities. Furthermore, it merges the OOM call for the HSDir cache with
current protocol v2 and the new upcoming v3.

Add hs_cache.{c|h} with store/lookup API.

Closes #18572

Signed-off-by: David Goulet <dgoulet@torproject.org>
Signed-off-by: George Kadianakis <desnacked@riseup.net>
This commit is contained in:
David Goulet 2016-03-29 15:08:04 -04:00 committed by David Goulet
parent 473f99bf7b
commit 025610612d
11 changed files with 483 additions and 74 deletions

364
src/or/hs_cache.c Normal file
View File

@ -0,0 +1,364 @@
/* Copyright (c) 2016, The Tor Project, Inc. */
/* See LICENSE for licensing information */
/**
* \file hs_cache.c
* \brief Handle hidden service descriptor caches.
**/
#include "hs_cache.h"
#include "or.h"
#include "config.h"
#include "hs_common.h"
#include "hs_descriptor.h"
#include "rendcache.h"
/* Directory descriptor cache. Map indexed by blinded key. */
static digest256map_t *hs_cache_v3_dir;
/* Remove a given descriptor from our cache. */
static void
remove_v3_desc_as_dir(const hs_cache_dir_descriptor_t *desc)
{
tor_assert(desc);
digest256map_remove(hs_cache_v3_dir, desc->key);
}
/* Store a given descriptor in our cache. */
static void
store_v3_desc_as_dir(hs_cache_dir_descriptor_t *desc)
{
tor_assert(desc);
digest256map_set(hs_cache_v3_dir, desc->key, desc);
}
/* Query our cache and return the entry or NULL if not found. */
static hs_cache_dir_descriptor_t *
lookup_v3_desc_as_dir(const uint8_t *key)
{
tor_assert(key);
return digest256map_get(hs_cache_v3_dir, key);
}
/* Free a directory descriptor object. */
static void
cache_dir_desc_free(hs_cache_dir_descriptor_t *desc)
{
if (desc == NULL) {
return;
}
hs_desc_plaintext_data_free(desc->plaintext_data);
tor_free(desc->encoded_desc);
tor_free(desc);
}
/* Create a new directory cache descriptor object from a encoded descriptor.
* On success, return the heap-allocated cache object, otherwise return NULL if
* we can't decode the descriptor. */
static hs_cache_dir_descriptor_t *
cache_dir_desc_new(const char *desc)
{
hs_cache_dir_descriptor_t *dir_desc;
tor_assert(desc);
dir_desc = tor_malloc_zero(sizeof(hs_cache_dir_descriptor_t));
dir_desc->plaintext_data =
tor_malloc_zero(sizeof(hs_desc_plaintext_data_t));
dir_desc->encoded_desc = tor_strdup(desc);
if (hs_desc_decode_plaintext(desc, dir_desc->plaintext_data) < 0) {
log_debug(LD_DIR, "Unable to decode descriptor. Rejecting.");
goto err;
}
/* The blinded pubkey is the indexed key. */
dir_desc->key = dir_desc->plaintext_data->blinded_kp.pubkey.pubkey;
dir_desc->created_ts = time(NULL);
return dir_desc;
err:
cache_dir_desc_free(dir_desc);
return NULL;
}
/* Return the size of a cache entry in bytes. */
static size_t
cache_get_entry_size(const hs_cache_dir_descriptor_t *entry)
{
return (sizeof(*entry) + hs_desc_plaintext_obj_size(entry->plaintext_data)
+ strlen(entry->encoded_desc));
}
/* Try to store a valid version 3 descriptor in the directory cache. Return 0
* on success else a negative value is returned indicating that we have a
* newer version in our cache. On error, caller is responsible to free the
* given descriptor desc. */
static int
cache_store_v3_as_dir(hs_cache_dir_descriptor_t *desc)
{
hs_cache_dir_descriptor_t *cache_entry;
tor_assert(desc);
/* Verify if we have an entry in the cache for that key and if yes, check
* if we should replace it? */
cache_entry = lookup_v3_desc_as_dir(desc->key);
if (cache_entry != NULL) {
/* Only replace descriptor if revision-counter is greater than the one
* in our cache */
if (cache_entry->plaintext_data->revision_counter >=
desc->plaintext_data->revision_counter) {
log_info(LD_REND, "Descriptor revision counter in our cache is "
"greater or equal than the one we received. "
"Rejecting!");
goto err;
}
/* We now know that the descriptor we just received is a new one so
* remove the entry we currently have from our cache so we can then
* store the new one. */
remove_v3_desc_as_dir(cache_entry);
cache_dir_desc_free(cache_entry);
rend_cache_decrement_allocation(cache_get_entry_size(cache_entry));
}
/* Store the descriptor we just got. We are sure here that either we
* don't have the entry or we have a newer descriptor and the old one
* has been removed from the cache. */
store_v3_desc_as_dir(desc);
/* Update our total cache size with this entry for the OOM. This uses the
* old HS protocol cache subsystem for which we are tied with. */
rend_cache_increment_allocation(cache_get_entry_size(desc));
/* XXX: Update HS statistics. We should have specific stats for v3. */
return 0;
err:
return -1;
}
/* Using the query which is the blinded key for a descriptor version 3, lookup
* in our directory cache the entry. If found, 1 is returned and desc_out is
* populated with a newly allocated string being the encoded descriptor. If
* not found, 0 is returned and desc_out is untouched. On error, a negative
* value is returned and desc_out is untouched. */
static int
cache_lookup_v3_as_dir(const char *query, char **desc_out)
{
int found = 0;
ed25519_public_key_t blinded_key;
const hs_cache_dir_descriptor_t *entry;
tor_assert(query);
/* Decode blinded key using the given query value. */
if (ed25519_public_from_base64(&blinded_key, query) < 0) {
log_info(LD_REND, "Unable to decode the v3 HSDir query %s.",
safe_str_client(query));
goto err;
}
entry = lookup_v3_desc_as_dir(blinded_key.pubkey);
if (entry != NULL) {
found = 1;
if (desc_out) {
*desc_out = tor_strdup(entry->encoded_desc);
}
}
return found;
err:
return -1;
}
/* Clean the v3 cache by removing any entry that has expired using the
* <b>global_cutoff</b> value. If <b>global_cutoff</b> is 0, the cleaning
* process will use the lifetime found in the plaintext data section. Return
* the number of bytes cleaned. */
static size_t
cache_clean_v3_as_dir(time_t now, time_t global_cutoff)
{
size_t bytes_removed = 0;
/* Code flow error if this ever happens. */
tor_assert(global_cutoff >= 0);
if (!hs_cache_v3_dir) { /* No cache to clean. Just return. */
return 0;
}
DIGEST256MAP_FOREACH_MODIFY(hs_cache_v3_dir, key,
hs_cache_dir_descriptor_t *, entry) {
size_t entry_size;
time_t cutoff = global_cutoff;
if (!cutoff) {
/* Cutoff is the lifetime of the entry found in the descriptor. */
cutoff = now - entry->plaintext_data->lifetime_sec;
}
/* If the entry has been created _after_ the cutoff, not expired so
* continue to the next entry in our v3 cache. */
if (entry->created_ts > cutoff) {
continue;
}
/* Here, our entry has expired, remove and free. */
MAP_DEL_CURRENT(key);
entry_size = cache_get_entry_size(entry);
bytes_removed += entry_size;
/* Entry is not in the cache anymore, destroy it. */
cache_dir_desc_free(entry);
/* Update our cache entry allocation size for the OOM. */
rend_cache_decrement_allocation(entry_size);
/* Logging. */
{
char key_b64[BASE64_DIGEST256_LEN + 1];
base64_encode(key_b64, sizeof(key_b64), (const char *) key,
DIGEST256_LEN, 0);
log_info(LD_REND, "Removing v3 descriptor '%s' from HSDir cache",
safe_str_client(key_b64));
}
} DIGEST256MAP_FOREACH_END;
return bytes_removed;
}
/* Given an encoded descriptor, store it in the directory cache depending on
* which version it is. Return a negative value on error. On success, 0 is
* returned. */
int
hs_cache_store_as_dir(const char *desc)
{
hs_cache_dir_descriptor_t *dir_desc = NULL;
tor_assert(desc);
/* Create a new cache object. This can fail if the descriptor plaintext data
* is unparseable which in this case a log message will be triggered. */
dir_desc = cache_dir_desc_new(desc);
if (dir_desc == NULL) {
goto err;
}
/* Call the right function against the descriptor version. At this point,
* we are sure that the descriptor's version is supported else the
* decoding would have failed. */
switch (dir_desc->plaintext_data->version) {
case 3:
default:
if (cache_store_v3_as_dir(dir_desc) < 0) {
goto err;
}
break;
}
return 0;
err:
cache_dir_desc_free(dir_desc);
return -1;
}
/* Using the query, lookup in our directory cache the entry. If found, 1 is
* returned and desc_out is populated with a newly allocated string being
* the encoded descriptor. If not found, 0 is returned and desc_out is
* untouched. On error, a negative value is returned and desc_out is
* untouched. */
int
hs_cache_lookup_as_dir(uint32_t version, const char *query,
char **desc_out)
{
int found;
tor_assert(query);
/* This should never be called with an unsupported version. */
tor_assert(hs_desc_is_supported_version(version));
switch (version) {
case 3:
default:
found = cache_lookup_v3_as_dir(query, desc_out);
break;
}
return found;
}
/* Clean all directory caches using the current time now. */
void
hs_cache_clean_as_dir(time_t now)
{
time_t cutoff;
/* Start with v2 cache cleaning. */
cutoff = now - rend_cache_max_entry_lifetime();
rend_cache_clean_v2_descs_as_dir(cutoff);
/* Now, clean the v3 cache. Set the cutoff to 0 telling the cleanup function
* to compute the cutoff by itself using the lifetime value. */
cache_clean_v3_as_dir(now, 0);
}
/* Do a round of OOM cleanup on all directory caches. Return the amount of
* removed bytes. It is possible that the returned value is lower than
* min_remove_bytes if the caches get emptied out so the caller should be
* aware of this. */
size_t
hs_cache_handle_oom(time_t now, size_t min_remove_bytes)
{
time_t k;
size_t bytes_removed = 0;
/* Our OOM handler called with 0 bytes to remove is a code flow error. */
tor_assert(min_remove_bytes != 0);
/* The algorithm is as follow. K is the oldest expected descriptor age.
*
* 1) Deallocate all entries from v2 cache that are older than K hours.
* 1.1) If the amount of remove bytes has been reached, stop.
* 2) Deallocate all entries from v3 cache that are older than K hours
* 2.1) If the amount of remove bytes has been reached, stop.
* 3) Set K = K - RendPostPeriod and repeat process until K is < 0.
*
* This ends up being O(Kn).
*/
/* Set K to the oldest expected age in seconds which is the maximum
* lifetime of a cache entry. We'll use the v2 lifetime because it's much
* bigger than the v3 thus leading to cleaning older descriptors. */
k = rend_cache_max_entry_lifetime();
do {
time_t cutoff;
/* If K becomes negative, it means we've empty the caches so stop and
* return what we were able to cleanup. */
if (k < 0) {
break;
}
/* Compute a cutoff value with K and the current time. */
cutoff = now - k;
/* Start by cleaning the v2 cache with that cutoff. */
bytes_removed += rend_cache_clean_v2_descs_as_dir(cutoff);
if (bytes_removed < min_remove_bytes) {
/* We haven't remove enough bytes so clean v3 cache. */
bytes_removed += cache_clean_v3_as_dir(now, cutoff);
/* Decrement K by a post period to shorten the cutoff. */
k -= get_options()->RendPostPeriod;
}
} while (bytes_removed < min_remove_bytes);
return bytes_removed;
}
/* Initialize the hidden service cache subsystem. */
void
hs_cache_init(void)
{
/* Calling this twice is very wrong code flow. */
tor_assert(!hs_cache_v3_dir);
hs_cache_v3_dir = digest256map_new();
}

53
src/or/hs_cache.h Normal file
View File

@ -0,0 +1,53 @@
/* Copyright (c) 2016, The Tor Project, Inc. */
/* See LICENSE for licensing information */
/**
* \file hs_cache.h
* \brief Header file for hs_cache.c
**/
#ifndef TOR_HS_CACHE_H
#define TOR_HS_CACHE_H
#include <stdint.h>
#include "crypto.h"
#include "crypto_ed25519.h"
#include "hs_common.h"
#include "hs_descriptor.h"
#include "torcert.h"
/* Descriptor representation on the directory side which is a subset of
* information that the HSDir can decode and serve it. */
typedef struct hs_cache_dir_descriptor_t {
/* This object is indexed using the blinded pubkey located in the plaintext
* data which is populated only once the descriptor has been successfully
* decoded and validated. This simply points to that pubkey. */
const uint8_t *key;
/* When does this entry has been created. Used to expire entries. */
time_t created_ts;
/* Descriptor plaintext information. Obviously, we can't decrypt the
* encrypted part of the descriptor. */
hs_desc_plaintext_data_t *plaintext_data;
/* Encoded descriptor which is basically in text form. It's a NUL terminated
* string thus safe to strlen(). */
char *encoded_desc;
} hs_cache_dir_descriptor_t;
/* Public API */
void hs_cache_init(void);
void hs_cache_clean_as_dir(time_t now);
size_t hs_cache_handle_oom(time_t now, size_t min_remove_bytes);
/* Store and Lookup function. They are version agnostic that is depending on
* the requested version of the descriptor, it will be re-routed to the
* right function. */
int hs_cache_store_as_dir(const char *desc);
int hs_cache_lookup_as_dir(uint32_t version, const char *query,
char **desc_out);
#endif /* TOR_HS_CACHE_H */

View File

@ -3,7 +3,7 @@
/**
* \file hs_common.h
* \brief Header file for hs_common.c.
* \brief Header file containing common data for the whole HS subsytem.
**/
#ifndef TOR_HS_COMMON_H

View File

@ -1918,3 +1918,15 @@ hs_descriptor_free(hs_descriptor_t *desc)
desc_encrypted_data_free_contents(&desc->encrypted_data);
tor_free(desc);
}
/* Return the size in bytes of the given plaintext data object. A sizeof() is
* not enough because the object contains pointers and the encrypted blob.
* This is particularly useful for our OOM subsystem that tracks the HSDir
* cache size for instance. */
size_t
hs_desc_plaintext_obj_size(const hs_desc_plaintext_data_t *data)
{
tor_assert(data);
return (sizeof(*data) + sizeof(*data->signing_key_cert) +
data->encrypted_blob_size);
}

View File

@ -207,6 +207,8 @@ int hs_desc_decode_plaintext(const char *encoded,
int hs_desc_decode_encrypted(const hs_descriptor_t *desc,
hs_desc_encrypted_data_t *desc_out);
size_t hs_desc_plaintext_obj_size(const hs_desc_plaintext_data_t *data);
#ifdef HS_DESCRIPTOR_PRIVATE
/* Encoding. */

View File

@ -48,6 +48,7 @@ LIBTOR_A_SOURCES = \
src/or/entrynodes.c \
src/or/ext_orport.c \
src/or/hibernate.c \
src/or/hs_cache.c \
src/or/hs_common.c \
src/or/hs_descriptor.c \
src/or/keypin.c \
@ -159,6 +160,7 @@ ORHEADERS = \
src/or/geoip.h \
src/or/entrynodes.h \
src/or/hibernate.h \
src/or/hs_cache.h \
src/or/hs_common.h \
src/or/hs_descriptor.h \
src/or/keypin.h \

View File

@ -37,6 +37,7 @@
#include "entrynodes.h"
#include "geoip.h"
#include "hibernate.h"
#include "hs_cache.h"
#include "keypin.h"
#include "main.h"
#include "microdesc.h"
@ -1651,7 +1652,7 @@ clean_caches_callback(time_t now, const or_options_t *options)
rep_history_clean(now - options->RephistTrackTime);
rend_cache_clean(now, REND_CACHE_TYPE_CLIENT);
rend_cache_clean(now, REND_CACHE_TYPE_SERVICE);
rend_cache_clean_v2_descs_as_dir(now, 0);
hs_cache_clean_as_dir(now);
microdesc_cache_rebuild(NULL, 0);
#define CLEAN_CACHES_INTERVAL (30*60)
return CLEAN_CACHES_INTERVAL;

View File

@ -25,6 +25,7 @@
#include "connection_or.h"
#include "control.h"
#include "geoip.h"
#include "hs_cache.h"
#include "main.h"
#include "networkstatus.h"
#include "nodelist.h"
@ -2404,9 +2405,7 @@ cell_queues_check_size(void)
if (rend_cache_total > get_options()->MaxMemInQueues / 5) {
const size_t bytes_to_remove =
rend_cache_total - (size_t)(get_options()->MaxMemInQueues / 10);
rend_cache_clean_v2_descs_as_dir(time(NULL), bytes_to_remove);
alloc -= rend_cache_total;
alloc += rend_cache_get_total_allocation();
alloc -= hs_cache_handle_oom(time(NULL), bytes_to_remove);
}
circuits_handle_oom(alloc);
return 1;

View File

@ -86,7 +86,7 @@ rend_cache_get_total_allocation(void)
}
/** Decrement the total bytes attributed to the rendezvous cache by n. */
STATIC void
void
rend_cache_decrement_allocation(size_t n)
{
static int have_underflowed = 0;
@ -103,7 +103,7 @@ rend_cache_decrement_allocation(size_t n)
}
/** Increase the total bytes attributed to the rendezvous cache by n. */
STATIC void
void
rend_cache_increment_allocation(size_t n)
{
static int have_overflowed = 0;
@ -462,45 +462,36 @@ rend_cache_intro_failure_note(rend_intro_point_failure_t failure,
}
/** Remove all old v2 descriptors and those for which this hidden service
* directory is not responsible for any more.
*
* If at all possible, remove at least <b>force_remove</b> bytes of data.
*/
void
rend_cache_clean_v2_descs_as_dir(time_t now, size_t force_remove)
* directory is not responsible for any more. The cutoff is the time limit for
* which we want to keep the cache entry. In other words, any entry created
* before will be removed. */
size_t
rend_cache_clean_v2_descs_as_dir(time_t cutoff)
{
digestmap_iter_t *iter;
time_t cutoff = now - REND_CACHE_MAX_AGE - REND_CACHE_MAX_SKEW;
const int LAST_SERVED_CUTOFF_STEP = 1800;
time_t last_served_cutoff = cutoff;
size_t bytes_removed = 0;
do {
for (iter = digestmap_iter_init(rend_cache_v2_dir);
!digestmap_iter_done(iter); ) {
const char *key;
void *val;
rend_cache_entry_t *ent;
digestmap_iter_get(iter, &key, &val);
ent = val;
if (ent->parsed->timestamp < cutoff ||
ent->last_served < last_served_cutoff) {
char key_base32[REND_DESC_ID_V2_LEN_BASE32 + 1];
base32_encode(key_base32, sizeof(key_base32), key, DIGEST_LEN);
log_info(LD_REND, "Removing descriptor with ID '%s' from cache",
safe_str_client(key_base32));
bytes_removed += rend_cache_entry_allocation(ent);
iter = digestmap_iter_next_rmv(rend_cache_v2_dir, iter);
rend_cache_entry_free(ent);
} else {
iter = digestmap_iter_next(rend_cache_v2_dir, iter);
}
}
/* In case we didn't remove enough bytes, advance the cutoff a little. */
last_served_cutoff += LAST_SERVED_CUTOFF_STEP;
if (last_served_cutoff > now)
break;
} while (bytes_removed < force_remove);
for (iter = digestmap_iter_init(rend_cache_v2_dir);
!digestmap_iter_done(iter); ) {
const char *key;
void *val;
rend_cache_entry_t *ent;
digestmap_iter_get(iter, &key, &val);
ent = val;
if (ent->parsed->timestamp < cutoff) {
char key_base32[REND_DESC_ID_V2_LEN_BASE32 + 1];
base32_encode(key_base32, sizeof(key_base32), key, DIGEST_LEN);
log_info(LD_REND, "Removing descriptor with ID '%s' from cache",
safe_str_client(key_base32));
bytes_removed += rend_cache_entry_allocation(ent);
iter = digestmap_iter_next_rmv(rend_cache_v2_dir, iter);
rend_cache_entry_free(ent);
} else {
iter = digestmap_iter_next(rend_cache_v2_dir, iter);
}
}
return bytes_removed;
}
/** Lookup in the client cache the given service ID <b>query</b> for

View File

@ -53,10 +53,17 @@ typedef enum {
REND_CACHE_TYPE_SERVICE = 2,
} rend_cache_type_t;
/* Return maximum lifetime in seconds of a cache entry. */
static inline time_t
rend_cache_max_entry_lifetime(void)
{
return REND_CACHE_MAX_AGE + REND_CACHE_MAX_SKEW;
}
void rend_cache_init(void);
void rend_cache_clean(time_t now, rend_cache_type_t cache_type);
void rend_cache_failure_clean(time_t now);
void rend_cache_clean_v2_descs_as_dir(time_t now, size_t min_to_remove);
size_t rend_cache_clean_v2_descs_as_dir(time_t cutoff);
void rend_cache_purge(void);
void rend_cache_free_all(void);
int rend_cache_lookup_entry(const char *query, int version,
@ -77,6 +84,8 @@ void rend_cache_intro_failure_note(rend_intro_point_failure_t failure,
const uint8_t *identity,
const char *service_id);
void rend_cache_failure_purge(void);
void rend_cache_decrement_allocation(size_t n);
void rend_cache_increment_allocation(size_t n);
#ifdef RENDCACHE_PRIVATE
@ -89,8 +98,6 @@ STATIC int cache_failure_intro_lookup(const uint8_t *identity,
const char *service_id,
rend_cache_failure_intro_t
**intro_entry);
STATIC void rend_cache_decrement_allocation(size_t n);
STATIC void rend_cache_increment_allocation(size_t n);
STATIC rend_cache_failure_intro_t *rend_cache_failure_intro_entry_new(
rend_intro_point_failure_t failure);
STATIC rend_cache_failure_t *rend_cache_failure_entry_new(void);

View File

@ -1072,9 +1072,10 @@ static void
test_rend_cache_clean_v2_descs_as_dir(void *data)
{
rend_cache_entry_t *e;
time_t now;
time_t now, cutoff;
rend_service_descriptor_t *desc;
now = time(NULL);
cutoff = now - (REND_CACHE_MAX_AGE + REND_CACHE_MAX_SKEW);
const char key[DIGEST_LEN] = "abcde";
(void)data;
@ -1082,7 +1083,7 @@ test_rend_cache_clean_v2_descs_as_dir(void *data)
rend_cache_init();
// Test running with an empty cache
rend_cache_clean_v2_descs_as_dir(now, 0);
rend_cache_clean_v2_descs_as_dir(cutoff);
tt_int_op(digestmap_size(rend_cache_v2_dir), OP_EQ, 0);
// Test with only one new entry
@ -1094,38 +1095,15 @@ test_rend_cache_clean_v2_descs_as_dir(void *data)
e->parsed = desc;
digestmap_set(rend_cache_v2_dir, key, e);
rend_cache_clean_v2_descs_as_dir(now, 0);
/* Set the cutoff to minus 10 seconds. */
rend_cache_clean_v2_descs_as_dir(cutoff - 10);
tt_int_op(digestmap_size(rend_cache_v2_dir), OP_EQ, 1);
// Test with one old entry
desc->timestamp = now - (REND_CACHE_MAX_AGE + REND_CACHE_MAX_SKEW + 1000);
rend_cache_clean_v2_descs_as_dir(now, 0);
desc->timestamp = cutoff - 1000;
rend_cache_clean_v2_descs_as_dir(cutoff);
tt_int_op(digestmap_size(rend_cache_v2_dir), OP_EQ, 0);
// Test with one entry that has an old last served
e = tor_malloc_zero(sizeof(rend_cache_entry_t));
e->last_served = now - (REND_CACHE_MAX_AGE + REND_CACHE_MAX_SKEW + 1000);
desc = tor_malloc_zero(sizeof(rend_service_descriptor_t));
desc->timestamp = now;
desc->pk = pk_generate(0);
e->parsed = desc;
digestmap_set(rend_cache_v2_dir, key, e);
rend_cache_clean_v2_descs_as_dir(now, 0);
tt_int_op(digestmap_size(rend_cache_v2_dir), OP_EQ, 0);
// Test a run through asking for a large force_remove
e = tor_malloc_zero(sizeof(rend_cache_entry_t));
e->last_served = now;
desc = tor_malloc_zero(sizeof(rend_service_descriptor_t));
desc->timestamp = now;
desc->pk = pk_generate(0);
e->parsed = desc;
digestmap_set(rend_cache_v2_dir, key, e);
rend_cache_clean_v2_descs_as_dir(now, 20000);
tt_int_op(digestmap_size(rend_cache_v2_dir), OP_EQ, 1);
done:
rend_cache_free_all();
}