You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 51 Next »

Self-assignment can occur in situations of varying complexity, but essentially, all self-assignments entail some variation of the following:

#include <utility>
 
struct S { /* ... */ }
 
void f() {
  S s;
  s = s; // Self-copy assignment
  s = std::move(s); // Self-move assignment
}

User-provided copy and move assignment operators must properly handle self-assignment.

Copy Assignment

The postconditions required for copy assignment are specified by the C++ Standard, [utility.arg.requirements], Table 23 [ISO/IEC 14882-2014], which states that for x = y, the value of y is unchanged. When &x == &y, this postcondition translates into the values of both x and y remaining unchanged. A naive implementation of copy assignment will destroy object-local resources in the process of copying resources from the given parameter. If the given parameter is the same object as the local object, the act of destroying object-local resources will invalidate them. The subsequent copy of those resources will be left in an indeterminate state, which violates the postcondition.

A user-provided copy assignment operator must prevent self-copy assignment from leaving the object in an indeterminate state. This can be accomplished by self-assignment tests, copy-and-swap, or other idiomatic design patterns.

The C++ Standard, [copyassignable], specifies that types must ensure that self-copy assignment leave the object in a consistent state when passed to Standard Template Library functions. Since objects of Standard Template Library types are used in contexts where CopyAssignable is required, Standard Template Library types are required to gracefully handle self-copy assignment.

Move Assignment

The postconditions required for a move assignment are specified by the C++ Standard, [utility.arg.requirements], Table 22 [ISO/IEC 14882-2014], which states that for x = std::move(y), the value of y is left in a valid but unspecified state. When &x == &y, this postcondition translates into the values of both x and y remaining in a valid but unspecified state. Leaving the values in an unspecified state may result in vulnerabilities leading to exploitable code.

A user-provided move assignment operator must prevent self-move assignment from leaving the object in a valid but unspecified state. Akin to copy assignment, it can be accomplished by self-assignment tests, move-and-swap, or other idiomatic design patterns.

The C++ Standard, [res.on.arguments], paragraph 1, specifies that function arguments that bind to rvalue reference parameters must be a unique reference to that parameter. Thus, the Standard Template Library does not gracefully handle self-move assignment, so such operations lead to undefined behavior. While this statement only normatively holds for objects passed as rvalue references into Standard Template Library functions, it is common practice to omit checks for self-move assignment within a move assignment operator [Meyers 14]. An rvalue reference parameter logically denotes an object that is either a true temporary object, or an object the caller wishes to be treated as being temporary. When given a reference to an object that is a true temporary object, by definition that object is unique, and so self-move assignment is impossible. Because move assignment is an optimized form of copy assignment, removing the check for self-move assignment is reasonable performance advice because an extra branch (that should never be taken) can be removed from the implementation of the move assignment operator. However, because the caller is able to denote an object as being logically temporary, there exists the possibility of self-move assignment with security implications.

Noncompliant Code Example

In this noncompliant code example, the copy and move assignment operators do not protect against self-assignment. If self-copy assignment occurs, this->S1 is deleted, which results in RHS.S1 also being deleted. The invalidated memory for RHS.S1 is then passed into the copy constructor for S, which can result in dereferencing an invalid pointer. If self-move assignment occurs, it is dependent on the implementation of std::swap as to whether S1 is left in a valid but unspecified state.

#include <new>
#include <utility>
 
struct S { /* ... */ }; // Has nonthrowing copy constructor
 
class T {
  int N;
  S *S1;
 
public:
  T(const T &RHS) : N(RHS.N), S1(RHS.S1 ? new S(*RHS.S1) : nullptr) {}
  T(T &&RHS) noexcept : N(RHS.N), S1(RHS.S1) { RHS.S1 = nullptr; }
  ~T() { delete S1; }
 
  // ...
 
  T& operator=(const T &RHS) {
    N = RHS.N;
    delete S1;
    S1 = new S(*RHS.S1);
    return *this;
  }
 
  T& operator==(T &&RHS) noexcept {
    N = RHS.N;
    S1 = RHS.S1;
    return *this;
  }
};

Compliant Solution (Self-Test)

