Files
Martin Vogel a654acbc88 fix(test-infra): grant the build-dir ACL by SID, not by name (#1532 follow-up)
The local Windows leg caught this within hours of merging #1532: on a machine
that is not domain-joined, USERDOMAIN is the literal string WORKGROUP, and
`WORKGROUP\test` is not a resolvable principal —

    WARN: build-dir DACL stamp (pre-wave) failed (user=WORKGROUP\test ...):
    WORKGROUP\test: No mapping between account names and security IDs was done.

so the grant fails outright and the tree keeps its inherited
`Authenticated Users:(M)`. The guard then correctly reports that cross-account
mutation is still possible, and the whole leg stops. Since most Windows
machines are not domain-joined, that is the common case, not the exotic one.

#1532 was fixing a real bug in the other direction (a bare name resolves
against the machine first, so a host named like its user grants to an empty
principal). Both failures are name RESOLUTION failures, so this stops resolving
names: the account is identified by SID, which is unambiguous everywhere. The
SYSTEM and Administrators grants in these same commands have always used that
form — the current user was the odd one out. Name lookup survives only as a
fallback where PowerShell is unavailable.

Applied to all three scripts carrying the stamp (run-tests-parallel, soak-test,
memlab), since they share the failure and would otherwise drift apart.

Co-Authored-By: Kiborgik <Kiborgik@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-12 11:57:35 +02:00

164 lines
6.9 KiB
Bash

#!/usr/bin/env bash
#
# memlab.sh — deterministic memory-attribution run for #581.
#
# The soak answers "did memory grow over eight minutes"; this answers "which
# allocation sites retained it", in about a minute, with a fixed request count
# so two runs are directly comparable. Everything that made soak numbers hard
# to compare — wall-clock duration, background reindexes, crash-recovery
# phases, throughput differences between builds — is deliberately absent.
#
# Usage: scripts/memlab.sh <binary> [requests] [label]
#
# Output: memlab-<label>.jsonl (profiler records, one block per sample)
# memlab-<label>.log (daemon log with mem.census lines)
# Analyse with: scripts/memlab-report.py memlab-<label>.jsonl --census memlab-<label>.log
set -u
BINARY="${1:?usage: memlab.sh <binary> [requests] [label]}"
REQUESTS="${2:-200}"
LABEL="${3:-$(uname -s | tr '[:upper:]' '[:lower:]')}"
if [ ! -x "$BINARY" ]; then
echo "FAIL: $BINARY is not executable" >&2
exit 2
fi
WORK=$(mktemp -d 2>/dev/null || mktemp -d -t memlab)
# Native Windows: the server walks the full ancestor chain of both the binary
# and the cache dir and refuses to start when any ancestor grants mutation to
# untrusted SIDs. The repo checkout and the msys /tmp tree both carry such ACEs,
# so the run has to happen from a stamped root under USERPROFILE — the same
# reason soak-test.sh and the guard suites copy their binaries there.
if [[ "$BINARY" == *.exe ]] && command -v cygpath >/dev/null 2>&1 &&
! command -v winepath >/dev/null 2>&1; then
WIN_ROOT=$(mktemp -d "$(cygpath "$USERPROFILE")/cbm-memlab.XXXXXX")
WIN_ROOT_W="$(cygpath -w "$WIN_ROOT")"
# Qualify with the domain: a bare name from coreutils `whoami` resolves
# against the machine first, so on a host whose name equals the user's the
# grant lands on an empty principal.
# Identify the account by SID, never by name. icacls resolves a bare name
# against the machine first, so a host whose name equals the user's grants
# to an empty principal (#1532); and a USERDOMAIN-qualified name is
# UNRESOLVABLE on a workgroup machine — "WORKGROUP\test: No mapping between
# account names and security IDs was done" — which fails the grant outright
# and silently leaves the tree writable by Authenticated Users. A SID has
# neither ambiguity, and the SYSTEM/Administrators grants below already use
# this form. Name lookup remains only as a fallback where PowerShell is
# unavailable.
ME="$(powershell.exe -NoProfile -NonInteractive -Command \
'[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value' 2>/dev/null |
tr -d '\r\n')"
case "${ME}" in
S-1-*) ME="*${ME}" ;;
*)
ME="$(whoami | tr -d '\r')"
if [ -n "${USERDOMAIN:-}" ]; then
ME="${USERDOMAIN}\\${ME}"
fi
;;
esac
MSYS2_ARG_CONV_EXCL='*' icacls "$WIN_ROOT_W" /reset /Q >/dev/null 2>&1 || true
if ! MSYS2_ARG_CONV_EXCL='*' icacls "$WIN_ROOT_W" /inheritance:r \
/grant:r "${ME}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' \
/Q >/dev/null 2>&1; then
echo "FAIL: could not stamp $WIN_ROOT_W" >&2
exit 1
fi
cp "$BINARY" "$WIN_ROOT/codebase-memory-mcp.exe"
BINARY="$WIN_ROOT/codebase-memory-mcp.exe"
WORK="$WIN_ROOT"
MSYS2_ARG_CONV_EXCL='*' icacls "${WIN_ROOT_W}\\*" /reset /T /C /Q >/dev/null 2>&1 || true
fi
PROFILE_OUT="$PWD/memlab-${LABEL}.jsonl"
RUN_LOG="$PWD/memlab-${LABEL}.log"
rm -f "$PROFILE_OUT" "$RUN_LOG"
cleanup() { rm -rf "$WORK" 2>/dev/null || true; rm -rf "${WIN_ROOT:-}" 2>/dev/null || true; }
trap cleanup EXIT
# A fixed corpus: same file count and content on every platform, so a
# difference in the output cannot come from a difference in the input.
CORPUS="$WORK/corpus"
mkdir -p "$CORPUS/src"
for i in $(seq 1 60); do
cat > "$CORPUS/src/module_$i.py" <<EOF
class Widget$i:
"""Fixed corpus file $i."""
def __init__(self, name):
self.name = name
self.parts = []
def attach(self, part):
self.parts.append(part)
return self
def render(self):
return "-".join(str(p) for p in self.parts)
def build_$i(count):
widget = Widget$i("w$i")
for index in range(count):
widget.attach(index)
return widget.render()
EOF
done
echo "=== memlab: binary=$BINARY requests=$REQUESTS label=$LABEL ==="
echo "corpus: $(find "$CORPUS" -name '*.py' | wc -l | tr -d ' ') files"
# The Windows binary needs a native path here; an msys /c/... path is not one.
if command -v cygpath >/dev/null 2>&1 && ! command -v winepath >/dev/null 2>&1; then
mkdir -p "$WORK/cache"
export CBM_CACHE_DIR="$(cygpath -w "$WORK/cache")"
else
export CBM_CACHE_DIR="$WORK/cache"
fi
export CBM_MEM_PROFILE=1
export CBM_MEM_PROFILE_OUT="$PROFILE_OUT"
export CBM_MEM_CENSUS=1
export CBM_LOG_LEVEL=info
export CBM_LOG_FORMAT=text
mkdir -p "$CBM_CACHE_DIR"
# Drive the MCP stdio path via the request/response driver (see its header for
# why batching into a closed stdin does not work).
# The VM's python3 is a NATIVE win32 build, so it cannot open msys paths like
# /c/Users/... . Hand it Windows paths, or it starts nothing and the failure
# looks like the server closing stdout.
DRIVE_BINARY="$BINARY"
DRIVE_CORPUS="$CORPUS"
DRIVE_STDERR="$WORK/server-stderr.log"
if command -v cygpath >/dev/null 2>&1 && ! command -v winepath >/dev/null 2>&1; then
DRIVE_BINARY="$(cygpath -w "$BINARY")"
DRIVE_CORPUS="$(cygpath -w "$CORPUS")"
DRIVE_STDERR="$(cygpath -w "$WORK/server-stderr.log")"
fi
python3 "$(dirname "$0")/memlab-drive.py" "$DRIVE_BINARY" "$DRIVE_CORPUS" "$REQUESTS" --tool "${MEMLAB_TOOL:-search_graph}" --idle-seconds "${MEMLAB_IDLE:-0}" --stderr "$DRIVE_STDERR" > "$WORK/drive.out" 2>&1
RC=$?
# With CBM_CACHE_DIR set the process logs to its own file rather than stderr,
# so fold that in or the census series is invisible.
cat "$WORK"/cache/logs/*.log >> "$RUN_LOG" 2>/dev/null || true
cat "$WORK/server-stderr.log" >> "$RUN_LOG" 2>/dev/null || true
RESPONSES=$(sed -n "s/.*served=\\([0-9]*\\).*/\\1/p" "$WORK/drive.out" | head -1); RESPONSES=${RESPONSES:-0}
CENSUS=$(grep -c "mem.census" "$RUN_LOG" 2>/dev/null | head -1); CENSUS=${CENSUS:-0}
SITES=$(grep -c '"site"' "$PROFILE_OUT" 2>/dev/null | head -1); SITES=${SITES:-0}
echo "exit=$RC responses=$RESPONSES census_samples=$CENSUS profile_records=$SITES"
if [ "$RESPONSES" -eq 0 ]; then
# A harness that hides why it failed is worse than no harness.
echo "--- driver output ---" >&2
cat "$WORK/drive.out" 2>/dev/null | tail -15 >&2
fi
if [ "$CENSUS" -eq 0 ]; then
echo "WARN: no census samples — check CBM_MEM_CENSUS wiring" >&2
fi
if [ "$SITES" -eq 0 ]; then
# Not fatal, but never silent: on macOS there is no --wrap, so the
# profiler legitimately has no observation point.
echo "WARN: no profiler records — expected on macOS (no --wrap); a gap anywhere else" >&2
fi
echo "profile: $PROFILE_OUT"
echo "log: $RUN_LOG"