Skip to content

Commit 776644e

Browse files
author
netliomax25-code
committed
Add concurrency reproducer and parser synchronization hardening
1 parent 66dc042 commit 776644e

5 files changed

Lines changed: 159 additions & 0 deletions

File tree

.github/workflows/tsan.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: ThreadSanitizer repro & verify
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
tsan-compare:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Checkout
11+
uses: actions/checkout@v4
12+
13+
- name: Install dependencies
14+
run: |
15+
sudo apt-get update
16+
sudo apt-get install -y clang cmake build-essential ninja-build
17+
18+
- name: Create build dir
19+
run: mkdir -p build
20+
21+
- name: Build & run (mutex disabled) - expect races
22+
run: |
23+
cmake -S . -B build/mutex_disabled -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
24+
-DCMAKE_CXX_FLAGS='-fsanitize=thread -g -fno-omit-frame-pointer' \
25+
-DARGS_DISABLE_PARSE_MUTEX=1
26+
cmake --build build/mutex_disabled --parallel
27+
echo "Running concurrency reproducer with ARGS_DISABLE_PARSE_MUTEX=1 (should show races)"
28+
build/mutex_disabled/argstest-concurrency_reproducer || true
29+
env:
30+
ASAN_OPTIONS: 'halt_on_error=1'
31+
32+
- name: Build & run (mutex enabled) - expect no races
33+
run: |
34+
cmake -S . -B build/mutex_enabled -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
35+
-DCMAKE_CXX_FLAGS='-fsanitize=thread -g -fno-omit-frame-pointer' \
36+
-DARGS_DISABLE_PARSE_MUTEX=0
37+
cmake --build build/mutex_enabled --parallel
38+
echo "Running concurrency reproducer with mutex enabled (should be clean)"
39+
build/mutex_enabled/argstest-concurrency_reproducer || true
40+
env:
41+
ASAN_OPTIONS: 'halt_on_error=1'
42+
43+
- name: Upload logs
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: tsan-logs
47+
path: |
48+
build/mutex_disabled/tsan-log-*.txt
49+
build/mutex_enabled/tsan-log-*.txt

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ if(ARGS_ENABLE_SECURITY_HARDENING)
7171
# including the standard `argv + 1, argv + argc` idiom. It produces
7272
# false positives on bounded argv handling, so it is not enabled.
7373

74+
# -Wunsafe-buffer-usage (clang) flags all raw pointer arithmetic,
75+
# including the standard `argv + 1, argv + argc` idiom. It produces
76+
# false positives on bounded argv handling, so it is not enabled.
77+
7478
if(ARGS_ENABLE_HARDENING_WERROR)
7579
args_add_compile_flag_if_supported(-Werror)
7680
endif()

args.hxx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include <iterator>
4747
#include <exception>
4848
#include <functional>
49+
#include <mutex>
4950
#include <sstream>
5051
#include <string>
5152
#include <tuple>
@@ -2565,6 +2566,13 @@ namespace args
25652566

25662567
bool readCompletion = false;
25672568
CompletionFlag *completion = nullptr;
2569+
// Mutex to serialize Parse calls on this ArgumentParser instance.
2570+
// Recursive mutex is required because Parse may call Parse recursively
2571+
// (nested Parse for completion/subparsers).
2572+
// Can be disabled for reproducer builds by defining ARGS_DISABLE_PARSE_MUTEX
2573+
#ifndef ARGS_DISABLE_PARSE_MUTEX
2574+
mutable std::recursive_mutex parseMutex;
2575+
#endif
25682576

