Client: java
Requests now go out with the default request config of the HttpClient
passed in; a connect timeout set on the transport is added on top of it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client: java
readJSONString assembled the four hex digits of a \uXXXX escape into a
short, so escapes from \u8000 to \uffff became negative code units. The
surrogate branches cast the value back to char, but the plain-character
branch passed it to new String(int[]) and the read ended in an
IllegalArgumentException. Keep the code unit as a char.
An unpaired high surrogate escape was also accepted silently when the
string ended or when a plain character or a non-surrogate escape
followed it, in the last case emitting the characters out of order.
Reject it with INVALID_DATA, as the C++ reader does.
Client: java
TBinaryProtocol and TCompactProtocol now apply the same length check on the
skip path that readString and readBinary use, so the declared size is
validated before any bytes are consumed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: java
The nonblocking SASL server now closes the failing connection before it logs,
and routes the logging through a call that cannot end the network thread, so
handling one connection's failure leaves the others served.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: java
The generated byte[] getX() right-sized the field and stored it through setX(),
which copies again with copyBinary, so every call copied the bytes although it
returns the field's own array either way. It now assigns the right-sized buffer
directly, as Apache Accumulo already does by patching the generated code.
Union getters and bufferForX() are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client: java
A failure while a network thread handles one connection now closes only
that connection. If a network thread ends anyway, the server stops, as it
already does when the acceptor thread ends.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Client: java
The nonblocking servers now clear the read interest of a connection whose
frame does not fit into the read buffer memory limit and restore it once
memory is released. A frame larger than that limit closes its connection,
as a frame over the maximum frame size does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Client: java
When evaluating a client's SASL response throws an unexpected runtime
exception, transition the connection to its closing phase so the network
thread closes it, matching how the processing phase already handles such
failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: go,test
The Go test client sent no client certificate, so every socket transport
over SSL failed the handshake against the Python server, which requires
one. It now takes the server's --certPath flag and loads client_v3.
Go 1.27 and later refuse to load client_v3.crt as shipped: its
alternative names include ::ffff:127.0.0.1, and the x509 parser rejects
an IPv4-mapped IPv6 SAN since go1.27.0. The lib-go job runs on 1.27 and
test/go now loads the certificate, so it is regenerated without that
entry. THRIFT-3660 added it for the Python peer-address check, which
since THRIFT-6201 reduces a mapped address itself. The new certificate
is self-signed with the same key, distinguished name and extensions, as
the shipped one was. The Java client keystore is regenerated from it,
and the README recipe now produces what is shipped instead of a
CA-signed certificate the Python server does not trust.
The 18 py-go SSL entries leave the known failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generated Java decoders sized a list, set or map from the element count in the
container header before reading any element, so a peer could name a count it
never backs with data and have the decoder reserve capacity for all of it. The
same holds for the functional read API in TProtocol (readList/readSet/readMap
with the default collection creators), which sized the collection from the wire
count too.
TBaseHelper.preallocSize caps that initial reservation at 1024 elements and
treats a negative count -- including a hash container's doubled count overflowing
int -- as zero. The generated decoders and the default creators now route the
count through it. Containers larger than the cap grow as elements are added,
exactly as counts under the cap already did. This mirrors thrift.PreallocSize in
the Go binding and prealloc_size in Rust.
Client: java
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: cpp,java,kotlin,nodejs,py,rb,rs
The SSL cross tests against a Python server have failed since the server side
validation callback became a real check on Python 3.12 and later.
TSSLServerSocket matches a client certificate against the address the
connection arrived from, and test/keys/client.crt carries no subjectAltName,
so every *-ip-ssl case with a Python server fails: py-py, py-cpp, py-rb,
py-rs, py-nodejs and py-kotlin.
test/keys/client_v3.crt exists for exactly this case. It carries 127.0.0.1,
::1, ::ffff:127.0.0.1 and localhost, which covers every peer address the
servers actually see.
Presenting it is not enough on its own. Every certificate under test/keys
carries the same distinguished name and none carries an authority key
identifier, so OpenSSL treats each one as self-signed and never chains it to
CA.pem. A certificate is accepted only when the verifying side holds that
exact certificate, and a store keeps only one certificate per distinguished
name, so client.crt and client_v3.crt cannot both be trusted at once and the
servers have to move over together with the clients.
test/py/TestServer.py and test/rb/integration/TestServer.rb therefore trust
client_v3.crt, and every client that presents a certificate to them sends it:
Python, Ruby, Rust and Node.js were already changed, C++ follows here, and Java
and Kotlin through .clientkeystore, regenerated from client_v3 with the
algorithms that were already in it. Go sends no client certificate at all and
is unaffected; the Ruby server accepts that because it verifies without
VERIFY_FAIL_IF_NO_PEER_CERT.
client.crt is left unchanged so the Python unit tests keep exercising the
rejection path, where it is CLIENT_CERT_NO_IP.
Verified by running test/py/TestServer.py against test/py/TestClient.py over
binary/buffered/--ssl in three configurations: client.crt against the old store
fails on the address check, client_v3.crt against the old store fails with
unknown ca, and client_v3.crt against the new store passes. The Ruby context
was exercised the same way against both certificates, and the regenerated
keystore was read back with keytool.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Client: java
The `props` parameter is a pass through to `Sasl.createSaslClient` and
previously had a narrower type than required by `Sasl.createSaslClient`
which restricted use artificially.
Client: java
The thrift-maven-plugin in contrib has not been published since ~2017,
is not part of the release build or test procedures, and merely shelled
out to an externally installed thrift executable.
Remove the unmaintained plugin from contrib and document how to generate
Java Thrift code in Maven projects using standard exec-maven-plugin and
build-helper-maven-plugin in lib/java/README.md.
Client: java
receiveSaslMessage() reads a five-byte header and allocates the payload buffer
from the length it declares, before readAll blocks for the payload itself. It
runs during the handshake, so the peer choosing that length has not
authenticated yet, and the only bound was max message size -- 100 MB by
default, which describes a whole message rather than one negotiation header.
Bound it by max frame size instead. The nonblocking SASL server already holds
its own negotiation frames to that value (sasl/SaslNegotiationFrameReader), and
this is the same protocol, so the two paths now agree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TAsyncMethodCall sizes the response frame from the four bytes the peer
sends ahead of it, with no check of any kind:
if (sizeBuffer.remaining() == 0) {
state = State.READING_RESPONSE_BODY;
frameBuffer = ByteBuffer.allocate(TFramedTransport.decodeFrameSize(sizeBufferArray));
}
decodeFrameSize returns a signed int, so the peer picks anything from
-2147483648 to 2147483647 and ByteBuffer.allocate is handed it. A negative
one raises IllegalArgumentException, which is not a TTransportException and
reaches the callback as it is; a large positive one is allocated, and
ByteBuffer.allocate zeroes the array, so the memory is resident rather than
reserved.
It is the odd one out among the three places lib/java reads a frame size.
TFramedTransport.readFrame and TFastFramedTransport.readFrame both refuse a
negative size and one over getMaxFrameSize(). AbstractNonblockingServer's
FrameBuffer refuses a size <= 0, one over trans_.getMaxFrameSize(), and on
top of that defers a frame that would push the server past
MAX_READ_BUFFER_BYTES. Only the async client takes the number as it comes,
so apply the same two checks with the same messages.
Worth a release note: maxFrameSize defaults to 16384000, so an async client
now refuses a response frame larger than that where it used to allocate it.
setMaxFrameSize raises the bound with it.
Three tests drive a real TAsyncMethodCall against a socket that answers
with a frame size and no body. They assert the buffer that was allocated
rather than only that the call failed, because a response body that never
arrives ends the call either way. Two fail before the change -- "allocated
65536 for a frame that is over the maximum", and "a negative size reached
ByteBuffer.allocate: java.lang.IllegalArgumentException: capacity < 0:
(-1 < 0)". The third reads a frame of exactly the maximum end to end and
passes either way.
Client: java
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wrapping a transport in TFramedTransport or TFastFramedTransport overwrote
the maximum frame size it was configured with, even when no maximum had
been asked for:
public TFramedTransport(TTransport transport) throws TTransportException {
this(transport, TConfiguration.DEFAULT_MAX_FRAME_SIZE);
}
public TFramedTransport(TTransport transport, int maxLength) ... {
TConfiguration _configuration = ...transport.getConfiguration();
_configuration.setMaxFrameSize(maxLength);
So
socket.getConfiguration().setMaxFrameSize(1024);
TTransport framed = new TFramedTransport(socket);
left the socket configured for 16384000. Two things went wrong at once: a
maximum the operator set was replaced by the library default, and the
object written into belongs to the transport underneath, so anything else
sharing that TConfiguration changed with it. A layered transport has no
configuration of its own to write into -- TLayeredTransport.getConfiguration()
delegates to the inner transport by design.
TFastFramedTransport(underlying) and TFastFramedTransport(underlying,
initialBufferCapacity) did the same, as did TFramedTransport.Factory() and
TFastFramedTransport.Factory() / Factory(initialCapacity), so a server
configured with new TFramedTransport.Factory() raised the limit on every
connection it accepted.
Now a constructor or factory that was not given a maximum passes back the
one the transport already carries, so wrapping changes nothing. Given one
explicitly, the behaviour is unchanged: it is applied, as the argument's
javadoc promises.
Go solved this for its own deprecated constructors, and the reason is
written into its TConfiguration: noPropagation exists "to avoid overriding
underlying TTransport/TProtocol's cfg by accidental propagations". C++,
netstd, Delphi and Haxe all read the configured maximum and never write it.
Four tests in TestTFramedTransport, inherited by TestTFastFramedTransport
so both classes are covered: wrapping keeps a lowered maximum, the factory
keeps it, an explicit maximum is still applied, and -- the point of the
other three -- a frame declaring 2048 bytes is still refused by a transport
configured for 1024 after being wrapped. Three of the four fail on each
class beforehand; the explicit-maximum one is a regression guard and passes
either way.
Client: java
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TFramedTransport and TFastFramedTransport read a frame header that tells them
the exact size of the message they are about to hand to the protocol, and never
tell the budget. checkReadBytesAvailable is therefore measured against
TConfiguration.maxMessageSize for the life of the connection, so a 68-byte frame
may declare a 64 MB field and the array is allocated before the shortfall is
found.
Binding it needs an API that does not exist. resetConsumedMessageSize(-1) is the
only genuine full reset and it is protected, so a layered transport cannot call
it on its inner transport; updateKnownMessageSize() refuses to grow a budget an
earlier smaller frame narrowed, and carries that frame's consumption forward
besides. So this adds resetMessageSizeAndConsumedBytes(long) to TTransport,
mirroring netstd's ResetMessageSizeAndConsumedBytes, implemented on
TEndpointTransport, delegated by TLayeredTransport, and a no-op on
TFileTransport alongside its existing no-op budget methods.
readFrame() then resets twice, for two different reasons. Before the framing
reads, because what is left of the previous frame's bound describes a frame we
are done with, and an inner transport that decrements on read -- TMemoryBuffer,
or the TMemoryInputTransport the nonblocking server uses -- would otherwise
refuse a frame larger than the last one. After them, binding to the frame,
discarding what the framing itself spent rather than charging it twice.
TMemoryInputTransport.reset() binds to the buffer it is given, as its
constructor already did. Reads there are clamped to the buffer and short-read
rather than checked against the budget, so this only tightens what the protocol
may declare.
The new abstract method is a source-compatibility break for anything extending
TTransport directly rather than through TEndpointTransport or TLayeredTransport;
four test doubles in this repository needed it.
Nine tests, eight of them written before the change, of which three fail
unmodified. The rest describe traffic that is legitimate today: the natural
implementation of frame binding breaks them, which is what makes them worth
having. The ninth records why the bound cannot go on the inner transport --
with the nonblocking server's shape, getting the frame off the wire spends the
inner budget entirely, and without the first reset the frame's own bytes are
then unreadable. 346 tests pass.
Unlike C++ (THRIFT-5371), Java does not accumulate across frames: the socket
path never decrements the budget on read, its only reset being the flush() one
on the write side. What it shares is the budget never being narrowed to the
frame.
Client: java
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Starting from go 1.27, it starts to enforce RFC 5280 IP SAN parsing [1],
and reject certs with IPv4-mapped IPv6 address.
Our pre-generated test certs contains one such IP (`::ffff:127.0.0.1`)
so it starts to fail in go test server when we update to go 1.27.
This change removes that IP from all the test certs and keep other
things in the test certs (mostly) intact, including all the private
keys, to prepare for the go 1.27 update.
[1]: https://github.com/golang/go/commit/a5b2a19336774bf0f59a0d43eafdb66b3f1c1c5b
Client: cpp,java
Two follow-ups to the recent TJSONProtocol message-size work:
lib/java: rewrap a javadoc comment in TestMessageSizeLimits so it matches
google-java-format; spotlessCheck rejected the previous wrapping.
lib/cpp: test_tthriftjsonprotocol_read_check_exception configured a message
size of 2 bytes, which the JSON reader now rejects while reading the container
element type name ("i32") rather than at the container size check the test
targets. readListBegin() then threw earlier in the stream than before and the
following readListEnd() hit an unexpected character. Raise the configured limit
so the container header is readable and enlarge the declared container so the
size check still exceeds the limit, keeping the original intent of the test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Client: java
readJSONString() and readJSONNumericChars() decode a quote-delimited string and
a character-by-character numeric literal with no declared length, so unlike the
length-prefixed protocols they had no size gate and could grow without bound.
Track the running byte count and reject a single string or numeric literal once
it exceeds the configured MaxMessageSize, so it is bounded the way a
length-prefixed read already is. The count is compared against MaxMessageSize
directly rather than the decrementing remaining-message-size counter, so the
bound stays tight on the non-decrementing stream read path and does not further
tighten transports that already decrement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: java
TSaslTransport.readFrame() allocated the data-frame buffer directly from the
wire-declared length, checking only for negative values. The SASL negotiation
phase (receiveSaslMessage) and the layered TFramedTransport already bound the
declared size by TConfiguration; apply the same getMaxFrameSize() limit here so
the post-handshake data path rejects a frame larger than the configured maximum
before allocating the buffer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client: java
FrameReader allocated the payload buffer directly from the header-declared
size. The layered TFramedTransport already bounds incoming frames by
TConfiguration.getMaxFrameSize(); apply the same limit here so the SASL
negotiation and data frame paths reject a payload size larger than the
configured maximum before allocating the buffer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client: java,kotlin
Kotlin-generated struct and exception serialization routes through
TProtocol.readStruct() and writeStruct() (the callback helpers in the shared
Java runtime), which did not bound recursion depth, so a deeply nested or
cyclic value was read or written without a limit. Kotlin-generated unions
extend the shared org.apache.thrift.TUnion, whose read/write path calls
readStructBegin()/writeStructBegin() directly and was likewise unbounded --
this affects Java-generated unions too.
Wrap the readStruct()/writeStruct() bodies, and TUnion's standard- and
tuple-scheme read/write bodies, in incrementRecursionDepth() / try ... finally /
decrementRecursionDepth(), reusing the existing TProtocol infrastructure (limit
from TConfiguration.getRecursionLimit(), default 64, TProtocolException.DEPTH_LIMIT
on excess). The Java generator emits an inline StandardScheme that already
increments separately and does not call the readStruct()/writeStruct() helpers,
so Java struct serialization is unaffected (no double-counting); only
Kotlin-generated code consumes them. A union's standard and tuple schemes are
mutually exclusive per protocol, so a union increments exactly once per level.
Replace the isolated counter test with a generated-code round-trip
(lib/kotlin RecursionDepthTest): the recursive types
CoRec/CoRec2/RecTree/RecUnion/RecError are generated from a new
src/test/resources/RecursionDepthTest.thrift (mirroring test/Recursive.thrift)
and driven through TBase.read()/write() -- chains at the limit round-trip,
chains one past it (write and read) are rejected with DEPTH_LIMIT for structs,
unions and exceptions, a wide structure confirms the counter is decremented per
sibling, and a cyclic graph is rejected instead of recursing without bound.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Client: java
TIOStreamTransport.read() does not call checkReadBytesAvailable() or
countConsumedMessageBytes(), so TZlibTransport inherited no limit
enforcement on decompressed bytes despite having a TConfiguration.
Override read() in TZlibTransport to check the pre-read budget and
count every decompressed byte against remainingMessageSize so that
maxMessageSize is enforced cumulatively across the full session.
Also adds testMessageSizeLimit to TestTZlibTransport covering this path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client: java
- consumeBuffer() in TMemoryInputTransport and AutoExpandingBufferReadTransport
now decrements remainingMessageSize inline so fast-path buffer reads are
counted toward the per-message size limit
- AutoExpandingBufferReadTransport.fill() resets remainingMessageSize at the
start of each frame, matching TMemoryInputTransport.reset() behavior in
TFramedTransport
- TBinaryProtocol.readString() now checks stringLengthLimit before the
fast-path branch
- Add TestMessageSizeLimits covering all fixed code paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client: java
TProtocolUtil.skip() previously defaulted to Integer.MAX_VALUE for its
recursion limit, making it effectively unbounded. This change aligns
the default with C++, Go, and Node.js (all use 64) by setting
maxSkipDepth to 64. Applications needing deeper nesting can still call
setMaxSkipDepth() to raise the limit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client: py,go,php,java,netstd,delphi,haxe,lua,swift,rb,dart,erl,rs
C++ and Node.js already enforce a 10-byte ceiling on varint reads.
This change brings all remaining runtimes into parity by raising a
protocol exception (INVALID_DATA) when more than 10 bytes are consumed
without finding a terminating byte.
Java: both fast-path and slow-path varint readers are covered.
Erlang: read_varint/3 gains a when Count < 10 guard clause; call sites
updated to propagate the error tuple rather than crashing on badmatch.
Rust: VarIntReader trait replaced with local bounded loops; zigzag
decode moved to free functions independent of the integer_encoding crate.
Ruby: both the pure-Ruby implementation and the C extension are fixed;
read_varint32 gains a separate 5-byte limit instead of delegating to
read_varint64.
Named constants replace bare integer literals in all runtimes, with a
comment explaining the derivation (ceil(64/7) = 10, ceil(32/7) = 5).
Java and Rust enforce both the 5-byte (32-bit) and 10-byte (64-bit)
limits; other runtimes enforce the 10-byte ceiling uniformly.
Tests added for py, go, php, java (all four original runtimes), plus
rb, swift, dart, and rs which have existing unit-test infrastructure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client: java
Set EndpointIdentificationAlgorithm to HTTPS on the SSLEngine parameters
in the TNonblockingSSLSocket constructor so that the server certificate
CN/SAN is validated against the target hostname during the TLS handshake.
This mirrors the behavior of TSSLTransportFactory.createClient() and
aligns the async SSL client with the sync client.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Client: java
Set EndpointIdentificationAlgorithm to HTTPS on SSLSocket parameters
in createClient() so that the server certificate CN/SAN is validated
against the target hostname during TLS handshake.
(03)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Client: Java
Patch: CJCombrink
This closes#3334
- Using gradle spotlessApply
Adding a decorator to assist with code that used the uuid field already in Java
- For those who can't migrate to new UUID format
- Also captures the code that was used before
Fix UUID implementation in Java to be according to the Thrift UUID specification
- Don't do byte swapping when writing to and reading from the wire.
- This is a breaking change for anyone using UUID between Java servers
- For users that depends on this behaviour a TProtocolDecorator can be used to get back the old behaviour
Java had TConfiguration.recursionLimit (default 64) and
TProtocolException.DEPTH_LIMIT defined but the limit was not actually
enforced during struct deserialization. This brings Java in line with
C++ which already enforces recursion limits.
Changes:
- Add recursionDepth_ counter and increment/decrement methods to TProtocol
- Modify Java code generator to emit try-finally depth tracking in
generate_standard_reader() and generate_java_struct_tuple_reader()
Structures exceeding the configured recursion limit now throw
TProtocolException(DEPTH_LIMIT) during deserialization.