Skip to content

Commit 3f75fed

Browse files
etrclaude
andcommitted
TASK-087: Restore msan CI lane
Restore the MemorySanitizer lane that went silently absent when the Ubuntu 18.04 / Clang 6 matrix entry was retired and never replaced. - verify-build.yml: delete the stale commented msan block; add a clang-18 msan matrix include entry mirroring asan; build+cache an instrumented libc++ (MemoryWithOrigins) and an instrumented libmicrohttpd; exclude msan from the stock gnutls and MHD steps; rewrite the configure msan branch to use the instrumented libc++ with origins tracking and frame pointers; add an MSAN_OPTIONS + scoped-TESTS branch in Run tests. msan coverage is deliberately scoped to the backend-free unit subset because curl is uninstrumented. - scripts/check-msan-lane.sh + scripts/test_check_msan_lane.sh: local structural gate (valid YAML, msan include entry, stale block gone, MSAN_OPTIONS + scoped TESTS wiring) with its self-test, mirroring the check-warning-suppressions.sh idiom. test/msan.supp is intentionally absent; the real green run and any finding-triage happen on the PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015bNfjNMbo9J5WvrcA4ZQqY
1 parent 2086fd2 commit 3f75fed

3 files changed

Lines changed: 406 additions & 16 deletions

File tree

‎.github/workflows/verify-build.yml‎

Lines changed: 117 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,26 @@ jobs:
7272
debug: debug
7373
coverage: nocoverage
7474
shell: bash
75-
# This test gives false positives on newer versions of clang
76-
# and ubuntu-18.04 is not supported anymore on github
77-
#- test-group: extra
78-
# os: ubuntu-18.04
79-
# os-type: ubuntu
80-
# build-type: msan
81-
# compiler-family: clang
82-
# c-compiler: clang-6.0
83-
# cc-compiler: clang++-6.0
84-
# debug: debug
85-
# coverage: nocoverage
75+
# TASK-087: restored msan lane. The previous entry (retired
76+
# Ubuntu / Clang toolchain) was dropped when GitHub removed that
77+
# runner image and never replaced, so MemorySanitizer went
78+
# silently absent. This lane runs clang-18 against an
79+
# instrumented libc++ + instrumented
80+
# libmicrohttpd (both built and cached in-CI), with gnutls and
81+
# curl kept out of the picture. See the "instrumented libc++"
82+
# and "Run tests" steps below for how the false-positive-free
83+
# dependency set is assembled and why coverage is scoped to the
84+
# backend-free unit tests.
85+
- test-group: extra
86+
os: ubuntu-latest
87+
os-type: ubuntu
88+
build-type: msan
89+
compiler-family: clang
90+
c-compiler: clang-18
91+
cc-compiler: clang++-18
92+
debug: debug
93+
coverage: nocoverage
94+
shell: bash
8695
- test-group: extra
8796
os: ubuntu-latest
8897
os-type: ubuntu
@@ -586,7 +595,10 @@ jobs:
586595
# TASK-076: also skip on the tls-no-cli lane — it does its own
587596
# install of libgnutls28-dev WITHOUT gnutls-bin in the next step
588597
# so the per-test LT_SKIP_IF(!has_gnutls_cli(), ...) gate fires.
589-
if: ${{ matrix.os-type == 'ubuntu' && matrix.build-type != 'flag-invariance-off' && matrix.build-type != 'tls-no-cli' }}
598+
# TASK-087: also skip on the msan lane — installing gnutls would pull
599+
# an uninstrumented gnutls/nettle/gmp tree into LDADD; with no header
600+
# present configure auto-detects HAVE_GNUTLS=no and -lgnutls drops out.
601+
if: ${{ matrix.os-type == 'ubuntu' && matrix.build-type != 'flag-invariance-off' && matrix.build-type != 'tls-no-cli' && matrix.build-type != 'msan' }}
590602

591603
- name: Setup gnutls dependency WITHOUT gnutls-cli (TASK-076 tls-no-cli lane)
592604
run: |
@@ -601,6 +613,34 @@ jobs:
601613
fi
602614
if: ${{ matrix.build-type == 'tls-no-cli' }}
603615

616+
# TASK-087: MemorySanitizer reports use-of-uninitialized-value for any
617+
# bytes written by uninstrumented code and later read by instrumented
618+
# code. libc++ is on nearly every code path, so it must be instrumented
619+
# too. Build LLVM libc++/libc++abi with MemoryWithOrigins once and cache
620+
# the install prefix (keyed by os + compiler); rebuild only on a miss.
621+
- name: Fetch instrumented libc++ from cache (msan lane)
622+
id: cache-libcxx-msan
623+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
624+
with:
625+
path: ${{ github.workspace }}/libcxx-msan
626+
key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libcxx-msan-origins-v1
627+
if: ${{ matrix.build-type == 'msan' }}
628+
629+
- name: Build instrumented libc++ (msan lane, if not cached)
630+
run: |
631+
sudo apt-get install -y ninja-build cmake ;
632+
git clone --depth 1 --branch llvmorg-18.1.8 https://github.com/llvm/llvm-project.git llvm-project ;
633+
cmake -G Ninja -S llvm-project/runtimes -B build-libcxx-msan \
634+
-DCMAKE_C_COMPILER=clang-18 \
635+
-DCMAKE_CXX_COMPILER=clang++-18 \
636+
-DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi" \
637+
-DLLVM_USE_SANITIZER=MemoryWithOrigins \
638+
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
639+
-DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/libcxx-msan" ;
640+
ninja -C build-libcxx-msan cxx cxxabi ;
641+
ninja -C build-libcxx-msan install-cxx install-cxxabi ;
642+
if: ${{ matrix.build-type == 'msan' && steps.cache-libcxx-msan.outputs.cache-hit != 'true' }}
643+
604644
- name: Fetch libmicrohttpd from cache
605645
id: cache-libmicrohttpd
606646
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
@@ -612,7 +652,10 @@ jobs:
612652
# The flag-invariance-on lane uses the stock build because
613653
# libmicrohttpd 1.0.3 ships TLS/Basic/Digest auth on by default and
614654
# does NOT ship the websocket extension at all.
615-
if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.build-type != 'flag-invariance-off' && matrix.compiler-family != 'arm-cross' }}
655+
# TASK-087: the msan lane rebuilds libmicrohttpd instrumented (so MHD
656+
# output is not read back as uninitialized), so it skips this stock
657+
# cache/build too.
658+
if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.build-type != 'flag-invariance-off' && matrix.build-type != 'msan' && matrix.compiler-family != 'arm-cross' }}
616659

617660
- name: Build libmicrohttpd dependency (if not cached)
618661
run: |
@@ -625,7 +668,8 @@ jobs:
625668
# TASK-037: as above -- flag-invariance-off has its own dedicated
626669
# libmicrohttpd build step below; flag-invariance-on uses the stock
627670
# build because libmicrohttpd 1.0.3 ships no websocket extension.
628-
if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.build-type != 'flag-invariance-off' && matrix.compiler-family != 'arm-cross' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }}
671+
# TASK-087: msan has its own dedicated instrumented build step below.
672+
if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.build-type != 'flag-invariance-off' && matrix.build-type != 'msan' && matrix.compiler-family != 'arm-cross' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }}
629673

630674

631675
- name: Build libmicrohttpd without digest auth (no-dauth test)
@@ -656,6 +700,30 @@ jobs:
656700
make ;
657701
if: ${{ matrix.build-type == 'flag-invariance-off' && steps.cache-libmicrohttpd-off.outputs.cache-hit != 'true' }}
658702

703+
- name: Fetch libmicrohttpd from cache (msan lane)
704+
id: cache-libmicrohttpd-msan
705+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
706+
with:
707+
path: libmicrohttpd-1.0.3
708+
key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-1.0.3-msan-v1
709+
if: ${{ matrix.build-type == 'msan' }}
710+
711+
- name: Build instrumented libmicrohttpd (TASK-087 msan lane)
712+
run: |
713+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ;
714+
echo "7816b57aae199cf5c3645e8770e1be5f0a4dfafbcb24b3772173dc4ee634126a libmicrohttpd-1.0.3.tar.gz" | sha256sum -c ;
715+
tar -xzf libmicrohttpd-1.0.3.tar.gz ;
716+
cd libmicrohttpd-1.0.3 ;
717+
# Instrument MHD with MemorySanitizer so its output buffers, which
718+
# libhttpserver reads on nearly every code path, are not flagged as
719+
# uninitialized. --disable-https keeps gnutls (uninstrumented) out.
720+
export CC=clang-18 ;
721+
export CFLAGS='-fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1' ;
722+
export LDFLAGS='-fsanitize=memory' ;
723+
./configure --disable-examples --disable-https ;
724+
make ;
725+
if: ${{ matrix.build-type == 'msan' && steps.cache-libmicrohttpd-msan.outputs.cache-hit != 'true' }}
726+
659727
- name: Install libmicrohttpd
660728
run: cd libmicrohttpd-1.0.3 ; sudo make install ;
661729
if: ${{ matrix.os-type != 'windows' && matrix.compiler-family != 'arm-cross' }}
@@ -785,7 +853,16 @@ jobs:
785853
run: |
786854
# Set memory check flags. They need to stay in step as env variables don't propagate across steps.
787855
if [ "$BUILD_TYPE" = "asan" ]; then export CFLAGS='-fsanitize=address'; export CXXFLAGS='-fsanitize=address'; export LDFLAGS='-fsanitize=address'; fi
788-
if [ "$BUILD_TYPE" = "msan" ]; then export CFLAGS='-fsanitize=memory'; export CXXFLAGS='-fsanitize=memory'; export LDFLAGS='-fsanitize=memory'; fi
856+
# TASK-087: msan needs the instrumented libc++ built above, plus
857+
# origins tracking and frame pointers for readable reports. Point
858+
# the C++ TUs at the instrumented libc++ headers/libs and drop the
859+
# default (uninstrumented) libstdc++ via -nostdinc++/-stdlib=libc++.
860+
if [ "$BUILD_TYPE" = "msan" ]; then
861+
LIBCXX_MSAN="${{ github.workspace }}/libcxx-msan" ;
862+
export CFLAGS="-fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1" ;
863+
export CXXFLAGS="-stdlib=libc++ -nostdinc++ -isystem ${LIBCXX_MSAN}/include/c++/v1 -fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer -g -O1" ;
864+
export LDFLAGS="-stdlib=libc++ -fsanitize=memory -L${LIBCXX_MSAN}/lib -Wl,-rpath,${LIBCXX_MSAN}/lib -lc++abi" ;
865+
fi
789866
if [ "$BUILD_TYPE" = "lsan" ]; then export CFLAGS='-fsanitize=leak'; export CXXFLAGS='-fsanitize=leak'; export LDFLAGS='-fsanitize=leak'; fi
790867
if [ "$BUILD_TYPE" = "tsan" ]; then export CFLAGS='-fsanitize=thread'; export CXXFLAGS='-fsanitize=thread'; export LDFLAGS='-fsanitize=thread'; fi
791868
if [ "$BUILD_TYPE" = "ubsan" ]; then export CFLAGS='-fsanitize=undefined'; export CXXFLAGS='-fsanitize=undefined'; export LDFLAGS='-fsanitize=undefined'; fi
@@ -949,8 +1026,32 @@ jobs:
9491026
# (see test/tsan.supp). All libhttpserver-internal races stay fatal.
9501027
if [ "$BUILD_TYPE" = "tsan" ]; then
9511028
export TSAN_OPTIONS="suppressions=$(pwd)/../test/tsan.supp" ;
1029+
make check ;
1030+
elif [ "$BUILD_TYPE" = "msan" ]; then
1031+
# TASK-087: msan coverage is deliberately scoped to the
1032+
# backend-free UNIT tests. curl (linked into every integ
1033+
# round-trip via test/integ/curl_helpers.hpp) cannot be
1034+
# practically instrumented, so its uninstrumented output would
1035+
# surface as false use-of-uninitialized-value reports. libc++ and
1036+
# libmicrohttpd ARE instrumented (see the steps above), so the
1037+
# unit subset — pure libhttpserver logic plus instrumented-MHD
1038+
# type inspection — runs clean without wildcard suppressions.
1039+
export LD_LIBRARY_PATH="${{ github.workspace }}/libcxx-msan/lib:${LD_LIBRARY_PATH:-}" ;
1040+
export MSAN_OPTIONS="abort_on_error=1" ;
1041+
# Auto-wire narrow suppressions only if triage produced the file.
1042+
# test/msan.supp is intentionally ABSENT until a real finding
1043+
# forces a narrow use-of-uninitialized-value:<symbol> entry with
1044+
# an upstream-issue comment (never a wildcard over libhttpserver).
1045+
if [ -f ../test/msan.supp ]; then
1046+
export MSAN_OPTIONS="${MSAN_OPTIONS}:suppressions=$(pwd)/../test/msan.supp" ;
1047+
fi
1048+
# Derive the unit/ subset from the test Makefile so newly-added
1049+
# unit tests are covered automatically.
1050+
MSAN_TESTS="$(grep -oE '^[A-Za-z0-9_]+_SOURCES[[:space:]]*=[[:space:]]*unit/' ../test/Makefile.am | sed -E 's/_SOURCES.*//' | tr '\n' ' ')" ;
1051+
make check TESTS="$MSAN_TESTS" ;
1052+
else
1053+
make check ;
9521054
fi
953-
make check;
9541055
# TASK-037: flag-invariance-{on,off} are compile/link-only gates, so
9551056
# skip the full `make check` (which spins up real servers and would
9561057
# crash on the OFF lane for the BAUTH/DAUTH/WS unit tests anyway).

