diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h --- a/clang/include/clang/AST/Decl.h +++ b/clang/include/clang/AST/Decl.h @@ -2839,6 +2839,7 @@ unsigned BitField : 1; unsigned Mutable : 1; mutable unsigned CachedFieldIndex : 30; + mutable unsigned OriginalFieldIndex : 30; /// The kinds of value we can store in InitializerOrBitWidth. /// @@ -2883,12 +2884,12 @@ protected: FieldDecl(Kind DK, DeclContext *DC, SourceLocation StartLoc, - SourceLocation IdLoc, IdentifierInfo *Id, - QualType T, TypeSourceInfo *TInfo, Expr *BW, bool Mutable, + SourceLocation IdLoc, IdentifierInfo *Id, QualType T, + TypeSourceInfo *TInfo, Expr *BW, bool Mutable, InClassInitStyle InitStyle) - : DeclaratorDecl(DK, DC, IdLoc, Id, T, TInfo, StartLoc), - BitField(false), Mutable(Mutable), CachedFieldIndex(0), - InitStorage(nullptr, (InitStorageKind) InitStyle) { + : DeclaratorDecl(DK, DC, IdLoc, Id, T, TInfo, StartLoc), BitField(false), + Mutable(Mutable), CachedFieldIndex(0), OriginalFieldIndex(0), + InitStorage(nullptr, (InitStorageKind)InitStyle) { if (BW) setBitWidth(BW); } @@ -2909,6 +2910,16 @@ /// as appropriate for passing to ASTRecordLayout::getFieldOffset. unsigned getFieldIndex() const; + /// For struct field reorg, this is the original index, 1-based, or + /// 0 if reorg did not happen. + unsigned getOriginalFieldIndex() const { return OriginalFieldIndex; } + + /// For struct field reorg, sets a 1-based index. + void setOriginalFieldIndex(unsigned Idx) { + assert(Idx && "Invalid original field index"); + OriginalFieldIndex = Idx; + } + /// Determines whether this field is mutable (C++ only). bool isMutable() const { return Mutable; } @@ -4051,6 +4062,10 @@ RecordDeclBits.ParamDestroyedInCallee = V; } + bool isRandomized() const { return RecordDeclBits.IsRandomized; } + + void setIsRandomized(bool V) { RecordDeclBits.IsRandomized = V; } + /// Determines whether this declaration represents the /// injected class name. /// diff --git a/clang/include/clang/AST/DeclBase.h b/clang/include/clang/AST/DeclBase.h --- a/clang/include/clang/AST/DeclBase.h +++ b/clang/include/clang/AST/DeclBase.h @@ -64,6 +64,10 @@ class TranslationUnitDecl; class UsingDirectiveDecl; +namespace randstruct { +struct Randstruct; +} // end namespace randstruct + /// Captures the result of checking the availability of a /// declaration. enum AvailabilityResult { @@ -1363,6 +1367,7 @@ /// For hasNeedToReconcileExternalVisibleStorage, /// hasLazyLocalLexicalLookups, hasLazyExternalLexicalLookups friend class ASTWriter; + friend struct randstruct::Randstruct; // We use uint64_t in the bit-fields below since some bit-fields // cross the unsigned boundary and this breaks the packing. @@ -1540,10 +1545,13 @@ /// Represents the way this type is passed to a function. uint64_t ArgPassingRestrictions : 2; + + /// Indicates whether this struct has had its field layout randomized. + uint64_t IsRandomized : 1; }; /// Number of non-inherited bits in RecordDeclBitfields. - enum { NumRecordDeclBits = 14 }; + enum { NumRecordDeclBits = 15 }; /// Stores the bits used by OMPDeclareReductionDecl. /// If modified NumOMPDeclareReductionDeclBits and the accessor diff --git a/clang/include/clang/AST/Randstruct.h b/clang/include/clang/AST/Randstruct.h new file mode 100644 --- /dev/null +++ b/clang/include/clang/AST/Randstruct.h @@ -0,0 +1,34 @@ +//===- Randstruct.h - Interfact for structure randomization -------*- C++ -*-=// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file contains the interface for Clang's structure field layout +// randomization. +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_AST_RANDSTRUCT_H +#define LLVM_CLANG_AST_RANDSTRUCT_H + +#include + +namespace clang { + +class ASTContext; +class RecordDecl; + +namespace randstruct { + +extern std::string SEED; + +bool shouldRandomize(const ASTContext &Context, const RecordDecl *RD); +void randomizeStructureLayout(const ASTContext &Context, const RecordDecl *RD); + +} // namespace randstruct +} // namespace clang + +#endif // LLVM_CLANG_AST_RANDSTRUCT_H diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -3933,3 +3933,19 @@ let Subjects = SubjectList<[Function], ErrorDiag>; let Documentation = [ErrorAttrDocs]; } + +def RandomizeLayout : InheritableAttr { + let Spellings = [GCC<"randomize_layout">, Declspec<"randomize_layout">, + Keyword<"randomize_layout">]; + let Subjects = SubjectList<[Record]>; + let Documentation = [ClangRandstructDocs]; + let LangOpts = [COnly]; +} + +def NoRandomizeLayout : InheritableAttr { + let Spellings = [GCC<"no_randomize_layout">, Declspec<"no_randomize_layout">, + Keyword<"no_randomize_layout">]; + let Subjects = SubjectList<[Record]>; + let Documentation = [ClangRandstructDocs]; + let LangOpts = [COnly]; +} diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -6346,3 +6346,15 @@ .. _Return-Oriented Programming: https://en.wikipedia.org/wiki/Return-oriented_programming }]; } + +def ClangRandstructDocs : Documentation { + let Category = DocCatVariable; + let Heading = "randomize_layout, no_randomize_layout"; + let Content = [{ +The attribute ``randomize_layout`` can be added to a record-type definition to select +it for structure layout field randomization; a compile-time hardening technique. + +The attribute ``no_randomize_layout`` can be added to a record-type definition to instruct +the compiler that this structure should not have its field layout randomized. + }]; +} diff --git a/clang/include/clang/Basic/DiagnosticASTKinds.td b/clang/include/clang/Basic/DiagnosticASTKinds.td --- a/clang/include/clang/Basic/DiagnosticASTKinds.td +++ b/clang/include/clang/Basic/DiagnosticASTKinds.td @@ -593,4 +593,9 @@ def warn_unaligned_access : Warning< "field %1 within %0 is less aligned than %2 and is usually due to %0 being " "packed, which can lead to unaligned accesses">, InGroup, DefaultIgnore; + +def warn_randomize_attr_conflict : Warning< + "struct declared with 'randomize_layout' and 'no_randomize_layout' attributes; " + "attribute 'no_randomize_layout' takes precedence">, InGroup>; + } diff --git a/clang/include/clang/Basic/DiagnosticDriverKinds.td b/clang/include/clang/Basic/DiagnosticDriverKinds.td --- a/clang/include/clang/Basic/DiagnosticDriverKinds.td +++ b/clang/include/clang/Basic/DiagnosticDriverKinds.td @@ -165,6 +165,8 @@ "invalid argument '-mno-amdgpu-ieee' only allowed with relaxed NaN handling">; def err_drv_argument_not_allowed_with : Error< "invalid argument '%0' not allowed with '%1'">; +def err_drv_cannot_open_randstruct_seed_filename : Error< + "cannot read randstruct seed file '%0'">; def err_drv_invalid_version_number : Error< "invalid version number in '%0'">; def err_drv_no_linker_llvm_support : Error< diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -11527,4 +11527,7 @@ "builtin requires at least one of the following extensions support to be enabled : %0">; def err_riscv_builtin_invalid_lmul : Error< "LMUL argument must be in the range [0,3] or [5,7]">; + +def cast_from_randomized_struct : Error< + "casting between randomized structure pointer types %0 and %1">; } // end of sema component. diff --git a/clang/include/clang/Driver/Options.td b/clang/include/clang/Driver/Options.td --- a/clang/include/clang/Driver/Options.td +++ b/clang/include/clang/Driver/Options.td @@ -2114,6 +2114,8 @@ def fmessage_length_EQ : Joined<["-"], "fmessage-length=">, Group, Flags<[CC1Option]>, HelpText<"Format message diagnostics so that they fit within N columns">, MarshallingInfoInt>; +def randstruct_seed_EQ : Joined<["-"], "randstruct-seed=">, Group; +def randstruct_seed_filename_EQ : Joined<["-"], "randstruct-seed-filename=">, Group; def fms_compatibility : Flag<["-"], "fms-compatibility">, Group, Flags<[CC1Option, CoreOption]>, HelpText<"Enable full Microsoft Visual C++ compatibility">, MarshallingInfoFlag>; diff --git a/clang/lib/AST/CMakeLists.txt b/clang/lib/AST/CMakeLists.txt --- a/clang/lib/AST/CMakeLists.txt +++ b/clang/lib/AST/CMakeLists.txt @@ -97,6 +97,7 @@ ParentMap.cpp PrintfFormatString.cpp QualTypeNames.cpp + Randstruct.cpp RawCommentList.cpp RecordLayout.cpp RecordLayoutBuilder.cpp diff --git a/clang/lib/AST/Decl.cpp b/clang/lib/AST/Decl.cpp --- a/clang/lib/AST/Decl.cpp +++ b/clang/lib/AST/Decl.cpp @@ -4569,6 +4569,7 @@ setHasNonTrivialToPrimitiveCopyCUnion(false); setParamDestroyedInCallee(false); setArgPassingRestrictions(APK_CanPassInRegs); + setIsRandomized(false); } RecordDecl *RecordDecl::Create(const ASTContext &C, TagKind TK, DeclContext *DC, diff --git a/clang/lib/AST/DeclBase.cpp b/clang/lib/AST/DeclBase.cpp --- a/clang/lib/AST/DeclBase.cpp +++ b/clang/lib/AST/DeclBase.cpp @@ -1344,6 +1344,9 @@ PrevDecl = D; } + // Last item in the linked list should have a null next pointer. + PrevDecl->NextInContextAndBits.setPointer(nullptr); + return std::make_pair(FirstNewDecl, PrevDecl); } diff --git a/clang/lib/AST/Randstruct.cpp b/clang/lib/AST/Randstruct.cpp new file mode 100644 --- /dev/null +++ b/clang/lib/AST/Randstruct.cpp @@ -0,0 +1,250 @@ +//===--- Randstruct.cpp ---------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file contains the implementation for Clang's structure field layout +// randomization. +// +//===----------------------------------------------------------------------===// + +#include "clang/AST/Randstruct.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/ASTDiagnostic.h" +#include "clang/AST/Attr.h" +#include "clang/Basic/Diagnostic.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include +#include +#include +#include + +namespace clang { +namespace randstruct { + +std::string SEED; + +struct Randstruct { + void randomize(const ASTContext &Context, + SmallVectorImpl &OutFields, + std::mt19937 &Rng) const; + void commit(const RecordDecl *RD, + SmallVectorImpl &NewDeclOrder) const; +}; + +bool shouldRandomize(const ASTContext &Context, const RecordDecl *RD) { + if (RD->isUnion()) + return false; + + bool HasRandAttr = RD->getAttr() != nullptr; + bool HasNoRandAttr = RD->getAttr() != nullptr; + if (HasRandAttr && HasNoRandAttr) + Context.getDiagnostics().Report(RD->getLocation(), + diag::warn_randomize_attr_conflict); + + return !HasNoRandAttr && HasRandAttr; +} + +void randomizeStructureLayout(const ASTContext &Context, const RecordDecl *RD) { + constexpr auto SMALL_VEC_SIZE = 16UL; + SmallVector Others; + SmallVector Fields; + FieldDecl *VLA = nullptr; + unsigned index = 1; + + std::set MutateGuard; + for (auto *Decl : RD->decls()) { + MutateGuard.insert(Decl); + if (isa(Decl)) { + auto *Field = cast(Decl); + Field->setOriginalFieldIndex(index); + ++index; + Fields.push_back(Field); + } else { + Others.push_back(Decl); + } + } + + if (Fields.size() > 0) { + // Struct might end with a variable-length array or an array of size 0 or 1: + auto MaybeVLA = Fields.back(); + auto Type = MaybeVLA->getType(); + auto *CA = dyn_cast(Type); + if ((CA && CA->getSize().sle(2)) || Type->isIncompleteArrayType() || + RD->hasFlexibleArrayMember()) { + VLA = Fields.pop_back_val(); + } + } + + std::stringstream ss(SEED); + ss << ":" << RD->getNameAsString(); + std::string seed = ss.str(); + std::seed_seq sseq(seed.begin(), seed.end()); + std::mt19937 Rng(sseq); + Randstruct Rand; + Rand.randomize(Context, Fields, Rng); + + SmallVector NewOrder = Others; + NewOrder.insert(NewOrder.end(), Fields.begin(), Fields.end()); + if (VLA) { + NewOrder.push_back(VLA); + } + + assert(MutateGuard.size() == NewOrder.size() && + "Decl count has been altered after Randstruct randomization!"); + Rand.commit(RD, NewOrder); + // FIXME: Oof, const_cast + const_cast(RD)->setIsRandomized(true); +} + +// FIXME: Replace this with some discovery once that mechanism exists. +const auto CACHE_LINE = 64; + +class Bucket { + std::vector Fields; + int Size = 0; + +public: + virtual ~Bucket() = default; + + std::vector &fields() { return Fields; } + void addField(FieldDecl *Field, int FieldSize); + virtual bool canFit(int FieldSize) const { + return Size + FieldSize <= CACHE_LINE; + } + virtual bool isBitfieldRun() const { return false; } + bool full() const { return Size >= CACHE_LINE; } +}; + +struct BitfieldRun : public Bucket { + bool canFit(int FieldSize) const override { return true; } + bool isBitfieldRun() const override { return true; } +}; + +void Bucket::addField(FieldDecl *Field, int FieldSize) { + Size += FieldSize; + Fields.push_back(Field); +} + +void Randstruct::randomize(const ASTContext &Context, + SmallVectorImpl &FieldsOut, + std::mt19937 &Rng) const { + // FIXME: Replace std::vector with LLVM ADT + using namespace randstruct; + // All of the Buckets produced by best-effort cache-line algorithm. + std::vector> Buckets; + + // The current bucket of fields that we are trying to fill to a cache-line. + std::unique_ptr CurrentBucket = nullptr; + // The current bucket containing the run of adjacent bitfields to ensure + // they remain adjacent. + std::unique_ptr CurrentBitfieldRun = nullptr; + + // Tracks the number of fields that we failed to fit to the current bucket, + // and thus still need to be added later. + auto Skipped = 0ul; + + while (!FieldsOut.empty()) { + // If we've Skipped more fields than we have remaining to place, + // that means that they can't fit in our current bucket, and we + // need to start a new one. + if (Skipped >= FieldsOut.size()) { + Skipped = 0; + Buckets.push_back(std::move(CurrentBucket)); + } + + // Take the first field that needs to be put in a bucket. + auto Field = FieldsOut.begin(); + auto *F = llvm::cast(*Field); + + if (F->isBitField() && !F->isZeroLengthBitField(Context)) { + // Start a bitfield run if this is the first bitfield + // we have found. + if (!CurrentBitfieldRun) { + CurrentBitfieldRun = std::make_unique(); + } + + // We've placed the field, and can remove it from the + // "awaiting Buckets" vector called "Fields" + CurrentBitfieldRun->addField(F, /*FieldSize is irrelevant here*/ 1); + FieldsOut.erase(Field); + } else { + // Else, current field is not a bitfield + // If we were previously in a bitfield run, end it. + if (CurrentBitfieldRun) { + Buckets.push_back(std::move(CurrentBitfieldRun)); + } + // If we don't have a bucket, make one. + if (!CurrentBucket) { + CurrentBucket = std::make_unique(); + } + + auto Width = Context.getTypeInfo(F->getType()).Width; + if (Width >= CACHE_LINE) { + std::unique_ptr OverSized = std::make_unique(); + OverSized->addField(F, Width); + FieldsOut.erase(Field); + Buckets.push_back(std::move(OverSized)); + continue; + } + + // If we can fit, add it. + if (CurrentBucket->canFit(Width)) { + CurrentBucket->addField(F, Width); + FieldsOut.erase(Field); + + // If it's now full, tie off the bucket. + if (CurrentBucket->full()) { + Skipped = 0; + Buckets.push_back(std::move(CurrentBucket)); + } + } else { + // We can't fit it in our current bucket. + // Move to the end for processing later. + ++Skipped; // Mark it skipped. + FieldsOut.push_back(F); + FieldsOut.erase(Field); + } + } + } + + // Done processing the fields awaiting a bucket. + + // If we were filling a bucket, tie it off. + if (CurrentBucket) { + Buckets.push_back(std::move(CurrentBucket)); + } + + // If we were processing a bitfield run bucket, tie it off. + if (CurrentBitfieldRun) { + Buckets.push_back(std::move(CurrentBitfieldRun)); + } + + std::shuffle(std::begin(Buckets), std::end(Buckets), Rng); + + // Produce the new ordering of the elements from our Buckets. + SmallVector FinalOrder; + for (auto &Bucket : Buckets) { + auto &Randomized = Bucket->fields(); + if (!Bucket->isBitfieldRun()) { + std::shuffle(std::begin(Randomized), std::end(Randomized), Rng); + } + FinalOrder.insert(FinalOrder.end(), Randomized.begin(), Randomized.end()); + } + + FieldsOut = FinalOrder; +} + +void Randstruct::commit(const RecordDecl *RD, + SmallVectorImpl &NewDeclOrder) const { + std::tie(RD->FirstDecl, RD->LastDecl) = + DeclContext::BuildDeclChain(NewDeclOrder, false); +} + +} // namespace randstruct +} // namespace clang diff --git a/clang/lib/AST/RecordLayoutBuilder.cpp b/clang/lib/AST/RecordLayoutBuilder.cpp --- a/clang/lib/AST/RecordLayoutBuilder.cpp +++ b/clang/lib/AST/RecordLayoutBuilder.cpp @@ -14,8 +14,9 @@ #include "clang/AST/DeclCXX.h" #include "clang/AST/DeclObjC.h" #include "clang/AST/Expr.h" -#include "clang/AST/VTableBuilder.h" +#include "clang/AST/Randstruct.h" #include "clang/AST/RecordLayout.h" +#include "clang/AST/VTableBuilder.h" #include "clang/Basic/TargetInfo.h" #include "llvm/ADT/SmallSet.h" #include "llvm/Support/Format.h" @@ -3285,6 +3286,10 @@ const ASTRecordLayout *Entry = ASTRecordLayouts[D]; if (Entry) return *Entry; + if (randstruct::shouldRandomize(*this, D)) { + randstruct::randomizeStructureLayout(*this, D); + } + const ASTRecordLayout *NewEntry = nullptr; if (isMsLayout(*this)) { diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -5870,6 +5870,16 @@ CmdArgs.push_back( Args.MakeArgString("-fmessage-length=" + Twine(MessageLength))); + if (Arg *A = Args.getLastArg(options::OPT_randstruct_seed_EQ)) { + CmdArgs.push_back("-randstruct-seed"); + CmdArgs.push_back(A->getValue(0)); + } + + if (Arg *A = Args.getLastArg(options::OPT_randstruct_seed_filename_EQ)) { + CmdArgs.push_back("-randstruct-seed-filename"); + CmdArgs.push_back(A->getValue(0)); + } + // -fvisibility= and -fvisibility-ms-compat are of a piece. if (const Arg *A = Args.getLastArg(options::OPT_fvisibility_EQ, options::OPT_fvisibility_ms_compat)) { diff --git a/clang/lib/Frontend/CompilerInvocation.cpp b/clang/lib/Frontend/CompilerInvocation.cpp --- a/clang/lib/Frontend/CompilerInvocation.cpp +++ b/clang/lib/Frontend/CompilerInvocation.cpp @@ -8,6 +8,7 @@ #include "clang/Frontend/CompilerInvocation.h" #include "TestModuleFileExtension.h" +#include "clang/AST/Randstruct.h" #include "clang/Basic/Builtins.h" #include "clang/Basic/CharInfo.h" #include "clang/Basic/CodeGenOptions.h" @@ -94,6 +95,7 @@ #include #include #include +#include #include #include #include @@ -2695,6 +2697,21 @@ Opts.ProgramAction = frontend::PluginAction; Opts.ActionName = A->getValue(); } + + if (const Arg *A = Args.getLastArg(OPT_randstruct_seed_filename_EQ)) { + std::string seed_filename = A->getValue(0); + std::ifstream seed_file(seed_filename.c_str()); + if (!seed_file.is_open()) { + Diags.Report(diag::err_drv_cannot_open_randstruct_seed_filename) + << A->getValue(0); + } + std::getline(seed_file, randstruct::SEED); + } + + if (const Arg *A = Args.getLastArg(OPT_randstruct_seed_EQ)) { + randstruct::SEED = A->getValue(0); + } + for (const auto *AA : Args.filtered(OPT_plugin_arg)) Opts.PluginArgs[AA->getValue(0)].emplace_back(AA->getValue(1)); diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -19,6 +19,7 @@ #include "clang/AST/ExprCXX.h" #include "clang/AST/ExprObjC.h" #include "clang/AST/ParentMap.h" +#include "clang/AST/Randstruct.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/AST/StmtCXX.h" #include "clang/AST/StmtObjC.h" @@ -2140,6 +2141,61 @@ } // namespace consumed } // namespace clang +//===----------------------------------------------------------------------===// +// Checking for bad casts from randomize structs +//===----------------------------------------------------------------------===// +namespace clang { +namespace randstruct { +namespace casts { + +class BadCastsASTWalk : public RecursiveASTVisitor { + Sema &S; + +public: + BadCastsASTWalk(Sema &S) : S(S){}; + + bool VisitCastExpr(const CastExpr *E) { + switch (E->getCastKind()) { + case CK_BitCast: { + const Expr *SubExpr = E->getSubExpr(); + if (auto ICE = dyn_cast(SubExpr)) { + CanQualType E_T = S.Context.getCanonicalType(E->getType()); + CanQualType ICE_T = S.Context.getCanonicalType(ICE->getType()); + if (isa(E_T) && isa(ICE_T)) { + auto E_SP = ((QualType)E_T)->getPointeeType(); + auto ICE_PT = ((QualType)ICE_T)->getPointeeType(); + + auto E_R = E_SP->getAsRecordDecl(); + auto ICE_R = ICE_PT->getAsRecordDecl(); + + if (E_R != nullptr && ICE_R != nullptr && E_R != ICE_R) { + if (ICE_R->isRandomized()) { + // The struct we are casting the pointer from was randomized. + S.Diag(E->getExprLoc(), diag::cast_from_randomized_struct) + << ICE_PT << ICE_R; + break; + } + } + } + } + break; + } + default: + break; + } + + return true; + } +}; + +void checkForBadCasts(Sema &S, AnalysisDeclContext &AC) { + BadCastsASTWalk walker(S); + walker.TraverseStmt(AC.getBody()); +} +} // namespace casts +} // namespace randstruct +} // namespace clang + //===----------------------------------------------------------------------===// // AnalysisBasedWarnings - Worker object used by Sema to execute analysis-based // warnings on a function, method, or block. @@ -2451,6 +2507,9 @@ ++NumFunctionsWithBadCFGs; } } + + // FIXME: Any way to get a handle to a RecordDecl struct here? + clang::randstruct::casts::checkForBadCasts(S, AC); } void clang::sema::AnalysisBasedWarnings::PrintStats() const { diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -8547,6 +8547,12 @@ case ParsedAttr::AT_Section: handleSectionAttr(S, D, AL); break; + case ParsedAttr::AT_RandomizeLayout: + handleSimpleAttribute(S, D, AL); + break; + case ParsedAttr::AT_NoRandomizeLayout: + handleSimpleAttribute(S, D, AL); + break; case ParsedAttr::AT_CodeSeg: handleCodeSegAttr(S, D, AL); break; diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test --- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test +++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test @@ -104,6 +104,7 @@ // CHECK-NEXT: NoMicroMips (SubjectMatchRule_function) // CHECK-NEXT: NoMips16 (SubjectMatchRule_function) // CHECK-NEXT: NoProfileFunction (SubjectMatchRule_function) +// CHECK-NEXT: NoRandomizeLayout (SubjectMatchRule_record) // CHECK-NEXT: NoSanitize (SubjectMatchRule_function, SubjectMatchRule_objc_method, SubjectMatchRule_variable_is_global) // CHECK-NEXT: NoSanitizeSpecific (SubjectMatchRule_function, SubjectMatchRule_variable_is_global) // CHECK-NEXT: NoSpeculativeLoadHardening (SubjectMatchRule_function, SubjectMatchRule_objc_method) @@ -148,6 +149,7 @@ // CHECK-NEXT: PassObjectSize (SubjectMatchRule_variable_is_parameter) // CHECK-NEXT: PatchableFunctionEntry (SubjectMatchRule_function, SubjectMatchRule_objc_method) // CHECK-NEXT: Pointer (SubjectMatchRule_record_not_is_union) +// CHECK-NEXT: RandomizeLayout (SubjectMatchRule_record) // CHECK-NEXT: ReleaseHandle (SubjectMatchRule_variable_is_parameter) // CHECK-NEXT: RenderScriptKernel (SubjectMatchRule_function) // CHECK-NEXT: ReqdWorkGroupSize (SubjectMatchRule_function) diff --git a/clang/unittests/AST/CMakeLists.txt b/clang/unittests/AST/CMakeLists.txt --- a/clang/unittests/AST/CMakeLists.txt +++ b/clang/unittests/AST/CMakeLists.txt @@ -25,6 +25,7 @@ EvaluateAsRValueTest.cpp ExternalASTSourceTest.cpp NamedDeclPrinterTest.cpp + RandstructTest.cpp RecursiveASTVisitorTest.cpp SizelessTypesTest.cpp SourceLocationTest.cpp diff --git a/clang/unittests/AST/RandstructTest.cpp b/clang/unittests/AST/RandstructTest.cpp new file mode 100644 --- /dev/null +++ b/clang/unittests/AST/RandstructTest.cpp @@ -0,0 +1,413 @@ +//===- unittest/AST/RandstructTest.cpp ------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file contains tests for Clang's structure field layout randomization. +// +//===----------------------------------------------------------------------===// + +/* + * Build this test suite by running `make ASTTests` in the build folder. + * + * Run this test suite by running the following in the build folder: + * ` ./tools/clang/unittests/AST/ASTTests + * --gtest_filter=StructureLayoutRandomization*` + */ + +#include "clang/AST/Randstruct.h" +#include "gtest/gtest.h" + +#include "DeclMatcher.h" +#include "clang/AST/RecordLayout.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Frontend/ASTUnit.h" +#include "clang/Testing/CommandLineArgs.h" +#include "clang/Tooling/Tooling.h" + +#include + +using namespace clang; +using namespace clang::randstruct; + +namespace clang { +namespace ast_matchers { + +static std::unique_ptr makeAST(const std::string &SourceCode, + TestLanguage Lang) { + const auto Args = getCommandLineArgsForTesting(Lang); + auto AST = tooling::buildASTFromCodeWithArgs(SourceCode, Args, "input.cc"); + return AST; +} + +static RecordDecl *getRecordDeclFromAST(const ASTContext &C, + const std::string &Name) { + return FirstDeclMatcher().match(C.getTranslationUnitDecl(), + recordDecl(hasName(Name))); +} + +static std::vector getFieldNamesFromRecord(const RecordDecl *RD) { + std::vector Fields; + Fields.reserve(8); + for (auto *Field : RD->fields()) + Fields.push_back(Field->getNameAsString()); + return Fields; +} + +static bool isSubsequence(const std::vector &Seq, + const std::vector &Subseq) { + const auto SeqLen = Seq.size(); + const auto SubLen = Subseq.size(); + + auto IsSubseq = false; + for (auto I = 0u; I < SeqLen; ++I) { + if (Seq[I] == Subseq[0]) { + IsSubseq = true; + for (auto J = 0u; J + I < SeqLen && J < SubLen; ++J) { + if (Seq[J + I] != Subseq[J]) { + IsSubseq = false; + break; + } + } + } + } + return IsSubseq; +} + +#define RANDSTRUCT_TEST_SUITE_TEST StructureLayoutRandomizationTestSuiteTest + +TEST(RANDSTRUCT_TEST_SUITE_TEST, CanDetermineIfSubsequenceExists) { + const std::vector S0 = {"a", "b", "c", "d"}; + ASSERT_TRUE(isSubsequence(S0, {"b", "c"})); + ASSERT_TRUE(isSubsequence(S0, {"a", "b", "c", "d"})); + ASSERT_TRUE(isSubsequence(S0, {"b", "c", "d"})); + ASSERT_TRUE(isSubsequence(S0, {"a"})); + ASSERT_FALSE(isSubsequence(S0, {"a", "d"})); +} + +#define RANDSTRUCT_TEST StructureLayoutRandomization + +TEST(RANDSTRUCT_TEST, UnmarkedStructuresAreNotRandomized) { + std::string Code = + R"( + struct dont_randomize_me { + int potato; + float tomato; + long cabbage; + }; + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = + getRecordDeclFromAST(AST->getASTContext(), "dont_randomize_me"); + const std::vector Expected = {"potato", "tomato", "cabbage"}; + const std::vector Actual = getFieldNamesFromRecord(RD); + + ASSERT_EQ(Expected, Actual); +} + +TEST(RANDSTRUCT_TEST, StructuresCanBeMarkedWithRandomizeLayoutAttr) { + std::string Code = + R"( + struct marked { + int bacon; + long lettuce; + } __attribute__((randomize_layout)); + + struct not_marked { + double cookies; + }; + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD0 = getRecordDeclFromAST(AST->getASTContext(), "marked"); + const auto *RD1 = getRecordDeclFromAST(AST->getASTContext(), "not_marked"); + + ASSERT_TRUE(RD0->getAttr()); + ASSERT_FALSE(RD1->getAttr()); +} + +TEST(RANDSTRUCT_TEST, StructuresCanBeMarkedWithNoRandomizeLayoutAttr) { + std::string Code = + R"( + struct marked { + int bacon; + long lettuce; + } __attribute__((no_randomize_layout)); + + struct not_marked { + double cookies; + }; + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD0 = getRecordDeclFromAST(AST->getASTContext(), "marked"); + const auto *RD1 = getRecordDeclFromAST(AST->getASTContext(), "not_marked"); + + ASSERT_TRUE(RD0->getAttr()); + ASSERT_FALSE(RD1->getAttr()); +} + +TEST(RANDSTRUCT_TEST, StructuresLayoutFieldLocationsCanBeRandomized) { + std::string Code = + R"( + struct test_struct { + int a; + int b; + int c; + int d; + int e; + int f; + }; + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + randomizeStructureLayout(AST->getASTContext(), RD); + const std::vector Before = {"a", "b", "c", "d", "e", "f"}; + const std::vector After = getFieldNamesFromRecord(RD); + + // FIXME: Could this be a brittle test? Planning on having a separate unit + // test for reproducible randomizations with seed. + ASSERT_NE(Before, After); +} + +TEST(RANDSTRUCT_TEST, + StructuresMarkedWithNoRandomizeLayoutShouldBeRejectedAndUnchanged) { + std::string Code = + R"( + struct test_struct { + int a; + int b; + int c; + int d; + int e; + int f; + } __attribute__((no_randomize_layout)); + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + + ASSERT_FALSE(randstruct::shouldRandomize(AST->getASTContext(), RD)); +} + +// FIXME: Clang trips an assertion in the DiagnosticsEngine when the warning is +// emitted while running under the test suite: +// clang/lib/Frontend/TextDiagnosticPrinter.cpp:150: virtual void +// clang::TextDiagnosticPrinter::HandleDiagnostic(clang::DiagnosticsEngine::Level, +// const clang::Diagnostic&): Assertion `TextDiag && "UnExpected diagnostic +// outside source file processing"' failed. +// +// Although the test *technically* is marked as pass; outside of the test suite +// this functionality works and no assertion is tripped. +TEST( + RANDSTRUCT_TEST, + DISABLED_EmitWarningWhenStructureIsMarkedWithBothRandomizeAndNoRandomizeAttributes) { + std::string Code = + R"( + struct test_struct { + int a; + int b; + int c; + } __attribute__((no_randomize_layout)) __attribute__((randomize_layout)); + )"; + + const auto AST = makeAST(Code, Lang_C99); + ASSERT_EQ(AST->getASTContext().getDiagnostics().getNumWarnings(), 1UL); +} + +TEST(RANDSTRUCT_TEST, + DISABLED_StructureMarkedWithBothAttributesRemainsUnchanged) { + std::string Code = + R"( + struct test_struct { + int a; + int b; + int c; + } __attribute__((no_randomize_layout)) __attribute__((randomize_layout)); + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + // FIXME getASTRecordLayout isn't the function we're testing here, it just + // happens to be the place where Randstruct makes a decision and then proceeds + // with that decision. I wonder if this is better as an integration test + // somehow. + static_cast(AST->getASTContext().getASTRecordLayout(RD)); + + const std::vector Expected = {"a", "b", "c"}; + const std::vector Actual = getFieldNamesFromRecord(RD); + ASSERT_EQ(Expected, Actual); +} + +// End of FIXME regarding DiagnosticsEngine assertion tests. + +TEST(RANDSTRUCT_TEST, AdjacentBitfieldsRemainAdjacentAfterRandomization) { + std::string Code = + R"( + struct test_struct { + int a; + int b; + int x : 1; + int y : 1; + int z : 1; + int c; + } __attribute__((randomize_layout)); + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + randomizeStructureLayout(AST->getASTContext(), RD); + + const std::vector Actual = getFieldNamesFromRecord(RD); + const std::vector Subseq = {"x", "y", "z"}; + ASSERT_TRUE(isSubsequence(Actual, Subseq)); +} + +TEST(RANDSTRUCT_TEST, VariableLengthArrayMemberRemainsAtEndOfStructure) { + std::string Code = + R"( + struct test_struct { + int a; + double b; + short c; + char name[]; + }; + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + randomizeStructureLayout(AST->getASTContext(), RD); + + std::vector Fields = getFieldNamesFromRecord(RD); + const auto VLA = std::find(Fields.begin(), Fields.end(), "name"); + ASSERT_TRUE(VLA + 1 == Fields.end()); +} + +TEST(RANDSTRUCT_TEST, RandstructDoesNotOverrideThePackedAttr) { + std::string Code = + R"( + struct test_struct { + char a; + short b; + int c; + } __attribute__((packed, randomize_layout)); + + struct another_struct { + char a; + int c; + } __attribute__((packed, randomize_layout)); + + struct last_struct { + char a; + long long b; + } __attribute__((packed, randomize_layout)); + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + // FIXME (?): calling getASTRecordLayout is probably a necessary evil so that + // Clang's RecordBuilders can actually flesh out the information like + // alignment, etc. + const auto &Layout = AST->getASTContext().getASTRecordLayout(RD); + + ASSERT_EQ(7, Layout.getSize().getQuantity()); + + const auto *RD1 = + getRecordDeclFromAST(AST->getASTContext(), "another_struct"); + const auto &Layout1 = AST->getASTContext().getASTRecordLayout(RD1); + + ASSERT_EQ(5, Layout1.getSize().getQuantity()); + + const auto *RD2 = getRecordDeclFromAST(AST->getASTContext(), "last_struct"); + const auto &Layout2 = AST->getASTContext().getASTRecordLayout(RD2); + + ASSERT_EQ(9, Layout2.getSize().getQuantity()); +} + +TEST(RANDSTRUCT_TEST, ZeroWidthBitfieldsSeparateAllocationUnits) { + std::string Code = + R"( + struct test_struct { + int a : 1; + int : 0; + int b : 1; + }; + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + const std::vector Before = getFieldNamesFromRecord(RD); + ASSERT_TRUE(isSubsequence(Before, {"a", "", "b"})); + + randomizeStructureLayout(AST->getASTContext(), RD); + const std::vector After = getFieldNamesFromRecord(RD); + ASSERT_FALSE(isSubsequence(After, {"a", "", "b"})); +} + +TEST(RANDSTRUCT_TEST, RandstructDoesNotRandomizeUnionFieldOrder) { + std::string Code = + R"( + union test_union { + int a; + int b; + int c; + int d; + int e; + int f; + int g; + } __attribute__((randomize_layout)); + )"; + + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_union"); + ASSERT_FALSE(shouldRandomize(AST->getASTContext(), RD)); +} + +TEST(RANDSTRUCT_TEST, AnonymousStructsAndUnionsRetainFieldOrder) { + std::string Code = + R"( + struct test_struct { + int a; + struct { + int b; + int c; + int d; + }; + int e; + union { + int f; + int h; + int j; + }; + int k; + } __attribute__((randomize_layout)); + )"; + const auto AST = makeAST(Code, Lang_C99); + const auto *RD = getRecordDeclFromAST(AST->getASTContext(), "test_struct"); + + randomizeStructureLayout(AST->getASTContext(), RD); + + bool AnonStructTested = false; + bool AnonUnionTested = false; + for (auto f : RD->decls()) { + if (auto *SubRD = dyn_cast(f)) + if (SubRD->isAnonymousStructOrUnion()) { + if (isSubsequence(getFieldNamesFromRecord(SubRD), {"b", "c", "d"})) { + AnonStructTested = true; + } + if (isSubsequence(getFieldNamesFromRecord(SubRD), {"f", "h", "j"})) { + AnonUnionTested = true; + } + } + } + ASSERT_TRUE(AnonStructTested); + ASSERT_TRUE(AnonUnionTested); +} + +} // namespace ast_matchers +} // namespace clang