Unverified Commit b91d8b28 authored by Ozan Tezcan's avatar Ozan Tezcan Committed by GitHub
Browse files

Add sanitizer support and clean up sanitizer findings (#9601)

- Added sanitizer support. `address`, `undefined` and `thread` sanitizers are available.  
- To build Redis with desired sanitizer : `make SANITIZER=undefined`
- There were some sanitizer findings, cleaned up codebase
- Added tests with address and undefined behavior sanitizers to daily CI.
- Added tests with address sanitizer to the per-PR CI (smoke out mem leaks sooner).

Basically, there are three types of issues : 

**1- Unaligned load/store** : Most probably, this issue may cause a crash on a platform that
does not support unaligned access. Redis does unaligned access only on supported platforms.

**2- Signed integer overflow.** Although, signed overflow issue can be problematic time to time
and change how compiler generates code, current findings mostly about signed shift or simple
addition overflow. For most platforms Redis can be compiled for, this wouldn't cause any issue
as far as I can tell (checked generated code on godbolt.org).

 **3 -Minor leak** (redis-cli), **use-after-free**(just before calling exit());

UB means nothing guaranteed and risky to reason about program behavior but I don't think any
of the fixes here worth backporting. As sanitizers are now part of the CI, preventing new issues
will be the real benefit. 
parent cd6b3d55
...@@ -37,11 +37,12 @@ ...@@ -37,11 +37,12 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <signal.h>
void _serverAssert(const char *estr, const char *file, int line) { void _serverAssert(const char *estr, const char *file, int line) {
fprintf(stderr, "=== ASSERTION FAILED ==="); fprintf(stderr, "=== ASSERTION FAILED ===");
fprintf(stderr, "==> %s:%d '%s' is not true",file,line,estr); fprintf(stderr, "==> %s:%d '%s' is not true",file,line,estr);
*((char*)-1) = 'x'; raise(SIGSEGV);
} }
void _serverPanic(const char *file, int line, const char *msg, ...) { void _serverPanic(const char *file, int line, const char *msg, ...) {
......
...@@ -4916,7 +4916,7 @@ void sentinelFailoverWaitStart(sentinelRedisInstance *ri) { ...@@ -4916,7 +4916,7 @@ void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
/* If I'm not the leader, and it is not a forced failover via /* If I'm not the leader, and it is not a forced failover via
* SENTINEL FAILOVER, then I can't continue with the failover. */ * SENTINEL FAILOVER, then I can't continue with the failover. */
if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) { if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
int election_timeout = sentinel_election_timeout; mstime_t election_timeout = sentinel_election_timeout;
/* The election timeout is the MIN between SENTINEL_ELECTION_TIMEOUT /* The election timeout is the MIN between SENTINEL_ELECTION_TIMEOUT
* and the configured failover timeout. */ * and the configured failover timeout. */
......
...@@ -45,8 +45,13 @@ void sha256_transform(SHA256_CTX *ctx, const BYTE data[]) ...@@ -45,8 +45,13 @@ void sha256_transform(SHA256_CTX *ctx, const BYTE data[])
{ {
WORD a, b, c, d, e, f, g, h, i, j, t1, t2, m[64]; WORD a, b, c, d, e, f, g, h, i, j, t1, t2, m[64];
for (i = 0, j = 0; i < 16; ++i, j += 4) for (i = 0, j = 0; i < 16; ++i, j += 4) {
m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]); m[i] = ((WORD) data[j + 0] << 24) |
((WORD) data[j + 1] << 16) |
((WORD) data[j + 2] << 8) |
((WORD) data[j + 3]);
}
for ( ; i < 64; ++i) for ( ; i < 64; ++i)
m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16]; m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16];
......
...@@ -55,6 +55,16 @@ int siptlw(int c) { ...@@ -55,6 +55,16 @@ int siptlw(int c) {
} }
} }
#if defined(__has_attribute)
#if __has_attribute(no_sanitize)
#define NO_SANITIZE(sanitizer) __attribute__((no_sanitize(sanitizer)))
#endif
#endif
#if !defined(NO_SANITIZE)
#define NO_SANITIZE(sanitizer)
#endif
/* Test of the CPU is Little Endian and supports not aligned accesses. /* Test of the CPU is Little Endian and supports not aligned accesses.
* Two interesting conditions to speedup the function that happen to be * Two interesting conditions to speedup the function that happen to be
* in most of x86 servers. */ * in most of x86 servers. */
...@@ -113,6 +123,7 @@ int siptlw(int c) { ...@@ -113,6 +123,7 @@ int siptlw(int c) {
v2 = ROTL(v2, 32); \ v2 = ROTL(v2, 32); \
} while (0) } while (0)
NO_SANITIZE("alignment")
uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k) { uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k) {
#ifndef UNALIGNED_LE_CPU #ifndef UNALIGNED_LE_CPU
uint64_t hash; uint64_t hash;
...@@ -172,6 +183,7 @@ uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k) { ...@@ -172,6 +183,7 @@ uint64_t siphash(const uint8_t *in, const size_t inlen, const uint8_t *k) {
#endif #endif
} }
NO_SANITIZE("alignment")
uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k) uint64_t siphash_nocase(const uint8_t *in, const size_t inlen, const uint8_t *k)
{ {
#ifndef UNALIGNED_LE_CPU #ifndef UNALIGNED_LE_CPU
......
...@@ -416,10 +416,10 @@ unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, uns ...@@ -416,10 +416,10 @@ unsigned int zipStoreEntryEncoding(unsigned char *p, unsigned char encoding, uns
(len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \ (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1]; \
} else if ((encoding) == ZIP_STR_32B) { \ } else if ((encoding) == ZIP_STR_32B) { \
(lensize) = 5; \ (lensize) = 5; \
(len) = ((ptr)[1] << 24) | \ (len) = ((uint32_t)(ptr)[1] << 24) | \
((ptr)[2] << 16) | \ ((uint32_t)(ptr)[2] << 16) | \
((ptr)[3] << 8) | \ ((uint32_t)(ptr)[3] << 8) | \
((ptr)[4]); \ ((uint32_t)(ptr)[4]); \
} else { \ } else { \
(lensize) = 0; /* bad encoding, should be covered by a previous */ \ (lensize) = 0; /* bad encoding, should be covered by a previous */ \
(len) = 0; /* ZIP_ASSERT_ENCODING / zipEncodingLenSize, or */ \ (len) = 0; /* ZIP_ASSERT_ENCODING / zipEncodingLenSize, or */ \
...@@ -557,7 +557,7 @@ void zipSaveInteger(unsigned char *p, int64_t value, unsigned char encoding) { ...@@ -557,7 +557,7 @@ void zipSaveInteger(unsigned char *p, int64_t value, unsigned char encoding) {
memcpy(p,&i16,sizeof(i16)); memcpy(p,&i16,sizeof(i16));
memrev16ifbe(p); memrev16ifbe(p);
} else if (encoding == ZIP_INT_24B) { } else if (encoding == ZIP_INT_24B) {
i32 = value<<8; i32 = ((uint64_t)value)<<8;
memrev32ifbe(&i32); memrev32ifbe(&i32);
memcpy(p,((uint8_t*)&i32)+1,sizeof(i32)-sizeof(uint8_t)); memcpy(p,((uint8_t*)&i32)+1,sizeof(i32)-sizeof(uint8_t));
} else if (encoding == ZIP_INT_32B) { } else if (encoding == ZIP_INT_32B) {
......
...@@ -181,6 +181,15 @@ proc log_crashes {} { ...@@ -181,6 +181,15 @@ proc log_crashes {} {
incr ::failed incr ::failed
} }
} }
set logs [glob */err.txt]
foreach log $logs {
set res [sanitizer_errors_from_file $log]
if {$res != ""} {
puts $res
incr ::failed
}
}
} }
proc is_alive pid { proc is_alive pid {
......
tags {"external:skip"} { tags {"external:skip"} {
set system_name [string tolower [exec uname -s]] set system_name [string tolower [exec uname -s]]
set system_supported 0 set backtrace_supported 0
# We only support darwin or Linux with glibc # We only support darwin or Linux with glibc
if {$system_name eq {darwin}} { if {$system_name eq {darwin}} {
set system_supported 1 set backtrace_supported 1
} elseif {$system_name eq {linux}} { } elseif {$system_name eq {linux}} {
# Avoid the test on libmusl, which does not support backtrace # Avoid the test on libmusl, which does not support backtrace
set ldd [exec ldd src/redis-server] set ldd [exec ldd src/redis-server]
if {![string match {*libc.musl*} $ldd]} { if {![string match {*libc.musl*} $ldd]} {
set system_supported 1 set backtrace_supported 1
} }
} }
if {$system_supported} { if {$backtrace_supported} {
set server_path [tmpdir server.log] set server_path [tmpdir server.log]
start_server [list overrides [list dir $server_path]] { start_server [list overrides [list dir $server_path]] {
test "Server is able to generate a stack trace on selected systems" { test "Server is able to generate a stack trace on selected systems" {
...@@ -35,21 +35,34 @@ if {$system_supported} { ...@@ -35,21 +35,34 @@ if {$system_supported} {
} }
} }
} }
}
# Valgrind will complain that the process terminated by a signal, skip it. # Valgrind will complain that the process terminated by a signal, skip it.
if {!$::valgrind} { if {!$::valgrind} {
set server_path [tmpdir server1.log] if {$backtrace_supported} {
start_server [list overrides [list dir $server_path]] { set crash_pattern "*STACK TRACE*"
test "Crash report generated on SIGABRT" { } else {
set pid [s process_id] set crash_pattern "*crashed by signal*"
exec kill -SIGABRT $pid }
set pattern "*STACK TRACE*"
set result [exec tail -1000 < [srv 0 stdout]] set server_path [tmpdir server1.log]
assert {[string match $pattern $result]} start_server [list overrides [list dir $server_path]] {
} test "Crash report generated on SIGABRT" {
set pid [s process_id]
exec kill -SIGABRT $pid
set result [exec tail -1000 < [srv 0 stdout]]
assert {[string match $crash_pattern $result]}
} }
} }
set server_path [tmpdir server2.log]
start_server [list overrides [list dir $server_path]] {
test "Crash report generated on DEBUG SEGFAULT" {
catch {r debug segfault}
set result [exec tail -1000 < [srv 0 stdout]]
assert {[string match $crash_pattern $result]}
}
}
} }
} }
...@@ -19,6 +19,13 @@ proc check_valgrind_errors stderr { ...@@ -19,6 +19,13 @@ proc check_valgrind_errors stderr {
} }
} }
proc check_sanitizer_errors stderr {
set res [sanitizer_errors_from_file $stderr]
if {$res != ""} {
send_data_packet $::test_server_fd err "Sanitizer error: $res\n"
}
}
proc clean_persistence config { proc clean_persistence config {
# we may wanna keep the logs for later, but let's clean the persistence # we may wanna keep the logs for later, but let's clean the persistence
# files right away, since they can accumulate and take up a lot of space # files right away, since they can accumulate and take up a lot of space
...@@ -44,6 +51,8 @@ proc kill_server config { ...@@ -44,6 +51,8 @@ proc kill_server config {
if {$::valgrind} { if {$::valgrind} {
check_valgrind_errors [dict get $config stderr] check_valgrind_errors [dict get $config stderr]
} }
check_sanitizer_errors [dict get $config stderr]
return return
} }
set pid [dict get $config pid] set pid [dict get $config pid]
...@@ -104,6 +113,8 @@ proc kill_server config { ...@@ -104,6 +113,8 @@ proc kill_server config {
check_valgrind_errors [dict get $config stderr] check_valgrind_errors [dict get $config stderr]
} }
check_sanitizer_errors [dict get $config stderr]
# Remove this pid from the set of active pids in the test server. # Remove this pid from the set of active pids in the test server.
send_data_packet $::test_server_fd server-killed $pid send_data_packet $::test_server_fd server-killed $pid
} }
...@@ -251,7 +262,10 @@ proc spawn_server {config_file stdout stderr} { ...@@ -251,7 +262,10 @@ proc spawn_server {config_file stdout stderr} {
} elseif ($::stack_logging) { } elseif ($::stack_logging) {
set pid [exec /usr/bin/env MallocStackLogging=1 MallocLogFile=/tmp/malloc_log.txt src/redis-server $config_file >> $stdout 2>> $stderr &] set pid [exec /usr/bin/env MallocStackLogging=1 MallocLogFile=/tmp/malloc_log.txt src/redis-server $config_file >> $stdout 2>> $stderr &]
} else { } else {
set pid [exec src/redis-server $config_file >> $stdout 2>> $stderr &] # ASAN_OPTIONS environment variable is for address sanitizer. If a test
# tries to allocate huge memory area and expects allocator to return
# NULL, address sanitizer throws an error without this setting.
set pid [exec /usr/bin/env ASAN_OPTIONS=allocator_may_return_null=1 src/redis-server $config_file >> $stdout 2>> $stderr &]
} }
if {$::wait_server} { if {$::wait_server} {
...@@ -299,6 +313,10 @@ proc dump_server_log {srv} { ...@@ -299,6 +313,10 @@ proc dump_server_log {srv} {
puts "\n===== Start of server log (pid $pid) =====\n" puts "\n===== Start of server log (pid $pid) =====\n"
puts [exec cat [dict get $srv "stdout"]] puts [exec cat [dict get $srv "stdout"]]
puts "===== End of server log (pid $pid) =====\n" puts "===== End of server log (pid $pid) =====\n"
puts "\n===== Start of server stderr log (pid $pid) =====\n"
puts [exec cat [dict get $srv "stderr"]]
puts "===== End of server stderr log (pid $pid) =====\n"
} }
proc run_external_server_test {code overrides} { proc run_external_server_test {code overrides} {
...@@ -599,6 +617,13 @@ proc start_server {options {code undefined}} { ...@@ -599,6 +617,13 @@ proc start_server {options {code undefined}} {
puts "$crashlog" puts "$crashlog"
puts "" puts ""
} }
set sanitizerlog [sanitizer_errors_from_file [dict get $srv "stderr"]]
if {[string length $sanitizerlog] > 0} {
puts [format "\nLogged sanitizer errors (pid %d):" [dict get $srv "pid"]]
puts "$sanitizerlog"
puts ""
}
} }
if {!$assertion && $::durable} { if {!$assertion && $::durable} {
......
...@@ -50,6 +50,27 @@ proc crashlog_from_file {filename} { ...@@ -50,6 +50,27 @@ proc crashlog_from_file {filename} {
join $result "\n" join $result "\n"
} }
# Return sanitizer log lines
proc sanitizer_errors_from_file {filename} {
set log [exec cat $filename]
set lines [split [exec cat $filename] "\n"]
foreach line $lines {
# Ignore huge allocation warnings
if ([string match {*WARNING: AddressSanitizer failed to allocate*} $line]) {
continue
}
# GCC UBSAN output does not contain 'Sanitizer' but 'runtime error'.
if {[string match {*runtime error*} $log] ||
[string match {*Sanitizer*} $log]} {
return $log
}
}
return ""
}
proc getInfoProperty {infostr property} { proc getInfoProperty {infostr property} {
if {[regexp "\r\n$property:(.*?)\r\n" $infostr _ value]} { if {[regexp "\r\n$property:(.*?)\r\n" $infostr _ value]} {
set _ $value set _ $value
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment