Connect with us

Entwicklung & Code

Core Java: Java Agents und Instrumentation API im praktischen Einsatz


Die Java Instrumentation API ist ein Teil des java.lang.instrument-Pakets und ermöglicht es, den Bytecode von Klassen zur Laufzeit zu verändern oder zu analysieren. Sie ist insbesondere für das Entwickeln von Profilern, Agenten, Monitoring-Tools oder auch dynamischen Sicherheitsmechanismen gedacht, die tief in das Verhalten einer Java-Anwendung eingreifen müssen, ohne dabei den Quellcode selbst zu verändern.


Sven Ruppert

Sven Ruppert

Seit 1996 programmiert Sven Java in Industrieprojekten und seit über 15 Jahren weltweit in Branchen wie Automobil, Raumfahrt, Versicherungen, Banken, UN und Weltbank. Seit über 10 Jahren ist er von Amerika bis nach Neuseeland als Speaker auf Konferenzen und Community Events, arbeitete als Developer Advocate für JFrog und Vaadin und schreibt regelmäßig Beiträge für IT-Zeitschriften und Technologieportale.
Neben seinem Hauptthema Core Java beschäftigt er sich mit TDD und Secure Coding Practices.

Im Zentrum dieser API steht das Konzept der Java Agents. Ein Agent ist eine spezielle Komponente, die beim Start der JVM oder auch zur Laufzeit über die Attach API geladen werden kann. Der Agent nutzt dann ein Instrumentation-Interface, das vom JVM-Launcher übergeben wird. Über dieses Interface erhält der Agent die Möglichkeit, bereits geladene Klassen zu inspizieren, neue Klassen zu transformieren oder das Verhalten der JVM zu beeinflussen – etwa durch das Einfügen von Hooks, das Messen von Aufrufzeiten oder das Injizieren von Sicherheitsprüfungen.

Die grundlegende Aufgabe besteht also darin, während des Classloading-Prozesses den Bytecode zu transformieren, ohne dabei die Semantik der Sprache Java zu verletzen. Technisch ermöglichen dies die ClassFileTransformer. Entwickler müssen sie zunächst registrieren, dann können die ClassFileTransformer bei jedem Ladevorgang einer Klasse den Bytecode manipulieren, bevor die JVM diesen interpretiert oder kompiliert.

Ein typischer Einstiegspunkt für einen statischen Agenten ist eine Klasse mit einer premain(String agentArgs, Instrumentation inst)-Methode. Bei dynamischen Agenten kommt stattdessen die agentmain(String agentArgs, Instrumentation inst)-Methode zum Einsatz. Beide Varianten erlauben es, Transformer zu registrieren und damit das Verhalten der Anwendung unterhalb der Sprachebene zu beeinflussen.

Das Verwenden der Instrumentation API erfordert ein tiefes Verständnis der JVM-Architektur und des Java-Klassenlademechanismus. Gleichzeitig eröffnet sie aber auch einzigartige Möglichkeiten zur Laufzeitanalyse und -modifikation, die mit den üblichen Mitteln in Java nicht erreichbar wären – und das in einer Art und Weise, die sich elegant in bestehende Anwendungen integrieren lässt, ohne diese neu zu kompilieren.

Ein zentraler Vorteil der Java Instrumentation API liegt in der Fähigkeit zur transparenten Bytecode-Manipulation: Das Verhalten bestehender Klassen lässt sich ändern, ohne deren Quellcode anzufassen. Das ist besonders relevant in sicherheitskritischen oder hochdynamischen Umgebungen, etwa beim Einfügen von Logging-, Tracing- oder Metrik-Code. Die API ermöglicht zudem eine Class Retransformation. Dabei lassen sich auch bereits geladene Klassen unter bestimmten Bedingungen nachträglich verändern – ein Feature, das in Verbindung mit Tools wie JRebel oder modernen Observability-Werkzeugen wie OpenTelemetry intensiv zum Einsatz kommt.




(Bild: Playful Creatives / Adobe Stock)