‎scripts/check-msan-lane.sh‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env bash
2+
#
3+
# check-msan-lane.sh — structural gate for TASK-087 ("Restore msan CI lane").
4+
#
5+
# The v2-branch-gap audit flagged that the MemorySanitizer (msan) CI lane
6+
# went silently absent: its matrix entry in
7+
# `.github/workflows/verify-build.yml` was commented out when Ubuntu 18.04 /
8+
# clang-6.0 were retired, and asan/lsan/tsan/ubsan carried on without it.
9+
# TASK-087 restores a current, instrumented-libc++ msan lane.
10+
#
11+
# A full GitHub Actions msan build cannot run on a developer's machine (it
12+
# needs an instrumented libc++ and libmicrohttpd built in-CI), so acceptance
13+
# is split: this LOCAL structural gate asserts the workflow is wired
14+
# correctly, and the REAL green run happens on the PR. This mirrors the
15+
# repo idiom of a self-testing `scripts/check-*.sh` gate paired with a
16+
# `scripts/test_check_*.sh` unit test (see check-warning-suppressions.sh).
17+
#
18+
# Assertions against `.github/workflows/verify-build.yml`:
19+
# (a) the file parses as valid YAML (via python3 + PyYAML; NOT actionlint
20+
# or yamllint, which are not guaranteed present on the runner);
21+
# (b) a matrix `include:` entry with `build-type: msan` exists and carries
22+
# the keys mirrored from the asan entry (test-group/os/os-type/
23+
# compiler-family/c-compiler/cc-compiler/debug/coverage/shell);
24+
# (c) the stale `ubuntu-18.04` / `clang-6.0` commented msan block is GONE;
25+
# (d) the "Run tests" step wires `MSAN_OPTIONS` and a scoped `TESTS=`
26+
# invocation (msan coverage is deliberately scoped to backend-free
27+
# unit tests because curl is uninstrumented).
28+
#
29+
# Exit codes:
30+
# 0 workflow is wired correctly
31+
# 1 one or more assertions failed
32+
set -euo pipefail
33+
34+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
35+
cd "$REPO_ROOT"
36+
37+
WF="${1:-.github/workflows/verify-build.yml}"
38+
39+
echo "check-msan-lane: inspecting $WF"
40+
41+
if [ ! -f "$WF" ]; then
42+
echo "check-msan-lane: FAIL — workflow file not found: $WF" >&2
43+
exit 1
44+
fi
45+
46+
fail=0
47+
48+
# (a) valid YAML + (b) msan include entry with mirrored keys.
49+
# Both are structural, so PyYAML parses the file and inspects the matrix.
50+
if ! python3 - "$WF" <<'PY'
51+
import sys
52+
try:
53+
import yaml
54+
except ImportError:
55+
print(" PyYAML not importable — install with 'pip install pyyaml'", file=sys.stderr)
56+
sys.exit(10)
57+
58+
path = sys.argv[1]
59+
try:
60+
doc = yaml.safe_load(open(path))
61+
except Exception as e: # noqa: BLE001 - surface any parse failure
62+
print(f" (a) YAML parse error: {e}", file=sys.stderr)
63+
sys.exit(2)
64+
65+
# Locate the matrix include list in whichever job defines it (job name is
66+
# not hard-coded so the gate survives a rename).
67+
include = None
68+
for job_name, job in (doc.get("jobs") or {}).items():
69+
if not isinstance(job, dict):
70+
continue
71+
inc = (((job.get("strategy") or {}).get("matrix") or {}).get("include"))
72+
if isinstance(inc, list):
73+
include = inc
74+
break
75+
if include is None:
76+
print(" (b) could not locate strategy.matrix.include in any job", file=sys.stderr)
77+
sys.exit(3)
78+
79+
msan = [e for e in include if isinstance(e, dict) and e.get("build-type") == "msan"]
80+
if not msan:
81+
print(" (b) no matrix include entry with build-type: msan", file=sys.stderr)
82+
sys.exit(4)
83+
if len(msan) > 1:
84+
print(f" (b) expected exactly one msan include entry, found {len(msan)}", file=sys.stderr)
85+
sys.exit(5)
86+
entry = msan[0]
87+
88+
# Keys mirrored from the asan entry: fixed-value keys must match, and the
89+
# per-lane keys must simply be present (value-agnostic so a future os/compiler
90+
# bump does not break the gate).
91+
fixed = {
92+
"test-group": "extra",
93+
"compiler-family": "clang",
94+
"debug": "debug",
95+
"coverage": "nocoverage",
96+
"shell": "bash",
97+
}
98+
present = ("os", "os-type", "c-compiler", "cc-compiler")
99+
problems = []
100+
for k, v in fixed.items():
101+
if entry.get(k) != v:
102+
problems.append(f"{k}={entry.get(k)!r} (want {v!r})")
103+
for k in present:
104+
if not entry.get(k):
105+
problems.append(f"{k} missing")
106+
if problems:
107+
print(" (b) msan include entry missing/incorrect keys: " + ", ".join(problems), file=sys.stderr)
108+
sys.exit(6)
109+
110+
print(f" (a) YAML parses; (b) msan include entry present with mirrored keys "
111+
f"(os={entry['os']}, c-compiler={entry['c-compiler']})")
112+
PY
113+
then
114+
echo " (a)/(b) FAILED" >&2
115+
fail=1
116+
fi
117+
118+
# (c) The stale ubuntu-18.04 / clang-6.0 commented msan block must be gone.
119+
# Look for a commented-out matrix entry that pairs a msan build-type with the
120+
# retired ubuntu-18.04 / clang-6.0 toolchain.
121+
if grep -qE '^[[:space:]]*#.*ubuntu-18\.04' "$WF" || grep -qE '^[[:space:]]*#.*clang-6\.0' "$WF"; then
122+
echo " (c) stale commented msan block (ubuntu-18.04 / clang-6.0) is still present" >&2
123+
fail=1
124+
else
125+
echo " (c) stale ubuntu-18.04 / clang-6.0 commented msan block removed"
126+
fi
127+
128+
# (d) The "Run tests" step must wire MSAN_OPTIONS and a scoped TESTS= run.
129+
if grep -q 'MSAN_OPTIONS' "$WF"; then
130+
echo " (d1) MSAN_OPTIONS wiring present"
131+
else
132+
echo " (d1) MSAN_OPTIONS wiring absent from the workflow" >&2
133+
fail=1
134+
fi
135+
if grep -qE 'make check TESTS=' "$WF"; then
136+
echo " (d2) scoped 'make check TESTS=' invocation present"
137+
else
138+
echo " (d2) scoped 'make check TESTS=' invocation absent from the workflow" >&2
139+
fail=1
140+
fi
141+
142+
if [ "$fail" -ne 0 ]; then
143+
echo "check-msan-lane: FAIL — the msan lane is not fully wired" >&2
144+
exit 1
145+
fi
146+
147+
echo "check-msan-lane: PASS — msan lane structurally wired"

0 commit comments

Comments
 (0)