-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Conversation
@@ -17,6 +17,8 @@ | |||
package io.grpc; | |||
|
|||
import com.google.common.base.Preconditions; | |||
import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; | |||
import io.grpc.internal.SynchronizedServerCall; |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
SynchronizedClientCall.sendMessage()
(locksSynchronizedClientCall.this
)ServerCall.Listener
calls into application code, which callsSynchronizedServerCall.close()
(locksSynchronizedServerCall.this
)
-
Thread2
SynchronizedServerCall.sendMessage()
(locksSynchronizedServerCall.this
)ClientCall.Listener
calls into application code, which callsSynchronizedClientCall.close()
(locksSynchronizedClientCall.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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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 = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t.getStatus()
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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, " |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t.getTrailers()
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use TestMethodDescriptors.noopMethod
?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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>() { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
Note: I wanted to document it at least here, since it has changed from the initial post above. |
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