diff --git a/build/la-lint/.clang-format b/build/la-lint/.clang-format new file mode 100644 index 00000000..613db0ca --- /dev/null +++ b/build/la-lint/.clang-format @@ -0,0 +1,46 @@ +--- +BasedOnStyle: Google +AlignAfterOpenBracket: DontAlign +AlignArrayOfStructures: Left +AlignConsecutiveAssignments: + AlignFunctionDeclarations: true +AlignConsecutiveMacros: + Enabled: true +AlignOperands: DontAlign +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakBeforeMultilineStrings: false +BinPackParameters: OnePerLine +BreakBeforeTemplateCloser: true +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterComma +BreakStringLiterals: false +ColumnLimit: 120 +ConstructorInitializerIndentWidth: 8 +EnumTrailingComma: Insert +IncludeBlocks: Preserve +IndentCaseLabels: false +IndentGotoLabels: false +IndentPPDirectives: AfterHash +IndentWidth: 8 +PPIndentWidth: 1 +PackConstructorInitializers: BinPack +PointerAlignment: Right +QualifierAlignment: Left +RemoveEmptyLinesInUnwrappedLines: true +SeparateDefinitionBlocks: Always +SortIncludes: + Enabled: false +SpaceBeforeParens: Custom +SpaceBeforeParensOptions: + AfterControlStatements: false + AfterForeachMacros: false + AfterIfMacros: false +SpacesInContainerLiterals: false +UseTab: ForIndentation + diff --git a/build/la-lint/CMakeLists.txt b/build/la-lint/CMakeLists.txt new file mode 100644 index 00000000..9480adc8 --- /dev/null +++ b/build/la-lint/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.12) +set(CMAKE_CXX_STANDARD 23) + +project(libarchive-clang-tools) +find_package(zstd REQUIRED) +find_package(Clang REQUIRED) +add_executable(la-lint) + +target_sources(la-lint + PRIVATE + main.cpp +) + +target_include_directories(la-lint + PRIVATE + ${CLANG_INCLUDE_DIRS} +) + +target_link_libraries(la-lint + PRIVATE + LLVM LLVMCore LLVMSupport + libclang clang-cpp +) + +install(TARGETS la-lint RUNTIME DESTINATION bin) diff --git a/build/la-lint/main.cpp b/build/la-lint/main.cpp new file mode 100644 index 00000000..077b1639 --- /dev/null +++ b/build/la-lint/main.cpp @@ -0,0 +1,190 @@ +/*- + * Copyright (c) 2026 Dustin L. Howett + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer + * in this position and unchanged. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace clang::tooling; +using namespace clang::ast_matchers; +using namespace llvm; + +static llvm::cl::OptionCategory LaToolCategory("la-clang-tool options"); +static llvm::cl::opt Prefix("prefix", + llvm::cl::desc("Project path prefix"), + llvm::cl::init(""), + llvm::cl::cat(LaToolCategory)); +static bool IsGithubCiBuild{}; + +struct MemCmpNeedleHandler : public MatchFinder::MatchCallback { + virtual void run(const MatchFinder::MatchResult &result) { + auto needle = result.Nodes.getNodeAs("needle"); + auto lenExpr = result.Nodes.getNodeAs("length"); + + /* + * This check uses [less than] rather than [less equal] because in some formats we want to verify the + * presence of the null terminator and in others we do not. + */ + if(lenExpr->getValue().ult(needle->getLength())) { + auto &diag = result.Context->getDiagnostics(); + auto did = diag.getCustomDiagID(clang::DiagnosticsEngine::Level::Error, + "memcmp needle has length `%0', but is checked with length `%1'"); + clang::DiagnosticBuilder db = diag.Report(lenExpr->getLocation(), did); + db.AddTaggedVal(needle->getLength(), clang::DiagnosticsEngine::ArgumentKind::ak_uint); + db.AddTaggedVal(lenExpr->getValue().getSExtValue(), + clang::DiagnosticsEngine::ArgumentKind::ak_sint); + db.AddSourceRange(clang::CharSourceRange::getCharRange(needle->getSourceRange())); + } + } +}; + +struct AssertUAFHandler : public MatchFinder::MatchCallback { + virtual void run(const MatchFinder::MatchResult &result) { + /* + * This code uses getFileLoc to specify locations disregarding macro expansion + * and to avoid Clang printing "macro expanded from here". + */ + auto &SM = *result.SourceManager; + auto uaf = result.Nodes.getNodeAs("uaf"); + auto assertion = result.Nodes.getNodeAs("assertion"); + auto freed = result.Nodes.getNodeAs("free"); + + auto &diag = result.Context->getDiagnostics(); + { + auto did = diag.getCustomDiagID(clang::DiagnosticsEngine::Level::Error, + "test assertion uses `%0' after free; prefer assertEqualInt"); + clang::DiagnosticBuilder db = diag.Report(SM.getFileLoc(uaf->getBeginLoc()), did); + db.AddString(uaf->getNameInfo().getAsString()); + db << clang::FixItHint::CreateReplacement( + clang::SourceRange{SM.getFileLoc(assertion->getBeginLoc()), + SM.getFileLoc(uaf->getBeginLoc())}, + "assertEqualInt("); + } + + { + auto did = diag.getCustomDiagID(clang::DiagnosticsEngine::Level::Note, "`%0' freed here"); + clang::DiagnosticBuilder db = diag.Report(freed->getBeginLoc(), did); + db.AddString(uaf->getNameInfo().getAsString()); + } + } +}; + +class GithubActionDiagnosticConsumer : public clang::DiagnosticConsumer { + virtual void HandleDiagnostic(clang::DiagnosticsEngine::Level DiagLevel, const clang::Diagnostic &Info) { + static constexpr const char *levels[] = {nullptr, "notice", "notice", "warning", "error", "error"}; + + const auto level = levels[DiagLevel]; + if(!level) + return; + + auto &SM = Info.getSourceManager(); + auto loc = Info.getLocation(); + loc = SM.getFileLoc(loc); + + auto file = SM.getFilename(loc); + if(file.empty()) { + return; + } + file.consume_front_insensitive(Prefix); + while(file.consume_front("/")) {} + + bool lineInvalid = false; + auto line = SM.getLineNumber(loc, &lineInvalid); + if(lineInvalid) { + return; + } + + llvm::SmallVector diagString; + Info.FormatDiagnostic(diagString); + + std::print("::{0} file={1},line={2},title=Lint::{3}\n", level, std::string_view{file}, line, + std::string_view{diagString}); + } +}; + +int main(int argc, const char **argv) { + auto exp = CommonOptionsParser::create(argc, argv, LaToolCategory); + auto &optionsParser = exp.get(); + ClangTool tool(optionsParser.getCompilations(), optionsParser.getSourcePathList()); + + if(const char *e = getenv("GITHUB_WORKSPACE")) { + IsGithubCiBuild = true; + + if(Prefix.c_str()[0] == '\0') { + Prefix.assign(e); + Prefix.append("/"); + } + } + + MatchFinder finder; + + /* + * Find every assertion_equal_mem that passes a string literal. + * --> assertEqualMem(foo, "literal", 7); + */ + MemCmpNeedleHandler memCmpHandler; + finder.addMatcher(callExpr(callee(functionDecl(hasName("assertion_equal_mem"))), + hasArgument(4, stringLiteral().bind("needle")), + hasArgument(6, integerLiteral(anything()).bind("length"))), + &memCmpHandler); + + /* + * Find every assertion_equal_int whose call site contains a `free` function, + * and then passes the freed decl to assertion_equal_int as context. + * --> assertEqualIntA(a, ARCHIVE_OK, archive_write_free(a)); + * ^-- must be the same as ----------^ + */ + AssertUAFHandler uafHandler; + finder.addMatcher( + callExpr(callee(functionDecl(hasName("assertion_equal_int"))), + hasArgument(4, + callExpr(callee(functionDecl(matchesName("archive.*_free$"))), + hasArgument(0, declRefExpr(to(varDecl(anything()).bind("freed"))))) + .bind("free")), + hasArgument(6, declRefExpr(to(varDecl(declaresSameEntityAsBoundNode("freed")))).bind("uaf"))) + .bind("assertion"), + &uafHandler); + + std::unique_ptr diagnosticConsumer; + + if(IsGithubCiBuild) { + /* Emit GitHub-formatted error messages by default when we are building in GitHub Actions */ + clang::DiagnosticOptions diagOptions; + diagOptions.ShowColors = true; + clang::TextDiagnosticPrinter defaultDiag(llvm::errs(), diagOptions, false); + diagnosticConsumer = std::make_unique(&defaultDiag, + std::make_unique()); + tool.setDiagnosticConsumer(diagnosticConsumer.get()); + } + + return tool.run(newFrontendActionFactory(&finder).get()); +}