diff --git a/.github/workflows/asan.yml b/.github/workflows/asan.yml new file mode 100644 index 0000000..f688f24 --- /dev/null +++ b/.github/workflows/asan.yml @@ -0,0 +1,95 @@ +name: ASAN (AddressSanitizer & LeakSanitizer) + +# Memory error and leak detection using AddressSanitizer and LeakSanitizer +# This workflow builds memtier_benchmark with sanitizers enabled and runs +# the full test suite to detect memory leaks and address errors. + +on: [push, pull_request] + +jobs: + test-with-sanitizers: + runs-on: ubuntu-latest + name: Memory leak detection (ASAN/LSAN) + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get -qq update + sudo apt-get install -y \ + build-essential \ + autoconf \ + automake \ + pkg-config \ + libevent-dev \ + zlib1g-dev \ + libssl-dev + + - name: Build with sanitizers + run: | + autoreconf -ivf + ./configure --enable-sanitizers + make -j + + - name: Verify ASAN is enabled + run: | + ldd ./memtier_benchmark | grep asan + echo "✓ AddressSanitizer is linked" + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + architecture: x64 + + - name: Install Python test dependencies + run: pip install -r ./tests/test_requirements.txt + + - name: Install Redis + run: | + curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg + echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list + sudo apt-get -qq update + sudo apt-get install redis + sudo service redis-server stop + + - name: Increase connection limit + run: | + sudo sysctl -w net.ipv4.tcp_fin_timeout=10 + sudo sysctl -w net.ipv4.tcp_tw_reuse=1 + ulimit -n 40960 + + - name: Generate TLS test certificates + run: ./tests/gen-test-certs.sh + + - name: Test OSS TCP with ASAN + timeout-minutes: 10 + run: | + ASAN_OPTIONS=detect_leaks=1 ./tests/run_tests.sh + + - name: Test OSS TCP TLS with ASAN + timeout-minutes: 10 + run: | + ASAN_OPTIONS=detect_leaks=1 TLS=1 ./tests/run_tests.sh + + - name: Test OSS TCP TLS v1.2 with ASAN + timeout-minutes: 10 + run: | + ASAN_OPTIONS=detect_leaks=1 TLS_PROTOCOLS='TLSv1.2' TLS=1 ./tests/run_tests.sh + + - name: Test OSS TCP TLS v1.3 with ASAN + timeout-minutes: 10 + run: | + ASAN_OPTIONS=detect_leaks=1 TLS_PROTOCOLS='TLSv1.3' TLS=1 ./tests/run_tests.sh + + - name: Test OSS-CLUSTER TCP with ASAN + timeout-minutes: 10 + run: | + ASAN_OPTIONS=detect_leaks=1 OSS_STANDALONE=0 OSS_CLUSTER=1 ./tests/run_tests.sh + + - name: Test OSS-CLUSTER TCP TLS with ASAN + timeout-minutes: 10 + run: | + ASAN_OPTIONS=detect_leaks=1 OSS_STANDALONE=0 OSS_CLUSTER=1 TLS=1 ./tests/run_tests.sh + diff --git a/README.md b/README.md index 2bd80f1..7cd83d5 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,27 @@ To understand what test options are available simply run: $ ./tests/run_tests.sh --help + +**Memory leak detection with sanitizers** + + +memtier_benchmark supports building with AddressSanitizer (ASAN) and LeakSanitizer (LSAN) to detect memory errors and leaks during testing. + +To build with sanitizers enabled: + + $ ./configure --enable-sanitizers + $ make + +To run tests with leak detection: + + $ ASAN_OPTIONS=detect_leaks=1 ./tests/run_tests.sh + +If memory leaks or errors are detected, tests will fail with detailed error messages showing the location of the issue. + +To verify ASAN is enabled: + + $ ldd ./memtier_benchmark | grep asan + ## Using Docker Use available images on Docker Hub: diff --git a/configure.ac b/configure.ac index 68c63e7..b75efcd 100755 --- a/configure.ac +++ b/configure.ac @@ -69,6 +69,16 @@ AS_IF([test "x$enable_tls" != "xno"], [ AC_SUBST(LIBCRYPTO_CFLAGS) AC_SUBST(LIBCRYPTO_LIBS)) ], []) +# Sanitizers support (ASAN/LSAN) is optional. +AC_ARG_ENABLE([sanitizers], + [AS_HELP_STRING([--enable-sanitizers], + [Enable AddressSanitizer and LeakSanitizer for memory error detection])]) +AS_IF([test "x$enable_sanitizers" = "xyes"], [ + AC_MSG_NOTICE([Enabling AddressSanitizer and LeakSanitizer]) + CXXFLAGS="$CXXFLAGS -fsanitize=address -fsanitize=leak -fno-omit-frame-pointer -O1" + LDFLAGS="$LDFLAGS -fsanitize=address -fsanitize=leak" + ], []) + # clock_gettime requires -lrt on old glibc only. AC_SEARCH_LIBS([clock_gettime], [rt], , AC_MSG_ERROR([rt is required libevent.])) diff --git a/memtier_benchmark.cpp b/memtier_benchmark.cpp index 0982354..761aee2 100755 --- a/memtier_benchmark.cpp +++ b/memtier_benchmark.cpp @@ -1956,6 +1956,16 @@ int main(int argc, char *argv[]) delete cfg.arbitrary_commands; } + // Clean up dynamically allocated strings from URI parsing + if (cfg.uri) { + if (cfg.server) { + free((void*)cfg.server); + } + if (cfg.authenticate) { + free((void*)cfg.authenticate); + } + } + #ifdef USE_TLS if(cfg.tls) { if (cfg.openssl_ctx) { diff --git a/tests/mb.py b/tests/mb.py new file mode 100644 index 0000000..fdbfb05 --- /dev/null +++ b/tests/mb.py @@ -0,0 +1,88 @@ +""" +Simple replacement for mbdirector package. +Contains only the Benchmark and RunConfig classes needed for tests. +""" +import os +import subprocess +import logging + + +class RunConfig(object): + """Configuration for a benchmark run.""" + next_id = 1 + + def __init__(self, base_results_dir, name, config, benchmark_config): + self.id = RunConfig.next_id + RunConfig.next_id += 1 + + self.redis_process_port = config.get('redis_process_port', 6379) + + mbconfig = config.get('memtier_benchmark', {}) + mbconfig.update(benchmark_config) + self.mb_binary = mbconfig.get('binary', 'memtier_benchmark') + self.mb_threads = mbconfig.get('threads') + self.mb_clients = mbconfig.get('clients') + self.mb_pipeline = mbconfig.get('pipeline') + self.mb_requests = mbconfig.get('requests') + self.mb_test_time = mbconfig.get('test_time') + self.explicit_connect_args = bool( + mbconfig.get('explicit_connect_args')) + + self.results_dir = os.path.join(base_results_dir, + '{:04}_{}'.format(self.id, name)) + + def __repr__(self): + return ''.format(self.id) + + +class Benchmark(object): + """Benchmark runner for memtier_benchmark.""" + + def __init__(self, config, **kwargs): + self.config = config + self.binary = self.config.mb_binary + self.name = kwargs['name'] + + # Configure + self.args = [self.binary] + if not self.config.explicit_connect_args: + self.args += ['--server', '127.0.0.1', + '--port', str(self.config.redis_process_port) + ] + self.args += ['--out-file', os.path.join(config.results_dir, + 'mb.stdout'), + '--json-out-file', os.path.join(config.results_dir, + 'mb.json')] + + if self.config.mb_threads is not None: + self.args += ['--threads', str(self.config.mb_threads)] + if self.config.mb_clients is not None: + self.args += ['--clients', str(self.config.mb_clients)] + if self.config.mb_pipeline is not None: + self.args += ['--pipeline', str(self.config.mb_pipeline)] + if self.config.mb_requests is not None: + self.args += ['--requests', str(self.config.mb_requests)] + if self.config.mb_test_time is not None: + self.args += ['--test-time', str(self.config.mb_test_time)] + + self.args += kwargs['args'] + + @classmethod + def from_json(cls, config, json): + return cls(config, **json) + + def write_file(self, name, data): + with open(os.path.join(self.config.results_dir, name), 'wb') as outfile: + outfile.write(data) + + def run(self): + logging.debug(' Command: %s', ' '.join(self.args)) + process = subprocess.Popen( + stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + executable=self.binary, args=self.args) + _stdout, _stderr = process.communicate() + if _stderr: + logging.debug(' >>> stderr <<<\n%s\n', _stderr) + self.write_file('mb.stderr', _stderr) + return process.wait() == 0 + diff --git a/tests/test_requirements.txt b/tests/test_requirements.txt index 8ca9e1a..5a9469b 100644 --- a/tests/test_requirements.txt +++ b/tests/test_requirements.txt @@ -1,3 +1,2 @@ redis>=3.0.0 -rltest==0.6.0 -git+https://github.com/RedisLabs/mbdirector.git@master +rltest>=0.7.17 diff --git a/tests/tests_oss_simple_flow.py b/tests/tests_oss_simple_flow.py index 2c059f7..c62ffec 100644 --- a/tests/tests_oss_simple_flow.py +++ b/tests/tests_oss_simple_flow.py @@ -1,8 +1,7 @@ import tempfile import json from include import * -from mbdirector.benchmark import Benchmark -from mbdirector.runner import RunConfig +from mb import Benchmark, RunConfig def test_preload_and_set_get(env): diff --git a/tests/tests_oss_zipfian_distribution.py b/tests/tests_oss_zipfian_distribution.py index 9ac603c..32816da 100644 --- a/tests/tests_oss_zipfian_distribution.py +++ b/tests/tests_oss_zipfian_distribution.py @@ -13,8 +13,7 @@ agg_info_commandstats, assert_minimum_memtier_outcomes ) -from mbdirector.benchmark import Benchmark -from mbdirector.runner import RunConfig +from mb import Benchmark, RunConfig def correlation_coeficient(x: list[float], y: list[float]) -> float: diff --git a/tests/zipfian_benchmark_runner.py b/tests/zipfian_benchmark_runner.py index e0ae0b7..f039871 100644 --- a/tests/zipfian_benchmark_runner.py +++ b/tests/zipfian_benchmark_runner.py @@ -12,8 +12,7 @@ assert_minimum_memtier_outcomes, get_expected_request_count, ) -from mbdirector.benchmark import Benchmark -from mbdirector.runner import RunConfig +from mb import Benchmark, RunConfig class MonitorThread(threading.Thread):