Skip to content
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

core: Do not leak server state when application callbacks throw exceptions #3064

Merged
merged 8 commits into from Jun 19, 2017

Conversation

zpencer
Copy link
Contributor

@zpencer zpencer commented Jun 5, 2017

Today JumpToApplicationThreadServerStreamListener leaks server state by transmitting details about uncaught StatusRuntimeException throwables to the client. This is a security problem.

This PR ensures that uncaught exceptions always close the ServerCall without leaking any state information. Users running in a trusted environment who want to transmit error details can use ServerInterceptors.STATUSRUNTIMEXCEPTION_TRANSMITTING_INTERCEPTOR .

fixes #2189

@@ -17,6 +17,8 @@
package io.grpc;

import com.google.common.base.Preconditions;
import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener;
import io.grpc.internal.SynchronizedServerCall;
Copy link
Contributor

Choose a reason for hiding this comment

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

io.grpc should not reference io.grpc.internal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved StatusRuntimeExceptionTransmitter to its own class under io.grpc.util

* if all clients are trusted.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2189")
public static final ServerInterceptor STATUSRUNTIMEXCEPTION_TRANSMITTING_INTERCEPTOR =
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not core API, but rather something provided for convenience. io.grpc.util seems to be a better place for this class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved StatusRuntimeExceptionTransmitter to its own class

