Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 46 additions & 41 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 20 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,26 @@ python = "^3.10"
# a wheel and never runs setup.py on DBR LTS -- the build-time break cannot
# trigger. The `DBR LTS Install` CI check (.github/workflows/dbr-lts-install.yml)
# installs the built artifact on real DBR LTS clusters and is the authoritative
# gate for this. Cap at <0.25.0: thrift is pre-1.0, each 0.x minor can carry
# breaking changes or packaging regressions (see 0.23.0), so bump this
# deliberately once a new minor ships and the DBR-LTS gate proves it safe.
thrift = "~=0.24.0"
# gate for this.
#
# Ceiling raised to <0.26.0 for 0.25.0 (released 2026-09-30, THRIFT-6067 series
# follow-up), which fixes 61 CVEs across language bindings, several affecting
# the Python binding this connector uses (see #969), e.g. CVE-2026-66858
# (skip() recursion-limit bypass in the Python accelerator) and CVE-2026-85494
# (framed transport / binary protocol size a read buffer from a peer-declared
# length with no effective maximum). 0.25.0 ships the same prebuilt wheel
# matrix as 0.24.0 (manylinux2014/macOS/musl/Windows, cp310-cp314), so the
# DBR LTS build-time risk above does not apply; the `DBR LTS Install` CI gate
# remains the authoritative check. thrift is pre-1.0, so keep this ceiling one
# minor ahead and bump deliberately once a newer minor ships and the DBR-LTS
# gate proves it safe -- do not use an open-ended `^`/`>=` constraint.
#
# NOTE: 0.25.0 also changes TBinaryProtocol's default `string_length_limit`
# from unbounded to ~15.6 MiB (see the explicit `string_length_limit=None` /
# `container_length_limit=None` passed in thrift_backend.py's protocol
# construction, which preserves the connector's pre-0.25 unbounded behavior
# for its own already-bounded, trusted-server result stream).
thrift = ">=0.24.0,<0.26.0"
pandas = [
{ version = ">=1.2.5,<4.0.0", python = ">=3.10,<3.13" },
{ version = ">=2.2.3,<4.0.0", python = ">=3.13" }
Expand Down
19 changes: 18 additions & 1 deletion src/databricks/sql/backend/thrift_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@
TIMESTAMP_AS_STRING_CONFIG = "spark.thriftserver.arrowBasedRowSet.timestampAsString"
DEFAULT_SOCKET_TIMEOUT = float(900)

# thrift>=0.25.0 changed TBinaryProtocol's default string_length_limit from
# unbounded (None) to DEFAULT_STRING_LENGTH_LIMIT (DEFAULT_MAX_FRAME_SIZE,
# ~15.6 MiB) as part of its CVE-2026-85494 fix (peer-declared length with no
# effective maximum). That default is sized for untrusted/unbounded peers;
# this connector already bounds its own result stream via the TLS-verified,
# server-negotiated `buffer_size_bytes` (default 100 MiB, see client.py's
# DEFAULT_RESULT_BUFFER_SIZE_BYTES) and the inline Arrow batches in
# TFetchResultsResp routinely exceed thrift's new ~15.6 MiB cap. Pass these
# explicitly so upgrading thrift does not silently cap -- or regress -- the
# connector's own, already-bounded result size behavior.
THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT = None
THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT = None

# see Connection.__init__ for parameter descriptions.
# - Min/Max avoids unsustainable configs (sane values are far more constrained)
# - 900s attempts-duration lines up w ODBC/JDBC drivers (for cluster startup > 10 mins)
Expand Down Expand Up @@ -237,7 +250,11 @@ def __init__(
self._transport.setTimeout(timeout and (float(timeout) * 1000.0))

self._transport.setCustomHeaders(dict(http_headers))
protocol = thrift.protocol.TBinaryProtocol.TBinaryProtocol(self._transport)
protocol = thrift.protocol.TBinaryProtocol.TBinaryProtocol(
self._transport,
string_length_limit=THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT,
container_length_limit=THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT,
)
self._client = TCLIService.Client(protocol)

try:
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/test_thrift_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,67 @@ def test_headers_are_set(self, t_http_client_class):
{"header": "value"}
)

@patch("databricks.sql.auth.thrift_http_client.THttpClient")
def test_binary_protocol_preserves_unbounded_string_and_container_limits(
self, t_http_client_class
):
"""thrift>=0.25.0 changed TBinaryProtocol's default string_length_limit
from unbounded (None) to ~15.6 MiB (DEFAULT_STRING_LENGTH_LIMIT) as part
of its CVE-2026-85494 fix. The connector must pass these limits through
explicitly so a thrift upgrade cannot silently cap -- or regress -- the
size of result data (e.g. inline Arrow batches) it can read, since the
connector already governs its own result size via buffer_size_bytes.
"""
import thrift.protocol.TBinaryProtocol

with patch(
"thrift.protocol.TBinaryProtocol.TBinaryProtocol"
) as mock_protocol_class:
ThriftDatabricksClient(
"foo",
123,
"bar",
[],
auth_provider=AuthProvider(),
ssl_options=SSLOptions(),
http_client=MagicMock(),
)

_, kwargs = mock_protocol_class.call_args
self.assertIsNone(kwargs.get("string_length_limit"))
self.assertIsNone(kwargs.get("container_length_limit"))

def test_binary_protocol_reads_result_larger_than_thrift_default_limit(self):
"""Guard against relying on thrift's new (>=0.25.0) default
string_length_limit of ~15.6 MiB, which is smaller than the
connector's own DEFAULT_RESULT_BUFFER_SIZE_BYTES (100 MiB). A field
larger than thrift's default limit, but within the connector's own
result-size bound, must still read successfully.
"""
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol

from databricks.sql.backend.thrift_backend import (
THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT,
THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT,
)

oversized_payload = b"x" * (
20 * 1024 * 1024
) # 20 MiB > thrift's ~15.6 MiB default

write_buffer = TTransport.TMemoryBuffer()
TBinaryProtocol.TBinaryProtocol(write_buffer).writeBinary(oversized_payload)

read_buffer = TTransport.TMemoryBuffer(write_buffer.getvalue())
protocol = TBinaryProtocol.TBinaryProtocol(
read_buffer,
string_length_limit=THRIFT_BINARY_PROTOCOL_STRING_LENGTH_LIMIT,
container_length_limit=THRIFT_BINARY_PROTOCOL_CONTAINER_LENGTH_LIMIT,
)

self.assertEqual(protocol.readBinary(), oversized_payload)

def test_proxy_headers_are_set(self):

from databricks.sql.common.http_utils import create_basic_proxy_auth_headers
Expand Down