Am 14. Oktober findet die betterCode() Java 2025 statt. Bei der von iX und dpunkt.verlag ausgerichteten Online-Konferenz dreht sich alles um das für September geplante Java 25, das auch als LTS-Release verfügbar sein wird. Außerdem gibt es eine Keynote und ein Panel zu 30 Jahren Java und einen Vortrag zu ML für Java-Anwendungen.

Ein weiterer Vorteil ist die Integration ohne Quelltextänderungen. Ein Agent lässt sich mit dem Java-Prozess starten oder zur Laufzeit hinzufügen, ohne dass die Zielanwendung überhaupt wissen muss, dass sie instrumentiert wird. Dies ist insbesondere bei Debugging-, Monitoring- oder Sicherheitsanwendungen essenziell.

Darüber hinaus ist die Instrumentation API seit Java 5 fester Bestandteil des JDK und erfordert keine zusätzlichen Bibliotheken. In sicherheitssensiblen Umgebungen – etwa in der kritischen Infrastruktur oder im behördlichen Einsatz – kann genau dieser Umstand sowohl für den Einsatz sprechen (da keine externen Abhängigkeiten eingebracht werden müssen), als auch dagegen (da Angreifer dieselbe Schnittstelle missbrauchen könnten, um laufende Anwendungen zu manipulieren).

Die Mächtigkeit der Instrumentation API bringt zwangsläufig auch erhebliche Nachteile mit sich. An erster Stelle steht die Komplexität der Bytecode-Manipulation. Obwohl es Libraries wie ASM (Assembly Manipulator) oder Byte Buddy gibt, die den Umgang mit Bytecode erleichtern, bleibt dessen Manipulation ein Einstieg tief in den Maschinenraum der JVM und setzt ein fundiertes Verständnis der Java-Klassenstruktur, des Classloading und der Bytecode-Spezifikation voraus.

Ein zweites Risiko betrifft die Stabilität und Vorhersehbarkeit: Eingriffe in den Bytecode können zu subtilen Fehlern führen, etwa wenn Methodensignaturen verändert, Sicherheitsprüfungen umgangen oder Synchronisationsblöcke manipuliert werden. Solche Fehler sind schwer zu testen und treten gegebenenfalls nur unter bestimmten Laufzeitbedingungen auf – was den Debugging-Aufwand drastisch erhöht.

Auch die Kompatibilität über verschiedene JVM-Versionen hinweg ist ein Problem. Nicht jede JVM verhält sich identisch beim Classloading oder bei der Transformation. Besonders mit GraalVM oder in AOT-Umgebungen kommt es vor, dass bestimmte Features der Instrumentation API nicht oder nur eingeschränkt funktionieren. Das betrifft insbesondere dynamisches Nachladen von Klassen oder den Zugriff auf Low-level JVM-Interna.

Zu guter Letzt ist auch der Performance-Overhead nicht zu vernachlässigen. Zwar ist das Registrieren eines Agent meist sehr effizient, aber wenn Transformer komplexe Operationen bei jedem Classload ausführen oder Metriken injizieren, kann dies zu messbaren Verzögerungen beim Start oder zur Laufzeit führen – insbesondere in I/O-intensiven Serveranwendungen.

Die Java Instrumentation API ist daher ein zweischneidiges Schwert: Sie bietet eine einzigartige Möglichkeit, tief in das Verhalten einer Java-Anwendung einzugreifen – allerdings zum Preis erhöhter Komplexität, potenzieller Instabilität und erschwerter Wartbarkeit. Ihr Einsatz sollte daher gut begründet und gezielt erfolgen – stets begleitet von einer Test- und Logging-Infrastruktur. In sicherheitskritischen oder hochdynamischen Kontexten, in denen keine Änderung des Anwendungscodes möglich ist, bleibt sie jedoch ein unverzichtbares Werkzeug.

Die Performanceimplikationen der Java Instrumentation API sind vielschichtig und hängen stark vom jeweiligen Anwendungsfall, der Komplexität der durchgeführten Transformationen und der Art der Zielanwendung ab. Grundsätzlich kann man drei Phasen unterscheiden, in denen die Instrumentation API die Performance beeinflusst: beim Classloading, zur Laufzeit der instrumentierten Klassen und im Zusammenhang mit speicher- oder sicherheitsrelevanten Operationen.

Der signifikanteste unmittelbare Einfluss entsteht beim Laden einer Klasse durch den Classloader. Sobald ein ClassFileTransformer registriert ist, wird er bei jedem Ladevorgang einer neuen Klasse aufgerufen. In diesem Moment übergibt die JVM den Original-Bytecode der Klasse an den Transformer, der ihn modifiziert oder unverändert zurückgeben kann. Die Dauer dieser Transformation wirkt sich direkt auf die Startzeit der Anwendung oder auf das dynamische Nachladen von Klassen aus.

Komplexe Transformationen, insbesondere solche, die mit ASM oder ähnlichen Bytecode-Libraries durchgeführt werden, erzeugen häufig einen messbaren Overhead – insbesondere bei Framework-basierten Anwendungen mit tausenden Klassen. Wenn zusätzlich Logging, Tracing oder Code-Injektionen in jede Methode erfolgen, steigt die Ladedauer teils exponentiell an, da jeder Methodenblock individuell bearbeitet werden muss.

Je nach den am Bytecode vorgenommenen Veränderungen, kann sich auch zur Laufzeit ein zusätzlicher Overhead ergeben. Das gilt insbesondere dann, wenn der Transformer zusätzlichen Kontrollfluss einfügt – etwa Logging-Anweisungen, Zeitmessung, zusätzliche Validierungen oder sicherheitsrelevante Checks. Solche Einfügungen können beispielsweise die Ausführung jeder Methode um mehrere Nanosekunden bis zu Millisekunden verlängern, was in hochfrequent aufgerufenen Methoden (z. B. in I/O-Schleifen oder Geschäftslogik) zu massiven Performanceverlusten führen kann.

Ein typisches Beispiel ist das Method Entry/Exit Tracing, bei dem vor und nach jeder Methode Logging-Code injiziert wird. Obwohl dieser Logging-Code selbst nicht aktiv schreibt, sondern nur passive Marker setzt, entsteht dennoch eine Speicher- und CPU-Last, die sich unter hoher Last summieren kann.

Darüber hinaus kann die Instrumentierung auch JIT-Optimierungen der JVM beeinträchtigen, insbesondere wenn unvorhersehbare Kontrollflüsse eingefügt oder Methoden künstlich aufgebläht werden. Dies führt gegebenenfalls dazu, dass bestimmte Hotspots nicht mehr in nativ optimierten Code überführt werden – mit entsprechenden Folgen für die Ausführungszeit.

Wird bei der Instrumentierung zusätzlicher Objektzustand eingeführt – beispielsweise durch statische Caches, Trace-Informationen oder assoziierte Metadaten –, kann das zu einem Anstieg der Heap-Nutzung führen. Kritisch wird die Situation, wenn Weak- oder Soft-References nicht korrekt verwaltet oder kontinuierlich neue Klassen mit leicht unterschiedlichen Strukturen erzeugt werden, was schlimmstenfalls zu einem OutOfMemoryError beim Klassenspeicher führt.

Die Java Instrumentation API lässt sich mit minimalem Einfluss auf die Performance nutzen, sofern man sie effizient und zielgerichtet einsetzt. Das setzt allerdings voraus, dass man die registrierten Transformer so schlank wie möglich hält, unnötige Transformationen vermeidet und Klassen gezielt filtert. Ferner ist stets zu bedenken, dass selbst kleine Änderungen im Bytecode weitreichende Auswirkungen auf die Optimierungsstrategien der JVM und das Speicherverhalten haben können. Daher sind ein sorgfältiges Benchmarking und gezieltes Monitoring unerlässlich, wenn man die API in Produktionssystemen einsetzen möchte.

Da Instrumentation API Klassen zur Laufzeit manipulieren, Sicherheitsprüfungen entfernen oder neuen Bytecode injizieren kann, eröffnet sie eine Angriffsfläche, die weit über das hinausgeht, was typischer Anwendungscode leisten darf. Sie stellt damit ein potenzielles Einfallstor für Privilege Escalation, Code Injection und Persistenzmechanismen dar, wie sie bei Advanced Persistent Threats (APT) oder bösartigen Agenten zu beobachten sind.

