Skip to content

Commit

Permalink
Add check for the MADV_FREE/fork arm64 Linux kernel bug (#8224)
Browse files Browse the repository at this point in the history
Older arm64 Linux kernels have a bug that could lead to data corruption during
background save under the following scenario:

1) jemalloc uses MADV_FREE on a page,
2) jemalloc reuses and writes the page,
3) Redis forks the background save process, and
4) Linux performs page reclamation.

Under these conditions, Linux will reclaim the page wrongfully and the
background save process will read zeros when it tries to read the page.

The bug has been fixed in Linux with commit:
ff1712f953e27f0b0718762ec17d0adb15c9fd0b ("arm64: pgtable: Ensure dirty bit is
preserved across pte_wrprotect()")

This Commit adds an ignore-warnings config, when not found, redis will
print a warning and exit on startup (default behavior).

Co-authored-by: Oran Agra <oran@redislabs.com>
  • Loading branch information
prekageo and oranagra committed Jan 7, 2021
1 parent ea930a3 commit b02780c
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 1 deletion.
7 changes: 7 additions & 0 deletions redis.conf
Expand Up @@ -1976,3 +1976,10 @@ jemalloc-bg-thread yes
#
# Set bgsave child process to cpu affinity 1,10,11
# bgsave_cpulist 1,10-11

# In some cases redis will emit warnings and even refuse to start if it detects
# that the system is in bad state, it is possible to suppress these warnings
# by setting the following config which takes a space delimited list of warnings
# to suppress
#
# ignore-warnings ARM64-COW-BUG
1 change: 1 addition & 0 deletions src/config.c
Expand Up @@ -2424,6 +2424,7 @@ standardConfig configs[] = {
createStringConfig("bio_cpulist", NULL, IMMUTABLE_CONFIG, EMPTY_STRING_IS_NULL, server.bio_cpulist, NULL, NULL, NULL),
createStringConfig("aof_rewrite_cpulist", NULL, IMMUTABLE_CONFIG, EMPTY_STRING_IS_NULL, server.aof_rewrite_cpulist, NULL, NULL, NULL),
createStringConfig("bgsave_cpulist", NULL, IMMUTABLE_CONFIG, EMPTY_STRING_IS_NULL, server.bgsave_cpulist, NULL, NULL, NULL),
createStringConfig("ignore-warnings", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.ignore_warnings, "", NULL, NULL),

/* SDS Configs */
createSDSConfig("masterauth", NULL, MODIFIABLE_CONFIG, EMPTY_STRING_IS_NULL, server.masterauth, NULL, NULL, NULL),
Expand Down
137 changes: 136 additions & 1 deletion src/server.c
Expand Up @@ -59,6 +59,10 @@
#include <sys/socket.h>
#include <sys/resource.h>

#ifdef __linux__
#include <sys/mman.h>
#endif

/* Our shared "common" objects */

struct sharedObjectsStruct shared;
Expand Down Expand Up @@ -5090,6 +5094,21 @@ void monitorCommand(client *c) {

/* =================================== Main! ================================ */

int checkIgnoreWarning(const char *warning) {
int argc, j;
sds *argv = sdssplitargs(server.ignore_warnings, &argc);
if (argv == NULL)
return 0;

for (j = 0; j < argc; j++) {
char *flag = argv[j];
if (!strcasecmp(flag, warning))
break;
}
sdsfreesplitres(argv,argc);
return j < argc;
}

#ifdef __linux__
int linuxOvercommitMemoryValue(void) {
FILE *fp = fopen("/proc/sys/vm/overcommit_memory","r");
Expand All @@ -5113,6 +5132,113 @@ void linuxMemoryWarnings(void) {
serverLog(LL_WARNING,"WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').");
}
}

#ifdef __arm64__

/* Get size in kilobytes of the Shared_Dirty pages of the calling process for the
* memory map corresponding to the provided address, or -1 on error. */
static int smapsGetSharedDirty(unsigned long addr) {
int ret, in_mapping = 0, val = -1;
unsigned long from, to;
char buf[64];
FILE *f;

f = fopen("/proc/self/smaps", "r");
serverAssert(f);

while (1) {
if (!fgets(buf, sizeof(buf), f))
break;

ret = sscanf(buf, "%lx-%lx", &from, &to);
if (ret == 2)
in_mapping = from <= addr && addr < to;

if (in_mapping && !memcmp(buf, "Shared_Dirty:", 13)) {
ret = sscanf(buf, "%*s %d", &val);
serverAssert(ret == 1);
break;
}
}

fclose(f);
return val;
}

/* Older arm64 Linux kernels have a bug that could lead to data corruption
* during background save in certain scenarios. This function checks if the
* kernel is affected.
* The bug was fixed in commit ff1712f953e27f0b0718762ec17d0adb15c9fd0b
* titled: "arm64: pgtable: Ensure dirty bit is preserved across pte_wrprotect()"
* Return 1 if the kernel seems to be affected, and 0 otherwise. */
int linuxMadvFreeForkBugCheck(void) {
int ret, pipefd[2];
pid_t pid;
char *p, *q, bug_found = 0;
const long map_size = 3 * 4096;

/* Create a memory map that's in our full control (not one used by the allocator). */
p = mmap(NULL, map_size, PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
serverAssert(p != MAP_FAILED);

q = p + 4096;

/* Split the memory map in 3 pages by setting their protection as RO|RW|RO to prevent
* Linux from merging this memory map with adjacent VMAs. */
ret = mprotect(q, 4096, PROT_READ | PROT_WRITE);
serverAssert(!ret);

/* Write to the page once to make it resident */
*(volatile char*)q = 0;

/* Tell the kernel that this page is free to be reclaimed. */
#ifndef MADV_FREE
#define MADV_FREE 8
#endif
ret = madvise(q, 4096, MADV_FREE);
serverAssert(!ret);

/* Write to the page after being marked for freeing, this is supposed to take
* ownership of that page again. */
*(volatile char*)q = 0;

/* Create a pipe for the child to return the info to the parent. */
ret = pipe(pipefd);
serverAssert(!ret);

/* Fork the process. */
pid = fork();
serverAssert(pid >= 0);
if (!pid) {
/* Child: check if the page is marked as dirty, expecing 4 (kB).
* A value of 0 means the kernel is affected by the bug. */
if (!smapsGetSharedDirty((unsigned long)q))
bug_found = 1;

ret = write(pipefd[1], &bug_found, 1);
serverAssert(ret == 1);

exit(0);
} else {
/* Read the result from the child. */
ret = read(pipefd[0], &bug_found, 1);
serverAssert(ret == 1);

/* Reap the child pid. */
serverAssert(waitpid(pid, NULL, 0) == pid);
}

/* Cleanup */
ret = close(pipefd[0]);
serverAssert(!ret);
ret = close(pipefd[1]);
serverAssert(!ret);
ret = munmap(p, map_size);
serverAssert(!ret);

return bug_found;
}
#endif /* __arm64__ */
#endif /* __linux__ */

void createPidFile(void) {
Expand Down Expand Up @@ -5711,7 +5837,16 @@ int main(int argc, char **argv) {
serverLog(LL_WARNING,"Server initialized");
#ifdef __linux__
linuxMemoryWarnings();
#endif
#if defined (__arm64__)
if (linuxMadvFreeForkBugCheck()) {
serverLog(LL_WARNING,"WARNING Your kernel has a bug that could lead to data corruption during background save. Please upgrade to the latest stable kernel.");
if (!checkIgnoreWarning("ARM64-COW-BUG")) {
serverLog(LL_WARNING,"Redis will now exit to prevent data corruption. Note that it is possible to suppress this warning by setting the following config: ignore-warnings ARM64-COW-BUG");
exit(1);
}
}
#endif /* __arm64__ */
#endif /* __linux__ */
moduleInitModulesSystemLast();
moduleLoadFromQueue();
ACLLoadUsersAtStartup();
Expand Down
1 change: 1 addition & 0 deletions src/server.h
Expand Up @@ -1136,6 +1136,7 @@ struct redisServer {
int in_eval; /* Are we inside EVAL? */
int in_exec; /* Are we inside EXEC? */
int propagate_in_transaction; /* Make sure we don't propagate nested MULTI/EXEC */
char *ignore_warnings; /* Config: warnings that should be ignored. */
/* Modules */
dict *moduleapi; /* Exported core APIs dictionary for modules. */
dict *sharedapi; /* Like moduleapi but containing the APIs that
Expand Down

0 comments on commit b02780c

Please sign in to comment.