7

Ich habe keine Möglichkeit, diese zu erklären, aber ich fand dieses Phänomen in Code der jemand anderem:Security von I/O-Code in einem parallelen Strom

import java.io.IOException; 
import java.io.UncheckedIOException; 
import java.nio.file.Files; 
import java.util.stream.Stream; 

import org.junit.Test; 

public class TestDidWeBreakJavaAgain 
{ 
    @Test 
    public void testIoInSerialStream() 
    { 
     doTest(false); 
    } 

    @Test 
    public void testIoInParallelStream() 
    { 
     doTest(true); 
    } 

    private void doTest(boolean parallel) 
    { 
     Stream<String> stream = Stream.of("1", "2", "3"); 
     if (parallel) 
     { 
      stream = stream.parallel(); 
     } 
     stream.forEach(name -> { 
      try 
      { 
       Files.createTempFile(name, ".dat"); 
      } 
      catch (IOException e) 
      { 
       throw new UncheckedIOException("Failed to create temp file", e); 
      } 
     }); 
    } 
} 

Wenn es mit dem Sicherheitsmanager ausführen aktiviert ist, nur parallel() Aufruf in einem Stream oder parallelStream() beim Abrufen des Streams aus einer Sammlung scheint zu garantieren, dass alle Versuche, I/O durchzuführen, SecurityException werfen. (Wahrscheinlich ruft jedes Verfahren, das kann throw SecurityException, wird Wurf.)

Ich verstehe, dass parallel() bedeutet, dass es in einem anderen Thread ausgeführt werden, die nicht die gleichen Privilegien wie die haben könnten wir begannen mit , aber ich denke, ich dachte, dass der Rahmen dafür sorgen würde.

Entfernen von Anrufen an parallel() oder parallelStream() in der Codebasis vermeidet das Risiko. Das Einfügen eines behebt es auch, aber klingt für mich nicht sicher, zumindest nicht in allen Situationen. Gibt es noch eine andere Option?

+2

Bitte geben Sie den Stack-Trace einer Ausnahme an, die Sie erhalten –

+2

Fügen Sie auch Ihren 'SecurityManager'-Code hinzu, damit wir Ihr Problem genau reproduzieren können. –

+1

