1

Memory Safe C++

 8 months ago
source link: https://lobste.rs/s/txcnjn/memory_safe_c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Memory Safe C++

By now you should have heard that various government agencies are issuing clear recommendations against using languages that aren’t memory safe by default. This led many to wonder what the future viability of C++ was. I’m an avid C++ programmer but when faced with the reality of how software is built, I have to applaud the effort here. Sure, maybe I trust myself to write correct programs but truthfully I simply would feel at least an order of magnitude safer if I knew the engineers building critical infrastructure like CT scan machines, commercial airline flight systems, or missile guidance systems were forced to use a memory safe language. I think that most reasonable people would admit the same, regretfully or not.

Anyway I got to wondering if it was possible to write C++ that was safe by default. I know that people on the Chromium team investigated modeling the Rust borrow checker using the C++ type system unsuccessfully. There have also been reports written by C++ committee members about a “safe subset” of the language which feels like complex vaporware. It generally seems like the consensus among everyone is that there is no viable or practical path forward to memory safety in C++.

So I thought about it a bit more and came up with a simple (though perhaps inefficient) approach to having memory safety in C++ in a way that you can deploy in your projects today, with varying degrees of difficulty. The idea is to provide two additional smart pointer types that mimic unique_ptr and shared_ptr but explicitly check bounds on pointer dereference. Additionally they annotate the pointer dereference operators with [[clang::lifetimebound]]. The combination of these two features make it impossible to trigger a use-after-free or buffer overflow error at runtime. Here’s the code:

#ifndef SAFE_PTR_H
#define SAFE_PTR_H

#include <cstddef>
#include <cstdlib>

#include <memory>
#include <new>
#include <type_traits>
#include <utility>

#ifdef __clang__
#define SAFE_PTR_LIFETIME_BOUND [[clang::lifetimebound]]
#else
#warning "No lifetimebound annotation available, code may contain memory vulnerabilities"
#define SAFE_PTR_LIFETIME_BOUND
#endif

namespace safe_ptr {

template <class PointerType>
class safe_ptr_template {
  PointerType _ptr;
  std::size_t _size;

  using element_type = typename PointerType::element_type;

  safe_ptr_template(PointerType ptr, std::size_t size) noexcept
    : _ptr(std::move(ptr)), _size(std::move(size)) {
  }

public:
  template <class... Args>
  static safe_ptr_template<PointerType> make_nothrow(Args && ...args)
    noexcept(noexcept(new (std::nothrow) element_type(std::forward<Args>(args)...))) {
    std::size_t n = 1;
    auto *ptr = new (std::nothrow) element_type(std::forward<Args>(args)...);
    if (!ptr) {
      n = 0;
    }
    return safe_ptr_template<PointerType>(PointerType(ptr), n);
  }

  static safe_ptr_template<PointerType> make_array_nothrow(std::size_t n)
    noexcept(noexcept(new (std::nothrow) element_type[n])) {
    element_type *ptr = new (std::nothrow) element_type[n];
    if (!ptr) {
      n = 0;
    }
    return safe_ptr_template<PointerType>(PointerType(ptr), n);
  }

  safe_ptr_template(safe_ptr_template<PointerType> && args) noexcept
    : safe_ptr_template(std::move(args._ptr), args._size) {
    args._size = 0;
  }

  safe_ptr_template<PointerType> &operator=(safe_ptr_template<PointerType> && args) noexcept {
    this->~safe_ptr_template();
    new (this) safe_ptr_template<PointerType>(std::move(args));
    return *this;
  }

  operator bool() noexcept {
    return !!_size;
  }

  element_type &operator[](std::size_t idx) noexcept SAFE_PTR_LIFETIME_BOUND {
    if (_size <= idx) {
      std::abort();
    }
    return _ptr.get()[idx];
  }

  element_type &operator *() noexcept SAFE_PTR_LIFETIME_BOUND {
    if (!_size) {
      std::abort();
    }

    return *_ptr.get();
  }

  element_type *operator->() noexcept {
    if (!_size) {
      std::abort();
    }
    return _ptr.get();
  }
};

template<class T>
using safe_ptr = safe_ptr_template<std::unique_ptr<T>>;

template <class T, class... Args>
safe_ptr<T> make_safe_ptr_nothrow(Args && ...args)
  noexcept(noexcept(safe_ptr<T>::make_nothrow(std::forward<Args>(args)...))) {
  return safe_ptr<T>::make_nothrow(std::forward<Args>(args)...);
}

template <class T>
safe_ptr<T[]> make_safe_ptr_array_nothrow(std::size_t n)
  noexcept(noexcept(safe_ptr<T[]>::make_array_nothrow(n))) {
  return safe_ptr<T[]>::make_array_nothrow(n);
}
}

#endif // SAFE_PTR_H

Here is an example usage:

#include "safe_ptr.h"

#include <stdlib.h>

int main(int argc, char *argv[]) {
  if (argc < 2) return -1;

  int n = atoi(argv[1]);

  auto h = safe_ptr::make_safe_ptr_nothrow<int>();
  h[n] = 10;
  return 0;
}

Now let’s run that with “-O3”

$ clang++ -O3 -Wall test.cc
$ ./a.out 10
Aborted (core dumped)

If you run the debugger on the dumped core you’ll find the stack trace at which the abort() call occured. What about use-after-free?

int main(int argc, char *argv[]) {
  int & n = *safe_ptr::make_safe_ptr_nothrow<int>();
  return n;
}
$ clang++ -O3 -Wall test.cc
test.cc:6:14: warning: temporary bound to local reference 'n' will be destroyed at the end of the full-expression
      [-Wdangling]
  int & n = *safe_ptr::make_safe_ptr_nothrow<int>();
             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.

Very nice.

Big Ifs

The nice thing about this tool is that it requires no tool support outside of [[clang:lifetimebound]] so it can be deployed anywhere easily. This also makes it easy to modify to your needs. The bad part about this is that it requires your entire codebase to use these smart pointer classes. For large C++ applications this is not practical but for smaller projects it’s doable.

A related concern is that you need to either ban the use of raw pointers or require that they are only used within a compiler annotated “unsafe” block similar to Rust. It would be nice if the mainstream C++ compilers added a warning on the use of raw pointers.

Another approach, if you are able to do it, is to modify your C++ standard library and add the runtime checks and function annotations to unique_ptr itself. This doesn’t work for arrays, so you’ll still have to make explicit references to the safe array version.

We should also be concerned about stack array types like int arr[] and std::array<int> arr. You’ll need safe versions of those as well.

One last point, you’ll notice that this smart pointer is larger than a standard pointer by the size of std::size_t. One optimization is to make the safe_ptr<T> type distinct from the safe_ptr<T[]> type, this allows the former type to not require an extra _size member. Another optimization is that you can use the high bits of the pointer on popular contemporary 64-bit platforms to store the number of elements (since those bits are unused). If the number of elements is larger than that space, just return an allocation error. This works because it’s likely you’ll only need to allocate very large arrays in a few special places.

There are probably many other caveats that apply but the point of this post was to raise awareness about this simple approach and to provide a practical path forward to memory safety in C++.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK