Skip to content

Fix security manager bug writing large blobs to GCS #55421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

jasontedor
Copy link
Member

This commit addresses a security manager permissions issue writing large blobs (on the resumable upload path) to GCS. The underlying issue here is that we need to wrap the close and write calls on the channel. It is not enough to do this:

SocketAccess.doPrivilegedVoidIOException(
  () -> Streams.copy(
    inputStream,
    Channels.newOutputStream(client().writer(blobInfo, writeOptions))));

This reason that this is not enough is because Streams#copy will be in the stacktrace and it is not granted the security manager permissions needed to close or write this channel. We only grant those permissions to classes loaded in the plugin classloader, and Streams#copy is from the parent classloader. This is why we must wrap the close and write calls as privileged, to truncate the Streams#copy call out of the stacktrace.

The reason that this issue is not caught in testing is because the size of data that we use in testing is too small to trigger the large blob resumable upload path. Therefore, we address this by adding a system property to control the threshold, which we can then set in tests to exercise this code path. Prior to rewriting the writeBlobResumable method to wrap the close and write calls as privileged, with this additional test, we are able to reproduce the security manager permissions issue. After adding the wrapping, this test now passes.

Relates #51596

This commit addresses a security manager permissions issue writing large
blobs (on the resumable upload path) to GCS. The underlying issue here
is that we need to wrap the close and write calls on the channel. It is
not enough to do this:

SocketAccess.doPrivilegedVoidIOException(
  () -> Streams.copy(
    inputStream,
    Channels.newOutputStream(client().writer(blobInfo, writeOptions))));

This reason that this is not enough is because Streams#copy will be in
the stacktrace and it is not granted the security manager permissions
needed to close or write this channel. We only grant those permissions
to classes loaded in the plugin classloader, and Streams#copy is from
the parent classloader. This is why we must wrap the close and write
calls as privileged, to truncate the Streams#copy call out of the
stacktrace.

The reason that this issue is not caught in testing is because the size
of data that we use in testing is too small to trigger the large blob
resumable upload path. Therefore, we address this by adding a system
property to control the threshold, which we can then set in tests to
exercise this code path. Prior to rewriting the writeBlobResumable
method to wrap the close and write calls as privileged, with this
additional test, we are able to reproduce the security manager
permissions issue. After adding the wrapping, this test now passes.
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-distributed (:Distributed/Snapshot/Restore)

@jasontedor
Copy link
Member Author

Here is a stacktrace that shows the issue

INTERNAL_SERVER_ERROR: com.google.cloud.storage.StorageException: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "setFactory")
        at com.google.cloud.storage.StorageException.translateAndThrow(StorageException.java:74)
        at com.google.cloud.storage.BlobWriteChannel.flushBuffer(BlobWriteChannel.java:67)
        at com.google.cloud.BaseWriteChannel.flush(BaseWriteChannel.java:112)
        at com.google.cloud.BaseWriteChannel.write(BaseWriteChannel.java:139)
        at java.base/java.nio.channels.Channels.writeFullyImpl(Channels.java:74)
        at java.base/java.nio.channels.Channels.writeFully(Channels.java:97)
        at java.base/java.nio.channels.Channels$1.write(Channels.java:172)
        at java.base/java.io.InputStream.transferTo(InputStream.java:777)
        at org.elasticsearch.core.internal.io.Streams.copy(Streams.java:47) <-- this does not have the necessary permission
        at org.elasticsearch.repositories.gcs.GoogleCloudStorageBlobStore.lambda$writeBlobResumable$5(GoogleCloudStorageBlobStore.java:216)
        at org.elasticsearch.repositories.gcs.SocketAccess.lambda$doPrivilegedVoidIOException$0(SocketAccess.java:54)
        at java.base/java.security.AccessController.doPrivileged(AccessController.java:554)
        at org.elasticsearch.repositories.gcs.SocketAccess.doPrivilegedVoidIOException(SocketAccess.java:53)
        at org.elasticsearch.repositories.gcs.GoogleCloudStorageBlobStore.writeBlobResumable(GoogleCloudStorageBlobStore.java:215)
        at org.elasticsearch.repositories.gcs.GoogleCloudStorageBlobStore.writeBlob(GoogleCloudStorageBlobStore.java:186)
        at org.elasticsearch.repositories.gcs.GoogleCloudStorageBlobContainer.writeBlob(GoogleCloudStorageBlobContainer.java:67)
        at org.elasticsearch.repositories.blobstore.BlobStoreRepository.snapshotFile(BlobStoreRepository.java:2075)
        at org.elasticsearch.repositories.blobstore.BlobStoreRepository.lambda$snapshotShard$55(BlobStoreRepository.java:1745)
        at org.elasticsearch.action.ActionRunnable$1.doRun(ActionRunnable.java:45)
        at org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:692)
        at org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
        at java.base/java.lang.Thread.run(Thread.java:832)
        Suppressed: com.google.cloud.storage.StorageException: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "setFactory")
                at com.google.cloud.storage.StorageException.translateAndThrow(StorageException.java:74)
                at com.google.cloud.storage.BlobWriteChannel.flushBuffer(BlobWriteChannel.java:67)
                at com.google.cloud.BaseWriteChannel.close(BaseWriteChannel.java:151)
                at java.base/java.nio.channels.Channels$1.close(Channels.java:177)
                at org.elasticsearch.core.internal.io.IOUtils.close(IOUtils.java:104)
                at org.elasticsearch.core.internal.io.IOUtils.close(IOUtils.java:74)
                at org.elasticsearch.core.internal.io.Streams.copy(Streams.java:54)
                ... 15 more
        Caused by: java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "setFactory")
                at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
                at java.base/java.security.AccessController.checkPermission(AccessController.java:1036)
                at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:408)
                at java.base/java.lang.SecurityManager.checkSetFactory(SecurityManager.java:1487)
                at java.base/javax.net.ssl.HttpsURLConnection.setSSLSocketFactory(HttpsURLConnection.java:365)
                at com.google.api.client.http.javanet.NetHttpTransport.buildRequest(NetHttpTransport.java:158)
                at com.google.api.client.http.javanet.NetHttpTransport.buildRequest(NetHttpTransport.java:55)
                at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:889)
                at com.google.cloud.storage.spi.v1.HttpStorageRpc.write(HttpStorageRpc.java:753)
                at com.google.cloud.storage.BlobWriteChannel$1.run(BlobWriteChannel.java:60)
                at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
                at com.google.api.gax.retrying.DirectRetryingExecutor.submit(DirectRetryingExecutor.java:105)
                at com.google.cloud.RetryHelper.run(RetryHelper.java:76)
                at com.google.cloud.RetryHelper.runWithRetries(RetryHelper.java:50)
                at com.google.cloud.storage.BlobWriteChannel.flushBuffer(BlobWriteChannel.java:53)
                ... 20 more

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jasontedor jasontedor merged commit a10b25e into elastic:master Apr 17, 2020
jasontedor added a commit to jasontedor/elasticsearch that referenced this pull request Apr 17, 2020
* Fix security manager bug writing large blobs to GCS

