CERT.EXPR.MOVED_FROM.RANGE
Do not access elements in the moved-from range produced by an erase-remove algorithm
The CERT.EXPR.MOVED_FROM.RANGE checker implements CERT C++ rule EXP63-CPP. It detects uses of container elements in the range returned by algorithms such as std::remove, std::remove_if, and std::unique. These algorithms do not remove elements from the container; instead, they move retained elements to the front and leave the remaining elements in a valid but unspecified state. Accessing elements in this trailing range can result in the use of unspecified values.
This checker complements CERT.EXPR.MOVED_FROM.USE, which detects uses of objects after explicit move operations, whereas CERT.EXPR.MOVED_FROM.RANGE focuses on accesses to the unspecified-value range produced by algorithms such as std::remove() and std::unique().
Vulnerability and risk
After std::remove(first, last, val) returns new_last, the elements in [new_last, last) are in a moved-from state. Iterating past new_last exposes the caller to any value the implementation happened to leave behind, typically a duplicate of an earlier element, but the standard does not guarantee this. The container's size and end iterator are unchanged, so range-based for loops and index-bounded size() loops silently read the unspecified tail. In pipelines that chain remove/unique with printing, hashing, or serialization, corrupt values propagate downstream without any warning at the source site.
Mitigation and prevention
Always consume the iterator returned by the algorithm. Either bound subsequent iteration to that iterator (for (auto i = c.begin(); i != new_last; ++i)), or apply the erase-remove idiom (c.erase(std::remove(c.begin(), c.end(), v), c.end())) so the moved-from tail is physically removed before any further access. When a helper function performs the algorithm, either erase inside the helper or return the boundary iterator so the caller can complete the erase-remove pattern.
Vulnerable code example 1: return value ignored, full iteration
#include <algorithm>
#include <iostream>
#include <vector>
void case1_remove_full_iteration(std::vector<int> &c) {
std::remove(c.begin(), c.end(), 42); // source: return value ignored
for (auto v : c) { // CERT.EXPR.MOVED_FROM.RANGE
std::cout << v << std::endl; // reads into [new_last, last)
}
}
std::remove shifts non-matching elements to the front and returns the new logical end, but the container size is unchanged. Iterating the whole range with for (auto v : c) reads every element up to c.end(), including the moved-from tail.
Vulnerable code example 2: c.end()-bounded index loop
#include <algorithm>
#include <iostream>
#include <vector>
void case4_walk_to_end(std::vector<int> &c) {
auto e = std::remove(c.begin(), c.end(), 0);
(void)e; // boundary discarded
for (auto it = c.begin(); it != c.end(); ++it) { // CERT.EXPR.MOVED_FROM.RANGE
std::cout << *it << std::endl; // deref into [e, end())
}
}
Capturing the returned iterator into e is not enough. If the loop still iterates up to c.end() and e is never used, the loop dereferences the moved-from tail.
Vulnerable code example 3: inter-procedural wrapper
#include <algorithm>
#include <iostream>
#include <vector>
static void applyRemove(std::vector<int> &c, int val) {
std::remove(c.begin(), c.end(), val); // moved-from state escapes via &c
}
void fn_a1_source_in_callee(std::vector<int> &c) {
applyRemove(c, 7);
std::cout << c.front() << '\n'; // CERT.EXPR.MOVED_FROM.RANGE
}
applyRemove leaves its reference parameter in a moved-from-range state. The checker re-taints the caller's container at the call site so subsequent access to c.front() is flagged with a nested trace pointing to the callee's std::remove line.
Fixed code example 1: iterate up to the returned iterator
#include <algorithm>
#include <iostream>
#include <vector>
void case1_respect_boundary(std::vector<int> &c) {
auto e = std::remove(c.begin(), c.end(), 42);
for (auto i = c.begin(); i != e; ++i) { // OK: stops at the valid end
std::cout << *i << std::endl;
}
}
Fixed code example 2: erase-remove idiom
#include <algorithm>
#include <iostream>
#include <vector>
void case2_erase_remove(std::vector<int> &c) {
c.erase(std::remove(c.begin(), c.end(), 42), c.end()); // OK: tail erased
for (auto v : c) {
std::cout << v << std::endl;
}
}
The c.erase(...) call physically removes the moved-from tail; EraseKillVisitor drops the container taint at this node so the subsequent iteration is clean.
External guidance
Extension
The checker generates and consumes an inter-procedural binary KB record (EMFR.ITER.SRC) so wrappers that perform an erase-remove algorithm without erasing propagate the moved-from state to callers. Reference and pointer parameters are both propagated. Multi-hop chains are supported.