From 8ba6540491c5d28869d845fce07740e68d4e8310 Mon Sep 17 00:00:00 2001 From: metsw24-max Date: Sat, 23 May 2026 15:27:59 +0530 Subject: [PATCH] Fix iterator UB in ARGS_NOEXCEPT completion parsing --- poc/build_and_run.sh | 35 ++++++++++++++++ poc/poc_noexcept_iter_oob.cxx | 76 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 poc/build_and_run.sh create mode 100644 poc/poc_noexcept_iter_oob.cxx diff --git a/poc/build_and_run.sh b/poc/build_and_run.sh new file mode 100644 index 0000000..d13b477 --- /dev/null +++ b/poc/build_and_run.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Build PoC against buggy and fixed args.hxx, run both under ASan+UBSan. +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +cd "$HERE" + +# Extract the two args.hxx versions from git history into this directory. +( cd /mnt/d/args && git show a1407f8^:args.hxx > "$HERE/args_buggy.hxx" ) +( cd /mnt/d/args && git show a1407f8:args.hxx > "$HERE/args_fixed.hxx" ) +ls -la args_*.hxx + +SAN='-fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer' +COMMON="-std=c++11 -O0 -g -I. $SAN -Wall" + +echo "===== BUILD: buggy =====" +clang++ $COMMON -DARGS_HXX_PATH='"args_buggy.hxx"' poc_noexcept_iter_oob.cxx -o poc_buggy +echo "build exit: $?" + +echo "===== BUILD: fixed =====" +clang++ $COMMON -DARGS_HXX_PATH='"args_fixed.hxx"' poc_noexcept_iter_oob.cxx -o poc_fixed +echo "build exit: $?" + +echo +echo "===== RUN: buggy =====" +ASAN_OPTIONS=abort_on_error=0:halt_on_error=0:print_stacktrace=1 \ +UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0 \ +./poc_buggy +echo "buggy exit: $?" + +echo +echo "===== RUN: fixed =====" +ASAN_OPTIONS=abort_on_error=0:halt_on_error=0:print_stacktrace=1 \ +UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=0 \ +./poc_fixed +echo "fixed exit: $?" diff --git a/poc/poc_noexcept_iter_oob.cxx b/poc/poc_noexcept_iter_oob.cxx new file mode 100644 index 0000000..78a441e --- /dev/null +++ b/poc/poc_noexcept_iter_oob.cxx @@ -0,0 +1,76 @@ +// PoC for ARGS_NOEXCEPT iterator OOB and dangling-iterator bugs fixed +// in commit a1407f8. Exercises both sites: +// Site 1: ParseArgsValues completion path sets `it = end`, caller does ++it +// on an already-end iterator -> OOB increment + OOB read. +// Site 2: ParseLong returns an iterator into a local `curArgs` vector that +// is destroyed when the function returns -> dangling iterator +// returned to ParseCLI and compared with == against outer end. +// +// Build: +// clang++ -std=c++11 -O0 -g -fsanitize=address,undefined \ +// -DARGS_HXX_PATH='"args_buggy.hxx"' poc_noexcept_iter_oob.cxx -o poc_buggy +// clang++ -std=c++11 -O0 -g -fsanitize=address,undefined \ +// -DARGS_HXX_PATH='"args_fixed.hxx"' poc_noexcept_iter_oob.cxx -o poc_fixed +// +// Run: +// ./poc_buggy # expect ASan heap-buffer-overflow + UBSan dangling compare +// ./poc_fixed # expect clean exit + +#define ARGS_NOEXCEPT + +#ifndef ARGS_HXX_PATH +#define ARGS_HXX_PATH "args.hxx" +#endif + +#include ARGS_HXX_PATH + +#include +#include +#include + +int main() +{ + // -------- Site 1: ARGS_NOEXCEPT completion on a flag value position -------- + // cword=2 lands on the empty value position of "-m". On the buggy build + // ParseArgsValues sets `it = end` then the caller does `++it` on an + // already-end iterator, after which the loop dereferences it. + { + args::ArgumentParser p("parser"); + args::CompletionFlag c(p, {"completion"}); + args::MapFlag m( + p, "mappos", "mappos", {'m', "map"}, + {{"alpha", 1}, {"beta", 2}}); + + p.ParseArgs(std::vector{ + "--completion", "bash", "2", "test", "-m", ""}); + + std::fprintf(stderr, "site1 ok: err=%d reply=[%s]\n", + static_cast(p.GetError()), + args::get(c).c_str()); + } + + // -------- Site 2: Dangling iterator returned across stack frames -------- + // ParseCLI compares the return value of ParseArgs against std::end(args). + // On the buggy build the returned iterator points into a destroyed local + // `curArgs` vector inside the completion handler; the == compare is then + // a dangling-iterator compare (UB). + { + args::ArgumentParser p("parser"); + args::CompletionFlag c(p, {"completion"}); + args::MapFlag m( + p, "mappos", "mappos", {'m', "map"}, + {{"alpha", 1}, {"beta", 2}}); + + const char *argv[] = { + "prog", "--completion", "bash", "2", "test", "-m", ""}; + const int argc = static_cast(sizeof(argv) / sizeof(argv[0])); + p.ParseCLI(argc, argv); + + std::fprintf(stderr, "site2 ok: err=%d reply=[%s]\n", + static_cast(p.GetError()), + args::get(c).c_str()); + } + + std::fprintf(stderr, "done\n"); + return 0; +}