Es ist wichtig festzuhalten, dass die Instrumentation API bewusst an der JVM-Sandbox vorbei operieren kann. Ist ein Agent einmal aktiv – sei es beim Start über das -javaagent-Argument oder dynamisch über die Attach API – dann hat er im Kontext der laufenden JVM nahezu vollständige Kontrolle über alle geladenen Klassen. Der Agent kann Sicherheitsprüfungen entfernen, Stacktraces verändern, Kontrollfluss umleiten oder sensible Informationen aus dem Speicher extrahieren. Auch die Transformation von Klassen aus sicherheitsrelevanten Packages wie java.lang.reflect oder javax.crypto ist möglich, sofern keine SecurityManager-Restriktionen aktiv sind (die allerdings seit Java 17 deprecated und seit Java 21 vollständig entfallen sind).

Besonders kritisch in diesem Zusammenhang ist die Attach API, da sie es erlaubt, beliebige Java-Prozesse auf dem lokalen System zur Laufzeit mit einem Agenten zu versehen – solange derselbe Benutzerkontext vorliegt. Das bedeutet: Ein Angreifer, der sich Zugriff auf einen Shell-Account verschafft, könnte jeden beliebigen Java-Prozess dieses Benutzers kompromittieren, ohne dass dieser Prozess dafür vorbereitet sein müsste. Dieses Szenario entspricht einer Form von Runtime Privilege Escalation auf Prozessebene.



Source link

Entwicklung & Code

Software Testing: Modelle als gemeinsame Sprache im Test


In dieser Episode spricht Richard Seidl mit Oliver Schuhmacher und Marvin Jakob über modellbasiertes Testen als Brücke zwischen Fachbereich, Entwicklung und Test. Ausgangspunkt ist ein Projekt bei Lufthansa CityLine: ein neues Crew-Management unter hohem Zeitdruck, mit klarer Abnahme.

Modelle dienen als gemeinsame Sprache, starten auf hoher Flughöhe und werden über Interviews, Verfeinerung und regelmäßige Reviews konkret. Tools generieren aus den Modellen Testfälle, machen Abdeckung sowie Impact in Jira sichtbar und halten Tests bei Änderungen aktuell.

Bei diesem Podcast dreht sich alles um Softwarequalität: Ob Testautomatisierung, Qualität in agilen Projekten, Testdaten oder Testteams – Richard Seidl und seine Gäste schauen sich Dinge an, die mehr Qualität in die Softwareentwicklung bringen.

Die aktuelle Ausgabe ist auch auf Richard Seidls Blog verfügbar: „Modelle als gemeinsame Sprache im Test – Oliver Schuhmacher, Marvin Jakob“ und steht auf YouTube bereit.


(mdo)



Source link

Weiterlesen

Entwicklung & Code

Deno vs. Oracle: Deno sammelt 200.000 US-Dollar im Kampf um die Marke JavaScript


Im Kampf gegen den Markennamen JavaScript – im Besitz von Oracle – sammelt der Hersteller der JavaScript-Runtime Deno nun Spenden in einer Online-Kampagne ein. Der in den USA geführte Markenrechtsstreit ist in die „Discovery“-Phase getreten, in der Deno nun Beweise zusammentragen muss, dass JavaScript Public Domain geworden ist, also in der Öffentlichkeit als generischer Begriff für die Sprache gebraucht wird und nicht als Marke von Oracle.

In der GoFundMe-Kampagne versucht Deno nun, 200.000 US-Dollar einzusammeln. Das Geld benötigt die Firma für Umfragen, Expertisen und Aussagen, die fundiert belegen sollen, dass die Öffentlichkeit JavaScript nicht mit Oracle in Verbindung bringt. Außerdem will die Firma damit Prozesskosten decken und das, was am Schluss übrig bleibt, an OpenJS spenden: „Das Geld wird nicht an Deno gehen“, verspricht die Firma im Blog.

Deno hatte im November 2024 den Löschantrag beim US-Markenamt eingereicht, Oracle hatte dem im August 2025 widersprochen.


(who)



Source link

Weiterlesen

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

Rainer Grimm

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:


Code

Code

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:


Nicht trivialer Destruktor

Nicht trivialer Destruktor

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

Weiterlesen

Beliebt