Dies hat möglicherweise mit parallelen Streams zu tun, die den gemeinsamen Fork-Join-Pool verwenden. Wie funktioniert es [wenn Sie Ihren eigenen Pool zur Verfügung stellen] (http://stackoverflow.com/a/22269778/829571)? – assylias

Antwort

6

Parallele Stream-Ausführung verwendet das Fork/Join-Framework, genauer gesagt den gemeinsamen Fork/Join-Pool. Dies ist ein Implementierungsdetail, aber in diesem Fall können solche Details auf unerwartete Weise austreten.

Beachten Sie, dass das gleiche Verhalten auch beim asynchronen Ausführen einer Task mit CompletableFuture auftreten kann.

Wenn ein Sicherheitsmanager vorhanden ist, wird die Thread-Factory des gemeinsamen Fork/Join-Pools auf eine Factory gesetzt, die harmlose Threads erstellt. Solch ein harmloser-Thread hat keine gewährten Berechtigungen, ist kein Mitglied einer definierten Thread-Gruppe und nachdem ein Fork/Join-Task auf oberster Ebene seine Ausführung abgeschlossen hat, werden alle Thread-Locals (falls erstellt) gelöscht. Ein solches Verhalten stellt sicher, dass Fork/Join-Tasks bei der gemeinsamen Nutzung des gemeinsamen Pools voneinander isoliert sind.

Aus diesem Grund ist in dem Beispiel ein SecurityException, wahrscheinlich ausgelöst wird:

java.lang.SecurityException: Kann temporäre Datei oder das Verzeichnis

Es gibt zwei mögliche Workarounds erstellen. Je nach den Gründen, die ein Sicherheitsmanager verwendet, kann jede Arbeit das Risiko einer Unsicherheit erhöhen.

Die erste allgemeinere Vorgehensweise besteht darin, eine Fork/Join-Thread-Factory über eine Systemeigenschaft zu registrieren, um dem Fork/Join-Framework mitzuteilen, welche Standard-Thread-Factory für den gemeinsamen Pool verwendet werden soll. Zum Beispiel hier ist eine wirklich einfache Faden Fabrik:

public class MyForkJoinWorkerThreadFactory 
     implements ForkJoinPool.ForkJoinWorkerThreadFactory { 
    public final ForkJoinWorkerThread newThread(ForkJoinPool pool) { 
     return new ForkJoinWorkerThread(pool) {}; 
    } 
} 

die mit folgenden Systemeigenschaft registriert werden kann:

-Djava.util.concurrent.ForkJoinPool.common.threadFactory = MyForkJoinWorkerThreadFactory

Das Verhalten von MyForkJoinWorkerThreadFactory entspricht derzeit dem von ForkJoinPool.defaultForkJoinWorkerThreadFactory.

Die zweite, spezifischere, ist um einen neuen Fork/Join-Pool zu erstellen. In diesem Fall wird ForkJoinPool.defaultForkJoinWorkerThreadFactory für Konstruktoren verwendet, die kein ForkJoinWorkerThreadFactory Argument akzeptieren. Jede parallele Stream-Ausführung müsste innerhalb eines Tasks ausgeführt werden, der innerhalb dieses Pools ausgeführt wird. Beachten Sie, dass dies ein Implementierungsdetail ist und möglicherweise in zukünftigen Versionen nicht funktioniert.

+0

Gute Antwort. Beachten Sie auch, dass der einreichende Thread möglicherweise als Worker-Thread im F/J-Framework verwendet wird, was sich hier auswirken könnte: http://coopsoft.com/ar/Calamity2Article.html#submit – edharned

+0

Interessanterweise wird sogar ein neuer Thread erstellt ForkJoinPool ohne das Anpassen von irgendetwas erzeugt einen Pool, der mir volle Berechtigungen gibt. Es ist nur die Instanz, die von commonPool() stammt, die von ihrer benutzerdefinierten Factory "vergiftet" wurde. Die Stream-API scheint mir nicht die Möglichkeit zu geben, zu entscheiden, welcher Pool sowieso verwendet wird. Ich denke, wir müssen es einfach vermeiden und unsere bestehenden parallelen Ausführungsprogramme verwenden. – Trejkaz

+0

Trejkaz, du hast recht, ich habe das übersehen, als ich den Code angeschaut habe. Antwort aktualisiert, um den Fehler zu korrigieren. –

2

Ihre Sorge um AccessController.doPrivileged ist unnötig. Es reduziert nicht die Sicherheit wenn richtig gemacht. Die Versionen ein einzige Aktion Argument nehmen die Aktion in Ihrem Kontext ausführen, Ihre Anrufer zu ignorieren, aber es gibt überladene Methoden, ein zusätzliches Argument, einen zuvor aufgezeichneter Kontext:

private void doTest(boolean parallel) 
{ 
    Consumer<String> createFile=name -> { 
     try { 
      Files.createTempFile(name, ".dat"); 
     } 
     catch (IOException e) { 
      throw new UncheckedIOException("Failed to create temp file", e); 
     } 
    }, actualAction; 
    Stream<String> stream = Stream.of("1", "2", "3"); 

    if(parallel) 
    { 
     stream = stream.parallel(); 
     AccessControlContext ctx=AccessController.getContext(); 
     actualAction=name -> AccessController.doPrivileged(
      (PrivilegedAction<?>)()->{ createFile.accept(name); return null; }, ctx); 
    } 
    else actualAction = createFile; 

    stream.forEach(actualAction); 
} 

Die erste wichtige Linie ist die AccessControlContext ctx=AccessController.getContext(); Aussage es zeichnet Ihren aktuellen Sicherheitskontext auf, der Ihren Code und die aktuellen Anrufer enthält. (Denken Sie daran, dass die effektiven Berechtigungen die Kreuzung der Sätze aller Anrufer sind). Wenn Sie das resultierende Kontextobjekt ctx der Methode doPrivileged innerhalb der Consumer bereitstellen, wird der Kontext wiederhergestellt. Mit anderen Worten: PrivilegedAction verfügt über die gleichen Berechtigungen wie in Ihrem Singlethread-Szenario.

Verwandte Themen