Entwicklung & Code

Programmiersprache C++26: Sicherheitsverbesserungen in der Kernsprache


Sicherheit ist ein wichtiges Thema in C++26, und Contracts sind wahrscheinlich das größte Feature für die Sicherheit. Aber C++26 hat noch viel mehr zu bieten.




Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Heute möchte ich drei kleine, aber wichtige Verbesserungen in C++26 vorstellen, die typische Sicherheitsprobleme in C++ lösen.

Ein Sicherheitsproblem kann schwer zu finden sein. Dieser Codeausschnitt stammt aus dem Proposal P2748R5, auf den ich in diesem Abschnitt immer wieder Bezug nehme. Ich habe ihn in ein minimales, ausführbares Programm umgewandelt.


// bindReferenceToTemporary.cpp

#include 
#include 
#include 

const std::string_view& getString() {
    static std::string s = "Hallo Welt!";
    return s;
}

int main() {
    std::cout << getString() << '\n';
}   


Der GCC-Compiler gibt bereits eine aussagekräftige Fehlermeldung aus:



Hier kommt ein weiteres Beispiel mit einem Fehler im Code:


struct X {
    const std::map<:string int=""> d_map;
    const std::pair<:string int="">& d_first;

    
X(const std::map<:string int="">& map)
        : d_map(map), d_first(*d_map.begin()) {}
};


Wer den Code geschrieben hat, hat übersehen, dass das erste Element des Paares, das als Schlüssel bezeichnet wird, konstant ist. Dadurch wird eine temporäre Variable erstellt. Infolgedessen ist d_first nicht mehr gültig.

Das bringt uns zur nächsten Sicherheitsverbesserung.

Ich beziehe mich auf den Proposal P2795r5.

Zunächst einmal gilt es für Objekte mit automatischer Speicherdauer und temporäre Objekte. Deren Besonderheit ist, dass sie auf einen beliebigen Wert initialisiert werden. Das bedeutet, dass das Programm ein undefiniertes Verhalten aufweist.

Damit bleibt noch eine Frage offen: Was bedeutet automatische Speicherdauer? Die folgenden Variablen haben eine automatische Speicherdauer:

  • Variablen, die einen Blockbereich haben und nicht explizit als static, thread_local oder extern deklariert wurden. Der Speicher dieser Variablen bleibt nur so lange gültig, bis der Block abgeschlossen ist.
  • Variablen, die zu einem Parameterbereich gehören, beispielsweise einer Funktion. Sie werden automatisch zerstört, wenn der Parameterbereich aufgelöst wird.

Zwei Beispiele aus dem Proposal sollen diese Begriffe verdeutlichen:


extern void f(int);

int main() {
  int x;     // default-initialized, value of x is indeterminate
  f(x);      // glvalue-to-prvalue conversion has undefined behaviour
}   

void f(T&);
void g(bool);

void h() {
  T* p;    // uninitialized, has erroneous value (e.g. null)
  bool b;  // uninitialized, erroneous value may not be valid bool

  f(*p);   // dereferencing has undefined behaviour
  g(b);    // undefined behaviour if b is invalid
}


Um es auf den Punkt zu bringen: Der Proposal verwandelt nicht initialisierte Lesevorgänge, die in C++23 ein undefiniertes Verhalten darstellten, in fehlerhafte Programme in C++26.

Natürlich kann die vollständige Initialisierung automatischer Variablen ein relativ aufwendiger Vorgang sein. Daher gibt es einen Opt-out-Mechanismus:

Das Attribut [[indeterminate]] repräsentiert den Opt-out-Mechanismus. Dieses Feature ist nur für Experten gedacht. Das Attribut ermöglicht es, Variablen mit automatischer Speicherung, die nicht initialisiert wurden, zu lesen, ohne dass das Programm fehlerhaft ist. Das folgende vereinfachte Beispiel stammt aus dem Proposal:


int x [[indeterminate]];
std::cin >> x;

[[indeterminate]] int a, b[10], c[10][10];
compute_values(&a, b, c, 10);

// This class benefits from avoiding determinate-storage initialization guards.
struct SelfStorage {
  std::byte data_[512];
  void f();   // uses data_ as scratch space
};

SelfStorage s [[indeterminate]];   // documentation suggested this

void g([[indeterminate]] SelfStorage s = SelfStorage());   // same; unusual, but plausible


Die letzte Sicherheitsfunktion betrifft unvollständige Typen.

Ein unvollständiger Typ ist ein Datentyp, für den nur die Deklaration, aber keine Definition existiert. Ein Zeiger oder eine Referenz auf einen unvollständigen Typ ist völlig in Ordnung. Operationen, die die Größe, das Layout oder die Mitgliedsfunktionen dieses Datentyps erfordern, sind jedoch fehlerhaft.

Die neue Funktion ist etwas spezifischer: Das Löschen eines Zeigers auf einen unvollständigen Klassentyp ist fehlerhaft, es sei denn, dieser Klassentyp verfügt über einen trivialen Destruktor und keine klassenspezifische Deallokations-Funktion. Das bedeutet, dass der Compiler den Destruktor der Klasse erstellt hat und operator delete in der Klasse nicht überladen wurde.

Der Proposal liefert ein gutes Beispiel, um den feinen Unterschied zwischen einem trivialen Destruktor und einem nicht-trivialen Destruktor zu veranschaulichen:

Triviale Destruktoren:


// trivialDestructor.cpp
    
namespace xyz {
  struct Widget; // forward decl
  Widget *new_widget();
} // namespace xyz

int main() {
  xyz::Widget *p = xyz::new_widget();
  delete p;
}

namespace xyz {
struct Widget {
  const char *d_name;
  int         d_data;
  // (implicit) trivial destructor
  // This is the only difference.
}; 

Widget *new_widget() { 
    return new Widget(); 
}
} // namespace xyz


Nicht-trivialer Destruktor:


// nontrivialDestructor.cpp

namespace xyz {
  struct Widget; // forward decl
  Widget *new_widget();
} // namespace xyz

int main() {
  xyz::Widget *p = xyz::new_widget();
  delete p;
}
namespace xyz {
struct Widget {
  const char *d_name;
  int         d_data;
  ~Widget() {} // nontrivial dtor
  // This is the only difference.
};

Widget *new_widget() { 
    return new Widget(); 
}
} // namespace xyz


Das zweite Beispiel enthält einen nicht-trivialen Destruktor. Entsprechend dem Proposal bricht die Kompilierung mit einer Fehlermeldung ab:



In meinem nächsten Artikel werde ich mich mit einigen kleineren Verbesserungen rund um Templates befassen. Diese Verbesserungen dienen in erster Linie dazu, inkonsistentes Verhalten in C++ zu beseitigen.


(rme)



Source link

Beliebt

Die mobile Version verlassen