try {
super.onReady();
} catch (StatusRuntimeException t) {
closeWithException(t);
Copy link
Contributor

@zhangkun83 zhangkun83 Jun 9, 2017

Choose a reason for hiding this comment

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

I tried to analyze the locking order with in-process transport.

InProcessServerStream is synchronized on every method, therefore the lock order on the outbound path is
SynchronizedServerCall.this > InProcessServerStream.this.

On the inbound path, InProcessClientStream.serverStreamListener is called under synchronized (InProcessClientStream.this), and will potentially call closeWithException(t) that calls into InProcessServerStream.close(), forming this lock order:
InProcessClientStream.this > SynchronizedServerCall.this > InProcessServerStream.this.

So, this change doesn't impose a deadline scenario.

However, if we (or the user) did the same thing on the client side with a SynchronizedClientCall, it would form a reverse lock order, and a deadlock is possible:
InProcessServerStream.this > SynchronizedClientCall.this > InProcessClientStream.this.

@ejona86 is this a valid concern? Should we try to reduce locking, e.g., call callbacks outside of lock in in-process transport?

Copy link
Contributor

Choose a reason for hiding this comment

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

Per offline discussion, the deadlock issue with in-process transport is legit, while not the fault of this PR. I have filed #3084

However, even without #3084, using synchronized to make thread-safe calls to ServerCall and ClientCall is still prone to deadlock when in-process transport is used with direct executor:

  • Thread1

    1. SynchronizedClientCall.sendMessage() (locks SynchronizedClientCall.this)
    2. ServerCall.Listener calls into application code, which calls SynchronizedServerCall.close() (locks SynchronizedServerCall.this)
  • Thread2

    1. SynchronizedServerCall.sendMessage() (locks SynchronizedServerCall.this)
    2. ClientCall.Listener calls into application code, which calls SynchronizedClientCall.close() (locks SynchronizedClientCall.this)

This time, it's the two synchronized calls that are involved in the deadlock.

This can be considered application-level code, and if application does that and got deadlock, it's the application's fault. Application shouldn't do it, and it means we should not do it either.

Like the proposed solution for #3084, we could also use ChannelExecutor instead to provide the thread-safety. ChannelExecutor should probably be renamed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reimplemented the functionality via SerializingServerCall. Opted to make this a private nested class to minimize visibility for now.

@zhangkun83 zhangkun83 self-assigned this Jun 9, 2017
Copy link
Member

@ejona86 ejona86 left a comment

Choose a reason for hiding this comment

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

Oops. Forgot to send these out.

* if all clients are trusted.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2189")
public static final ServerInterceptor STATUSRUNTIMEXCEPTION_TRANSMITTING_INTERCEPTOR =
Copy link
Member

Choose a reason for hiding this comment

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

Expose this as a method, not as a static field. That allows us, in the future, to avoid creating it until first use or similar. Android in particular is hurt quite a bit by static initialization since apps start frequently and want really low memory usage. So while it's fine not to worry about that now, we shouldn't have the API dictate the lifetime.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

if (metadata == null) {
metadata = new Metadata();
}
syncServerCall.close(Status.fromThrowable(t), metadata);
Copy link
Member

Choose a reason for hiding this comment

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

t.getStatus()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -17,6 +17,8 @@
package io.grpc;

import com.google.common.base.Preconditions;
import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener;
import io.grpc.internal.SynchronizedServerCall;
Copy link
Member

Choose a reason for hiding this comment

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

We can't have references to internal from io.grpc. I guess move SynchronizedServerCall to io.grpc and make it package-private? Or maybe put it in ForwardingServerCall (and still package-private)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to io.grpc.util

getSoleMethod(intercepted).getServerCallHandler().startCall(call, headers).onHalfClose();
getSoleMethod(intercepted).getServerCallHandler().startCall(call, headers).onReady();
} catch (Throwable t) {
fail("The interceptor should have handled the error by directly closing the ServerCall, "
Copy link
Member

Choose a reason for hiding this comment

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

This throws away t. maybe just make this a comment and get rid of the try-catch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2189")
public final class StatusRuntimeExceptionTransmitter implements ServerInterceptor {
private StatusRuntimeExceptionTransmitter() {
// do not instantiate
Copy link
Member

Choose a reason for hiding this comment

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

This looks like the constructor normally in a static utility class that is never instantiated. However, this class is instantiated. Maybe just remove this comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


/**
* A class that intercepts uncaught exceptions of type {@link StatusRuntimeException} and handles
* them by closing the {@link ServerCall}, and transmitting the exception's details to the client.
Copy link
Member

Choose a reason for hiding this comment

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

s/exception's details/exception's status and metadata/

Otherwise people will think the stack trace shows up on the other side.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

}

private void closeWithException(StatusRuntimeException t) {
Metadata metadata = Status.trailersFromThrowable(t);
Copy link
Member

Choose a reason for hiding this comment

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

t.getTrailers()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

* limitations under the License.
*/

package io.grpc.util.interceptor.server;
Copy link
Member

Choose a reason for hiding this comment

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

That's a pretty deep package. I'd rather not group things by their class type (interceptor). @zhangkun83, you have a preference for package name?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer just io.grpc.util.

Copy link
Member

Choose a reason for hiding this comment

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

io.grpc.util SGTM as well.

Copy link
Contributor Author

@zpencer zpencer Jun 12, 2017

Choose a reason for hiding this comment

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

moved and renamed the class to be more obvious that it's an interceptor

@Mock
private ServerCall.Listener<String> listener;

private MethodDescriptor<String, Integer> flowMethod;
Copy link
Member

Choose a reason for hiding this comment

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

Use TestMethodDescriptors.noopMethod?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is handy, done

private Marshaller<Integer> responseMarshaller;

@Mock
private ServerCallHandler<String, Integer> handler;
Copy link
Member

Choose a reason for hiding this comment

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

Make a concrete implementation instead of a mock?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Most of these fields can turn into concrete implementations without too much boiler plate, so changing those as well.

}

private void closeWithException(StatusRuntimeException t) {
serverCall.close(t.getStatus(), t.getTrailers());
Copy link
Member

Choose a reason for hiding this comment

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

getTrailers may return null, but close() requires non-null Metadata

.thenReturn(listener);

serviceDefinition = ServerServiceDefinition.builder(new ServiceDescriptor("basic", flowMethod))
handler = new ServerCallHandler<String, Integer>() {
Copy link
Member

Choose a reason for hiding this comment

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

This initialization can be done at declaration time, and then serviceDefinition can also be initialized when declared. That would leave just initMocks in setup.

@@ -17,16 +17,12 @@
package io.grpc.util.interceptor.server;
Copy link
Member

Choose a reason for hiding this comment

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

This needs to move folder/package as well.

@zpencer zpencer merged commit 2b1eee9 into grpc:master Jun 19, 2017
@zpencer zpencer deleted the zpencer/2189/lostMetadata branch July 15, 2017 19:40
@d2chau
Copy link

d2chau commented Aug 23, 2017

Note: ServerInterceptors.STATUSRUNTIMEXCEPTION_TRANSMITTING_INTERCEPTOR has been moved to it's own class. It is now TransmitStatusRuntimeExceptionInterceptor. Instantiating it requires calling .instance().

I wanted to document it at least here, since it has changed from the initial post above.

@lock lock bot locked as resolved and limited conversation to collaborators Jan 20, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

metadata is lost when server sends StatusRuntimeException
4 participants