This commit addresses a security manager permissions issue writing large
blobs (on the resumable upload path) to GCS. The underlying issue here
is that we need to wrap the close and write calls on the channel. It is
not enough to do this:

SocketAccess.doPrivilegedVoidIOException(
  () -> Streams.copy(
    inputStream,
    Channels.newOutputStream(client().writer(blobInfo, writeOptions))));

This reason that this is not enough is because Streams#copy will be in
the stacktrace and it is not granted the security manager permissions
needed to close or write this channel. We only grant those permissions
to classes loaded in the plugin classloader, and Streams#copy is from
the parent classloader. This is why we must wrap the close and write
calls as privileged, to truncate the Streams#copy call out of the
stacktrace.

The reason that this issue is not caught in testing is because the size
of data that we use in testing is too small to trigger the large blob
resumable upload path. Therefore, we address this by adding a system
property to control the threshold, which we can then set in tests to
exercise this code path. Prior to rewriting the writeBlobResumable
method to wrap the close and write calls as privileged, with this
additional test, we are able to reproduce the security manager
permissions issue. After adding the wrapping, this test now passes.

* Fix forbidden APIs issue

* Remove leftover debugging
jasontedor added a commit to jasontedor/elasticsearch that referenced this pull request Apr 17, 2020
* Fix security manager bug writing large blobs to GCS

This commit addresses a security manager permissions issue writing large
blobs (on the resumable upload path) to GCS. The underlying issue here
is that we need to wrap the close and write calls on the channel. It is
not enough to do this:

SocketAccess.doPrivilegedVoidIOException(
  () -> Streams.copy(
    inputStream,
    Channels.newOutputStream(client().writer(blobInfo, writeOptions))));

This reason that this is not enough is because Streams#copy will be in
the stacktrace and it is not granted the security manager permissions
needed to close or write this channel. We only grant those permissions
to classes loaded in the plugin classloader, and Streams#copy is from
the parent classloader. This is why we must wrap the close and write
calls as privileged, to truncate the Streams#copy call out of the
stacktrace.

The reason that this issue is not caught in testing is because the size
of data that we use in testing is too small to trigger the large blob
resumable upload path. Therefore, we address this by adding a system
property to control the threshold, which we can then set in tests to
exercise this code path. Prior to rewriting the writeBlobResumable
method to wrap the close and write calls as privileged, with this
additional test, we are able to reproduce the security manager
permissions issue. After adding the wrapping, this test now passes.

* Fix forbidden APIs issue

* Remove leftover debugging
@jasontedor jasontedor deleted the repository-gcs-permissions-issue branch April 17, 2020 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants