diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..92ccf1c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,75 @@ +# CodeQL Queries Repository + +## CI Checks + +CI runs two steps (see `.github/workflows/test.yml`): + +1. `make format-check` — verifies all `.ql` and `.qll` files are properly formatted +2. `make test` — runs all CodeQL tests across all languages + +After editing any `.ql` or `.qll` file, run format check: +```sh +codeql query format --check-only # check only +codeql query format --in-place # auto-fix +``` + +Or check/fix all files at once: +```sh +make format-check # check all +make format # fix all +``` + +After significant changes (new queries, modified library logic, changed tests), run the full test suite: +```sh +make test +``` + +To run tests for a single query: +```sh +codeql test run cpp/test/query-tests/security/IteratorInvalidation/ +``` + +## C/C++ Test Stubs + +Tests cannot use real system headers. Minimal stub headers live in `cpp/test/include/` organized by library: +- `libc/` — C standard library stubs (signal.h, stdlib.h, string_stubs.h, etc.) +- `stl/` — C++ STL stubs (vector.h, deque.h, unordered_set.h) +- `openssl/` — OpenSSL stubs (evp.h, bn.h, rand.h, etc.) +- `mbedtls/` — mbed TLS stubs (bignum.h) + +Stubs use a `USE_HEADERS` guard pattern to optionally fall back to real headers: +```c +#ifndef USE_HEADERS +// ... stub definitions ... +#else +#include +#endif +``` + +Test `.cpp` files include stubs via relative paths: +```cpp +#include "../../../include/stl/vector.h" +``` + +Stubs only need enough declarations for CodeQL to resolve types and function names — no implementations required. + +## Updating README Query Tables + +When a query is added, removed, or its metadata changes, regenerate the README tables: +```sh +python ./scripts/queries_table_generator.py 2>/dev/null +``` + +This reads query metadata from all "full" suites and outputs markdown tables. Copy-paste the output into `README.md` under the `## Queries` section. + +## Qlpack Versioning + +Each language has a library pack (`/lib/qlpack.yml`) and a queries pack (`/src/qlpack.yml`) with a `version:` field. Test packs have no version. + +Bump versions when adding new queries or libraries, removing queries, or making breaking changes to library APIs. Keep the library and queries pack versions in sync for the same language. + +Packs per language: +- `trailofbits/cpp-all` (library) — `cpp/lib/qlpack.yml` +- `trailofbits/cpp-queries` — `cpp/src/qlpack.yml` +- `trailofbits/go-queries` — `go/src/qlpack.yml` +- `trailofbits/java-queries` — `java/src/qlpack.yml` diff --git a/README.md b/README.md index 8325fcd..8488b85 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ codeql database analyze database.db --format=sarif-latest --output=./tob.sarif - |[Find all problematic implicit casts](./cpp/src/docs/security/UnsafeImplicitConversions/UnsafeImplicitConversions.md)|Find all implicit casts that may be problematic. That is, casts that may result in unexpected truncation, reinterpretation or widening of values.|error|high| |[Inconsistent handling of return values from a specific function](./cpp/src/docs/security/InconsistentReturnValueHandling/InconsistentReturnValueHandling.md)|Detects functions whose return values are compared inconsistently across call sites, which may indicate bugs|warning|medium| |[Invalid string size passed to string manipulation function](./cpp/src/docs/security/CStrnFinder/CStrnFinder.md)|Finds calls to functions that take as input a string and its size as separate arguments (e.g., `strncmp`, `strncat`, ...) and the size argument is wrong|error|low| +|[Iterator invalidation](./cpp/src/docs/security/IteratorInvalidation/IteratorInvalidation.md)|Modifying a container while iterating over it can invalidate iterators, leading to undefined behavior.|warning|medium| |[Missing null terminator](./cpp/src/docs/security/NoNullTerminator/NoNullTerminator.md)|This query finds incorrectly initialized strings that are passed to functions expecting null-byte-terminated strings|error|high| ### Go diff --git a/cpp/lib/qlpack.yml b/cpp/lib/qlpack.yml index e70ab5b..ef8dd2f 100644 --- a/cpp/lib/qlpack.yml +++ b/cpp/lib/qlpack.yml @@ -1,7 +1,7 @@ --- name: trailofbits/cpp-all authors: Trail of Bits -version: 0.2.1 +version: 0.3.0 license: AGPL library: true extractor: cpp diff --git a/cpp/lib/trailofbits/itergator/DataFlow.qll b/cpp/lib/trailofbits/itergator/DataFlow.qll new file mode 100644 index 0000000..679b99f --- /dev/null +++ b/cpp/lib/trailofbits/itergator/DataFlow.qll @@ -0,0 +1,62 @@ +private import cpp +private import trailofbits.itergator.Iterators +import semmle.code.cpp.dataflow.new.DataFlow + +private module IteratorFlowConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.asExpr() instanceof Access or + exists(source.asParameter()) + } + + predicate isSink(DataFlow::Node sink) { sink.asExpr().(Access).getTarget() instanceof Iterator } + + predicate isBarrier(DataFlow::Node node) { + node.asExpr().(FunctionCall).getTarget() instanceof CopyConstructor + } +} + +module IteratorFlow = DataFlow::Global; + +private module IteratedFlowConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.asExpr() instanceof Access or + exists(source.asParameter()) + } + + predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof Iterated } + + predicate isBarrier(DataFlow::Node node) { + node.asExpr().(FunctionCall).getTarget() instanceof CopyConstructor + } +} + +module IteratedFlow = DataFlow::Global; + +private module InvalidationFlowConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + exists(Access a | a = source.asExpr()) or + exists(source.asParameter()) + } + + predicate isSink(DataFlow::Node sink) { exists(Invalidation i | sink.asExpr() = i.getAChild()) } + + predicate isBarrier(DataFlow::Node node) { + node.asExpr().(FunctionCall).getTarget() instanceof CopyConstructor + } +} + +module InvalidationFlow = DataFlow::Global; + +private module InvalidatorFlowConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { exists(source) } + + predicate isSink(DataFlow::Node sink) { + sink.asExpr().getEnclosingElement() instanceof Invalidator + } + + predicate isBarrier(DataFlow::Node node) { + node.asExpr().(FunctionCall).getTarget() instanceof CopyConstructor + } +} + +module InvalidatorFlow = DataFlow::Global; diff --git a/cpp/lib/trailofbits/itergator/Invalidations.qll b/cpp/lib/trailofbits/itergator/Invalidations.qll new file mode 100644 index 0000000..086c0e1 --- /dev/null +++ b/cpp/lib/trailofbits/itergator/Invalidations.qll @@ -0,0 +1,12 @@ +private import cpp +private import trailofbits.itergator.Iterators + +abstract class PotentialInvalidation extends Function { + cached + abstract predicate invalidates(Iterated i); + + Expr invalidatedChild(Invalidation invd) { + // by default, invalidates object method is called on + result = invd.getQualifier() + } +} diff --git a/cpp/lib/trailofbits/itergator/Iterators.qll b/cpp/lib/trailofbits/itergator/Iterators.qll new file mode 100644 index 0000000..52c8ad3 --- /dev/null +++ b/cpp/lib/trailofbits/itergator/Iterators.qll @@ -0,0 +1,111 @@ +private import cpp +private import trailofbits.itergator.Invalidations + +class Iterator extends Variable { + Iterator() { + this.getUnderlyingType().getName().matches("%iterator%") or + // getType is inconsistent + this.getAnAssignedValue() + .(FunctionCall) + .getTarget() + .(MemberFunction) + .getName() + .regexpMatch("c?r?begin") or + this.getAnAssignedValue() + .(FunctionCall) + .getTarget() + .(MemberFunction) + .getName() + .regexpMatch("c?r?end") or + this.getAnAssignedValue().(FunctionCall).getTarget().hasName("find") + } +} + +// the location where a variable is being iterated over +class Iterated extends VariableAccess { + Iterator iterator; + + Iterated() { + iterator.getAnAssignedValue().getChild(-1) = this and + not this.getTarget().isCompilerGenerated() + or + // show the iterable assigned to __range in ranged based for loops + iterator.getAnAssignedValue().getChild(-1).(VariableAccess).getTarget().isCompilerGenerated() and + this = + iterator.getAnAssignedValue().getChild(-1).(VariableAccess).getTarget().getAnAssignedValue() + } + + Iterator iterator() { result = iterator } +} + +// function call with utility predicates +private class FunctionCallR extends FunctionCall { + predicate containedBy(Stmt other) { + other.getASuccessor*() = this and + other.getAChild*() = this + or + // for destructors + exists(Function f | f.getBlock() = other and this.getEnclosingFunction() = f) + } + + predicate callsPotentialInvalidation() { + this.getTarget().(PotentialInvalidation).invalidates(any(Iterated i)) + } + + predicate callsPotentialInvalidation(Iterated i) { + this.getTarget().(PotentialInvalidation).invalidates(i) + } +} + +// a call to any function that could call a PotentialInvalidation +class InvalidatorT extends FunctionCallR { + InvalidatorT() { + this.callsPotentialInvalidation() + or + exists(InvalidatorT i | i.containedBy(this.getTarget().getBlock())) + } + + InvalidatorT child() { + result = this.getTarget().getBlock().getAChild+().(InvalidatorT) + or + exists(DestructorCall d | + d.getEnclosingFunction() = this.getTarget() and d.(InvalidatorT) = result + ) + } + + Iterated iterated_() { + this.callsPotentialInvalidation(result) or + result = this.child().iterated_() + } + + InvalidatorT potentialInvalidation() { + this.callsPotentialInvalidation(this.iterated_()) and result = this + or + result = this.child().potentialInvalidation() + } +} + +// calls that actually perform the invalidation +class Invalidation extends InvalidatorT { + Invalidator invalidator; + + Invalidation() { + this = invalidator.potentialInvalidation() and invalidator.iterated() = this.iterated_() + } + + Invalidator invalidator() { result = invalidator } +} + +// the top level invalidation calls (directly inside loop bodies) +class Invalidator extends InvalidatorT { + Iterated iterated; + + Invalidator() { + iterated = this.iterated_() and + this.containedBy(iterated.iterator().getParentScope()) + } + + Iterated iterated() { result = iterated } + + Invalidation invalidation() { result = any(Invalidation i | i.invalidator() = this) } +} diff --git a/cpp/lib/trailofbits/itergator/invalidations/Destructor.qll b/cpp/lib/trailofbits/itergator/invalidations/Destructor.qll new file mode 100644 index 0000000..7464df1 --- /dev/null +++ b/cpp/lib/trailofbits/itergator/invalidations/Destructor.qll @@ -0,0 +1,11 @@ +private import trailofbits.itergator.Iterators +private import cpp +import trailofbits.itergator.Invalidations + +class PotentialInvalidationDestructor extends PotentialInvalidation { + PotentialInvalidationDestructor() { + this instanceof MemberFunction and this.getName().matches("~%") + } + + override predicate invalidates(Iterated i) { i.getType().refersTo(this.getParentScope()) } +} diff --git a/cpp/lib/trailofbits/itergator/invalidations/STL.qll b/cpp/lib/trailofbits/itergator/invalidations/STL.qll new file mode 100644 index 0000000..dd0bea6 --- /dev/null +++ b/cpp/lib/trailofbits/itergator/invalidations/STL.qll @@ -0,0 +1,64 @@ +private import trailofbits.itergator.Iterators +private import cpp +import trailofbits.itergator.Invalidations + +private string typeName(Iterated i) { result = i.getTarget().getType().stripType().getName() } + +class PotentialInvalidationSTL extends PotentialInvalidation { + PotentialInvalidationSTL() { this instanceof MemberFunction } + + override Expr invalidatedChild(Invalidation invd) { + result = super.invalidatedChild(invd) + or + // swap can also invalidate the first argument + this.hasName("swap") and result = invd.getArgument(0) + } + + override predicate invalidates(Iterated i) { + i.getType().refersTo(this.getParentScope()) and + ( + typeName(i).matches("vector<%") and this.vectorInvalidation() + or + typeName(i).matches("deque<%") and this.dequeInvalidation() + or + typeName(i).regexpMatch("unordered_(set|multiset)<.*") and this.setInvalidation() + ) + } + + predicate vectorInvalidation() { + this.hasName("push_back") or + this.hasName("reserve") or + this.hasName("insert") or + this.hasName("emplace_back") or + this.hasName("emplace") or + this.hasName("erase") or + this.hasName("pop_back") or + this.hasName("resize") or + this.hasName("shrink_to_fit") or + this.hasName("clear") or + this.hasName("swap") + } + + predicate dequeInvalidation() { + this.hasName("push_back") or + this.hasName("push_front") or + this.hasName("pop_back") or + this.hasName("pop_front") or + this.hasName("insert") or + this.hasName("erase") or + this.hasName("emplace") or + this.hasName("emplace_front") or + this.hasName("emplace_back") or + this.hasName("resize") or + this.hasName("clear") or + this.hasName("shrink_to_fit") or + this.hasName("swap") + } + + predicate setInvalidation() { + this.hasName("emplace") or + this.hasName("emplace_hint") or + this.hasName("insert") or + this.hasName("clear") + } +} diff --git a/cpp/src/docs/security/IteratorInvalidation/IteratorInvalidation.md b/cpp/src/docs/security/IteratorInvalidation/IteratorInvalidation.md new file mode 100644 index 0000000..dcb45b7 --- /dev/null +++ b/cpp/src/docs/security/IteratorInvalidation/IteratorInvalidation.md @@ -0,0 +1,51 @@ +# Iterator invalidation +This is a CodeQL query that detects modifications to C++ containers while iterating over them. Such modifications can invalidate active iterators, leading to undefined behavior including use-after-free, out-of-bounds access, and crashes. + +When a container is modified (e.g., calling `push_back` on a `std::vector` during a range-based for loop), existing iterators may become invalid. Continuing to use those iterators results in undefined behavior per the C++ Standard. + +The query tracks data flow between the iterated container and the modifying call to ensure they operate on the same object. + + +## Recommendation +Avoid modifying containers while iterating over them. Common safe alternatives include: + +- Collect modifications and apply them after the loop completes +- Use the erase-remove idiom for deletions +- Iterate over a copy of the container if modification is necessary +- Use index-based loops instead of iterator-based loops when the container supports random access + + +## Example + +```cpp +#include + +// BAD: push_back during range-based for loop invalidates iterators +void bad_example(std::vector& vec) { + for (auto& elem : vec) { + if (elem < 0) { + vec.push_back(-elem); // invalidates iterators + } + } +} + +// GOOD: collect modifications, apply after loop +void good_example(std::vector& vec) { + std::vector to_add; + for (auto& elem : vec) { + if (elem < 0) { + to_add.push_back(-elem); + } + } + for (auto& val : to_add) { + vec.push_back(val); + } +} +``` +In the bad example, calling `push_back` inside the range-based for loop may cause the vector to reallocate, invalidating all iterators including the loop's internal iterator. The good example collects values to add and applies them after the loop. + + +## References +* [C++ Standard \[container.requirements.general\]](https://eel.is/c++draft/container.requirements.general) — iterator invalidation rules per container type +* [CWE-416: Use After Free](https://cwe.mitre.org/data/definitions/416.html) +* [Trail of Bits — Itergator: Iterator Invalidation Detector](https://github.com/trailofbits/itergator) diff --git a/cpp/src/qlpack.yml b/cpp/src/qlpack.yml index 792211a..ed95979 100644 --- a/cpp/src/qlpack.yml +++ b/cpp/src/qlpack.yml @@ -2,7 +2,7 @@ name: trailofbits/cpp-queries description: CodeQL queries for C and C++ developed by Trail of Bits authors: Trail of Bits -version: 0.2.1 +version: 0.3.0 license: AGPL library: false extractor: cpp diff --git a/cpp/src/security/IteratorInvalidation/IteratorInvalidation.ql b/cpp/src/security/IteratorInvalidation/IteratorInvalidation.ql new file mode 100644 index 0000000..d010ce5 --- /dev/null +++ b/cpp/src/security/IteratorInvalidation/IteratorInvalidation.ql @@ -0,0 +1,82 @@ +/** + * @name Iterator invalidation + * @id tob/cpp/iterator-invalidation + * @description Modifying a container while iterating over it can invalidate + * iterators, leading to undefined behavior. + * @kind problem + * @tags security correctness + * @problem.severity warning + * @precision medium + * @security-severity 7.5 + * @group security + */ + +import cpp +import trailofbits.itergator.Iterators +import trailofbits.itergator.DataFlow +import trailofbits.itergator.invalidations.STL +import trailofbits.itergator.invalidations.Destructor + +class NotStackVariable extends Variable { + NotStackVariable() { not this instanceof StackVariable } +} + +Variable nodeToVariable(DataFlow::Node node) { result = node.asExpr().(VariableAccess).getTarget() } + +predicate falsePositive(Iterator it, Invalidator inv) { + forex(ControlFlowNode n | n = inv.getASuccessor() | + n.(BreakStmt).getBreakable().(Loop) = it.getParentScope() + or + n.(ReturnStmt).getEnclosingFunction() = it.getParentScope+() + or + exists(ExitBasicBlock b | + b = n.getBasicBlock() and + b.getEnclosingFunction() = it.getParentScope+() and + not b.contains(it.getAnAccess()) + ) + ) + or + inv = it.getAnAssignedValue() +} + +predicate invalidatesChild(Invalidation invd, Expr container) { + invd.getTarget().(PotentialInvalidation).invalidatedChild(invd) = container +} + +from + int significance, DataFlow::Node source, DataFlow::Node invalidationNode, + DataFlow::Node iteratedNode, DataFlow::Node invalidatorNode, Invalidator inv, Iterated itd, + Iterator it, Invalidation invd +where + itd = iteratedNode.asExpr() and + itd = inv.iterated() and + it = itd.iterator() and + inv = invalidatorNode.asExpr().getEnclosingElement() and + invalidatesChild(inv.invalidation(), invalidationNode.asExpr()) and + invd = invalidationNode.asExpr().getEnclosingElement() and + // make sure the actions can operate on the same values + ( + // the same value flows to the iterator, the invalidator, and the invalidation + IteratedFlow::flow(source, iteratedNode) and + InvalidatorFlow::flow(source, invalidatorNode) and + InvalidationFlow::flow(source, invalidationNode) and + InvalidationFlow::flow(invalidatorNode, invalidationNode) and + significance = 0 + or + // or some access of the iterated variable flows to the invalidation + exists(DataFlow::Node source2 | + IteratedFlow::flow(source, iteratedNode) and + InvalidationFlow::flow(source2, invalidationNode) and + // stack variables should have sequential flow (caught by above) + nodeToVariable(source).(NotStackVariable) = nodeToVariable(source2).(NotStackVariable) and + ( + nodeToVariable(source) instanceof GlobalOrNamespaceVariable and significance = 1 + or + not nodeToVariable(source) instanceof GlobalOrNamespaceVariable and significance = 2 + ) + ) + ) and + not falsePositive(it, inv) +select itd, + "Iterating over this container may cause undefined behavior: $@ can invalidate active iterators.", + inv, inv.toString() diff --git a/cpp/test/include/stl/deque.h b/cpp/test/include/stl/deque.h new file mode 100644 index 0000000..ad59eb8 --- /dev/null +++ b/cpp/test/include/stl/deque.h @@ -0,0 +1,73 @@ +#ifndef USE_HEADERS + +#ifndef HEADER_DEQUE_STUB_H +#define HEADER_DEQUE_STUB_H + +namespace std { + +template +class deque { +public: + typedef T value_type; + typedef T& reference; + typedef const T& const_reference; + typedef unsigned long size_type; + + struct iterator { + T* ptr; + iterator() : ptr(0) {} + iterator& operator++() { return *this; } + iterator operator++(int) { return *this; } + T& operator*() { return *ptr; } + T* operator->() { return ptr; } + bool operator!=(const iterator& other) const { return ptr != other.ptr; } + bool operator==(const iterator& other) const { return ptr == other.ptr; } + }; + + struct const_iterator { + const T* ptr; + const_iterator() : ptr(0) {} + const_iterator& operator++() { return *this; } + const_iterator operator++(int) { return *this; } + const T& operator*() const { return *ptr; } + bool operator!=(const const_iterator& other) const { return ptr != other.ptr; } + bool operator==(const const_iterator& other) const { return ptr == other.ptr; } + }; + + deque(); + ~deque(); + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + + size_type size() const; + + void push_back(const T& value); + void push_front(const T& value); + void pop_back(); + void pop_front(); + iterator insert(iterator pos, const T& value); + iterator erase(iterator pos); + iterator emplace(iterator pos); + void emplace_front(); + void emplace_back(); + void resize(size_type count); + void clear(); + void shrink_to_fit(); + void swap(deque& other); + + reference operator[](size_type pos); + const_reference operator[](size_type pos) const; +}; + +} // namespace std + +#endif + +#else // --- else USE_HEADERS + +#include + +#endif // --- end USE_HEADERS diff --git a/cpp/test/include/stl/unordered_set.h b/cpp/test/include/stl/unordered_set.h new file mode 100644 index 0000000..0c7664c --- /dev/null +++ b/cpp/test/include/stl/unordered_set.h @@ -0,0 +1,85 @@ +#ifndef USE_HEADERS + +#ifndef HEADER_UNORDERED_SET_STUB_H +#define HEADER_UNORDERED_SET_STUB_H + +namespace std { + +template +class unordered_set { +public: + typedef T value_type; + typedef const T& const_reference; + typedef unsigned long size_type; + + struct iterator { + const T* ptr; + iterator() : ptr(0) {} + iterator& operator++() { return *this; } + iterator operator++(int) { return *this; } + const T& operator*() const { return *ptr; } + bool operator!=(const iterator& other) const { return ptr != other.ptr; } + bool operator==(const iterator& other) const { return ptr == other.ptr; } + }; + + typedef iterator const_iterator; + + unordered_set(); + ~unordered_set(); + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + + size_type size() const; + + iterator insert(const T& value); + iterator emplace(); + iterator emplace_hint(const_iterator hint); + void clear(); + iterator find(const T& value); + iterator erase(iterator pos); +}; + +template +class unordered_multiset { +public: + typedef T value_type; + typedef const T& const_reference; + typedef unsigned long size_type; + + struct iterator { + const T* ptr; + iterator() : ptr(0) {} + iterator& operator++() { return *this; } + iterator operator++(int) { return *this; } + const T& operator*() const { return *ptr; } + bool operator!=(const iterator& other) const { return ptr != other.ptr; } + bool operator==(const iterator& other) const { return ptr == other.ptr; } + }; + + typedef iterator const_iterator; + + unordered_multiset(); + ~unordered_multiset(); + + iterator begin(); + iterator end(); + + iterator insert(const T& value); + iterator emplace(); + iterator emplace_hint(const_iterator hint); + void clear(); + iterator find(const T& value); +}; + +} // namespace std + +#endif + +#else // --- else USE_HEADERS + +#include + +#endif // --- end USE_HEADERS diff --git a/cpp/test/include/stl/vector.h b/cpp/test/include/stl/vector.h new file mode 100644 index 0000000..94b6428 --- /dev/null +++ b/cpp/test/include/stl/vector.h @@ -0,0 +1,71 @@ +#ifndef USE_HEADERS + +#ifndef HEADER_VECTOR_STUB_H +#define HEADER_VECTOR_STUB_H + +namespace std { + +template +class vector { +public: + typedef T value_type; + typedef T& reference; + typedef const T& const_reference; + typedef unsigned long size_type; + + struct iterator { + T* ptr; + iterator() : ptr(0) {} + iterator& operator++() { return *this; } + iterator operator++(int) { return *this; } + T& operator*() { return *ptr; } + T* operator->() { return ptr; } + bool operator!=(const iterator& other) const { return ptr != other.ptr; } + bool operator==(const iterator& other) const { return ptr == other.ptr; } + }; + + struct const_iterator { + const T* ptr; + const_iterator() : ptr(0) {} + const_iterator& operator++() { return *this; } + const_iterator operator++(int) { return *this; } + const T& operator*() const { return *ptr; } + bool operator!=(const const_iterator& other) const { return ptr != other.ptr; } + bool operator==(const const_iterator& other) const { return ptr == other.ptr; } + }; + + vector(); + ~vector(); + + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; + + size_type size() const; + + void push_back(const T& value); + void emplace_back(); + iterator insert(iterator pos, const T& value); + iterator emplace(iterator pos); + iterator erase(iterator pos); + void pop_back(); + void clear(); + void resize(size_type count); + void reserve(size_type new_cap); + void shrink_to_fit(); + void swap(vector& other); + + reference operator[](size_type pos); + const_reference operator[](size_type pos) const; +}; + +} // namespace std + +#endif + +#else // --- else USE_HEADERS + +#include + +#endif // --- end USE_HEADERS diff --git a/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.cpp b/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.cpp new file mode 100644 index 0000000..7f27cf0 --- /dev/null +++ b/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.cpp @@ -0,0 +1,94 @@ +#include "../../../include/stl/vector.h" +#include "../../../include/stl/deque.h" +#include "../../../include/stl/unordered_set.h" + +// BAD: push_back during range-based for loop on vector +void bad_vector_push_back(std::vector& vec) { + for (auto& elem : vec) { // iterated here + if (elem < 0) { + vec.push_back(-elem); // invalidates iterators + } + } +} + +// BAD: erase during iterator-based while loop on vector +void bad_vector_erase(std::vector& vec) { + std::vector::iterator it = vec.begin(); + while (it != vec.end()) { + if (*it < 0) { + vec.erase(it); // invalidates iterators, should use it = vec.erase(it) + } + ++it; + } +} + +// BAD: insert during iteration on deque +void bad_deque_insert(std::deque& dq) { + for (auto& elem : dq) { // iterated here + if (elem > 100) { + dq.insert(dq.begin(), 0); // invalidates iterators + } + } +} + +// BAD: clear during iteration on unordered_set +void bad_set_clear(std::unordered_set& s) { + for (auto& elem : s) { // iterated here + if (elem == 42) { + s.clear(); // invalidates iterators + } + } +} + +// Helper that indirectly invalidates the vector +void indirect_push(std::vector& v) { + v.push_back(999); +} + +// BAD: indirect invalidation through function call +void bad_indirect_invalidation(std::vector& vec) { + for (auto& elem : vec) { // iterated here + if (elem == 0) { + indirect_push(vec); // indirectly invalidates iterators + } + } +} + +// GOOD: break immediately after invalidating call +void good_break_after_invalidation(std::vector& vec) { + for (auto& elem : vec) { + if (elem < 0) { + vec.push_back(-elem); + break; // loop exits, no further iterator use + } + } +} + +// GOOD: return immediately after invalidating call +void good_return_after_invalidation(std::vector& vec) { + for (auto& elem : vec) { + if (elem < 0) { + vec.push_back(-elem); + return; // function exits, no further iterator use + } + } +} + +// GOOD: iterating over a copy of the container +void good_iterate_copy(std::vector& vec) { + std::vector copy = vec; + for (auto& elem : copy) { + if (elem < 0) { + vec.push_back(-elem); // modifies original, not the copy being iterated + } + } +} + +// GOOD: index-based loop (no iterator involved) +void good_index_based(std::vector& vec) { + for (unsigned long i = 0; i < vec.size(); i++) { + if (vec[i] < 0) { + vec.push_back(-vec[i]); // no iterator to invalidate + } + } +} diff --git a/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.expected b/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.expected new file mode 100644 index 0000000..81fa1c5 --- /dev/null +++ b/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.expected @@ -0,0 +1,5 @@ +| IteratorInvalidation.cpp:7:23:7:25 | vec | Iterating over this container may cause undefined behavior: $@ can invalidate active iterators. | IteratorInvalidation.cpp:9:17:9:25 | call to push_back | call to push_back | +| IteratorInvalidation.cpp:16:37:16:39 | vec | Iterating over this container may cause undefined behavior: $@ can invalidate active iterators. | IteratorInvalidation.cpp:19:17:19:21 | call to erase | call to erase | +| IteratorInvalidation.cpp:27:23:27:24 | dq | Iterating over this container may cause undefined behavior: $@ can invalidate active iterators. | IteratorInvalidation.cpp:29:16:29:21 | call to insert | call to insert | +| IteratorInvalidation.cpp:36:23:36:23 | s | Iterating over this container may cause undefined behavior: $@ can invalidate active iterators. | IteratorInvalidation.cpp:38:15:38:19 | call to clear | call to clear | +| IteratorInvalidation.cpp:50:23:50:25 | vec | Iterating over this container may cause undefined behavior: $@ can invalidate active iterators. | IteratorInvalidation.cpp:52:13:52:25 | call to indirect_push | call to indirect_push | diff --git a/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.qlref b/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.qlref new file mode 100644 index 0000000..2fee379 --- /dev/null +++ b/cpp/test/query-tests/security/IteratorInvalidation/IteratorInvalidation.qlref @@ -0,0 +1 @@ +security/IteratorInvalidation/IteratorInvalidation.ql