This compliant solution guards against self-assignment by testing whether the given parameter is the same as this. If self-assignment occurs, the result of operator= is a noop; otherwise, the copy and move proceeds as in the original example.

#include <new>
#include <utility>
 
struct S { /* ... */ }; // Has nonthrowing copy constructor
 
class T {
  int N;
  S *S1;
 
public:
  T(const T &RHS) : N(RHS.N), S1(RHS.S1 ? new S(*RHS.S1) : nullptr) {}
  T(T &&RHS) noexcept : N(RHS.N), S1(RHS.S1) { RHS.S1 = nullptr; }
  ~T() { delete S1; }

  // ...
 
  T& operator=(const T &RHS) {
    if (this != &RHS) {
      N = RHS.N;
      delete S1;
      try {
        S1 = new S(*RHS.S1);
      } catch (std::bad_alloc &) {
        S1 = nullptr; // For basic exception guarantees
        throw;
      }
    }
    return *this;
  }
 
  T& operator==(T &&RHS) noexcept {
    if (this != &RHS) {
      N = RHS.N;
      S1 = RHS.S1;
    }
    return *this;
  }
};

Note that this solution does not provide a strong exception guarantee for the copy assignment. Specifically, if an exception is called when evaluating the new expression, this has already been modified. However, this solution does provide a basic exception guarantee because no resources are leaked and all data members contain valid values.

Compliant Solution (Copy and Swap)

This compliant solution avoids self-copy assignment by constructing a temporary object from RHS that is then swapped with *this. This compliant solution can provide a strong exception guarantee because swap() will never be called if resource allocation results in an exception being thrown while creating the temporary object. It avoids self-move assignment by testing for self-assignment, as in the previous compliant solution.

#include <new>
#include <utility>
 
struct S { /* ... */ }; // Has nonthrowing copy constructor
 
class T {
  int N;
  S *S1;
 
public:
  T(const T &RHS) : N(RHS.N), S1(RHS.S1 ? new S(*RHS.S1) : nullptr) {}
  T(T &&RHS) noexcept : N(RHS.N), S1(RHS.S1) { RHS.S1 = nullptr; }
  ~T() { delete S1; }

  // ...
 
  void swap(T &RHS) noexcept {
    using std::swap;
    swap(N, RHS.N);
    swap(S1, RHS.S1);
  }
 
  T& operator=(const T &RHS) noexcept {
    T(RHS).swap(*this);
    return *this;
  }
 
  T& operator==(T &&RHS) noexcept {
    if (&RHS != this) {
      N = RHS.N;
      S1 = RHS.S1;
    }
    return *this;
  }
};

Exceptions

OOP54-CPP-EX1: Self-swap is trivially self-move assignment safe. Calling std::swap(x, x) results in an operation that moves the resources from x into a temporary object, performs a self-move assignment that should have no ill-effect on the already-moved-from state of x, and finally moves the resources from the temporary object back into x with the end result that the resources stored within x should be unchanged. Because of how pervasive the use of std::swap() is within the Standard Template Library and the unlikelihood that a self-swap would cause undefined behavior in practice, guarding against self-move assignment through self-swap is not required.

Risk Assessment

Allowing a copy assignment operator to corrupt an object could lead to undefined behavior. Allowing a move assignment operator to leave an object in a valid but unspecified state could lead to abnormal program execution.

Rule

Severity

Likelihood

Remediation Cost

Priority

Level

OOP54-CPP

Low

Probable

High

P2

L3

Automated Detection

Tool

Version

Checker

Description

PRQA QA-C++4.44072, 4073, 4075, 4076 

Related Vulnerabilities

Search for other vulnerabilities resulting from the violation of this rule on the CERT website.

Bibliography

[Henricson 97]Rule 5.12, Copy assignment operators should be protected from doing destructive actions if an object is assigned to itself
[ISO/IEC 14882-2014]Subclause 17.6.3.1, "Template Argument Requirements"
Subclause 17.6.4.9, "Function Arguments"
[Meyers 05]Item 11, "Handle Assignment to Self in operator="
[Meyers 14] 

 


  • No labels