25692577
protected:
25702578
enum class OptionType
@@ -3478,6 +3486,11 @@ namespace args
34783486
template <typename It>
34793487
It ParseArgs(It begin, It end)
34803488
{
3489+
// Serialize concurrent Parse calls on the same parser instance (unless disabled)
3490+
#ifndef ARGS_DISABLE_PARSE_MUTEX
3491+
std::lock_guard<std::recursive_mutex> parseLock(parseMutex);
3492+
#endif
3493+
34813494
// Reset all Matched statuses and errors
34823495
Reset();
34833496
#ifdef ARGS_NOEXCEPT

test/concurrency_reproducer.cxx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#include "args.hxx"
2+
#include <thread>
3+
#include <vector>
4+
#include <iostream>
5+
6+
int main()
7+
{
8+
args::ArgumentParser parser("concurrency test");
9+
args::ValueFlag<std::string> f(parser, "name", "desc", {'f', "foo"}, "default");
10+
args::Positional<std::string> pos(parser, "pos", "positional");
11+
12+
const int threads = 8;
13+
const int iterations = 500;
14+
std::vector<std::thread> ths;
15+
std::atomic<int> failures{0};
16+
17+
for (int t = 0; t < threads; ++t)
18+
{
19+
ths.emplace_back([&parser, &failures, t, iterations]() {
20+
try
21+
{
22+
for (int i = 0; i < iterations; ++i)
23+
{
24+
std::vector<std::string> args = {"prog", "--foo=val", "posval"};
25+
// Alternate different arguments to exercise parsing paths
26+
if ((i + t) % 2 == 0)
27+
args[1] = "--foo=thread" + std::to_string(t);
28+
29+
parser.ParseArgs(args);
30+
}
31+
}
32+
catch (const std::exception &e)
33+
{
34+
++failures;
35+
std::cerr << "Thread " << t << " exception: " << e.what() << std::endl;
36+
}
37+
catch (...)
38+
{
39+
++failures;
40+
}
41+
});
42+
}
43+
44+
for (auto &th : ths) th.join();
45+
46+
if (failures.load() != 0)
47+
{
48+
std::cerr << "Failures: " << failures.load() << std::endl;
49+
return 1;
50+
}
51+
52+
std::cout << "Completed concurrent parsing without exception." << std::endl;
53+
return 0;
54+
}

tools/run_tsan.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Helper script to build and run the concurrency reproducer under ThreadSanitizer.
5+
# Produces logs: tsan_mutex_disabled.log and tsan_mutex_enabled.log
6+
7+
ROOT_DIR=$(cd "$(dirname "$0")/.." && pwd)
8+
BUILD_DIR_DISABLED="$ROOT_DIR/build.mutex_disabled"
9+
BUILD_DIR_ENABLED="$ROOT_DIR/build.mutex_enabled"
10+
11+
clang++ --version >/dev/null 2>&1 || { echo "clang++ not found in PATH"; exit 1; }
12+
13+
# Vulnerable build (mutex disabled)
14+
mkdir -p "$BUILD_DIR_DISABLED"
15+
cmake -S "$ROOT_DIR" -B "$BUILD_DIR_DISABLED" -G Ninja \
16+
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
17+
-DARGS_DISABLE_PARSE_MUTEX=1 \
18+
-DCMAKE_BUILD_TYPE=Debug \
19+
-DCMAKE_CXX_FLAGS='-fsanitize=thread -g -fno-omit-frame-pointer' \
20+
-DCMAKE_EXE_LINKER_FLAGS='-fsanitize=thread'
21+
cmake --build "$BUILD_DIR_DISABLED" --parallel
22+
23+
echo "Running reproducer with ARGS_DISABLE_PARSE_MUTEX=1 (expect races)"
24+
"$BUILD_DIR_DISABLED/argstest-concurrency_reproducer" 2>&1 | tee "$ROOT_DIR/tsan_mutex_disabled.log" || true
25+
26+
# Fixed build (mutex enabled)
27+
mkdir -p "$BUILD_DIR_ENABLED"
28+
cmake -S "$ROOT_DIR" -B "$BUILD_DIR_ENABLED" -G Ninja \
29+
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
30+
-DARGS_DISABLE_PARSE_MUTEX=0 \
31+
-DCMAKE_BUILD_TYPE=Debug \
32+
-DCMAKE_CXX_FLAGS='-fsanitize=thread -g -fno-omit-frame-pointer' \
33+
-DCMAKE_EXE_LINKER_FLAGS='-fsanitize=thread'
34+
cmake --build "$BUILD_DIR_ENABLED" --parallel
35+
36+
echo "Running reproducer with mutex enabled (expect clean)"
37+
"$BUILD_DIR_ENABLED/argstest-concurrency_reproducer" 2>&1 | tee "$ROOT_DIR/tsan_mutex_enabled.log" || true
38+
39+
echo "Logs saved to: $ROOT_DIR/tsan_mutex_disabled.log and $ROOT_DIR/tsan_mutex_enabled.log"

0 commit comments

Comments
 (0)