Skip to content

Commit b9b48cd

Browse files
kpumukcodex
andcommitted
THRIFT-6126: Handle unknown Compact and JSON types
Client: rb Co-Authored-By: OpenAI Codex (GPT-5.6) <codex@openai.com>
1 parent 95d4e3e commit b9b48cd

7 files changed

Lines changed: 167 additions & 52 deletions

File tree

lib/rb/ext/compact_protocol.c

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,11 @@ static int get_compact_type(VALUE type_value) {
9292
} else if (type == TTYPE_UUID) {
9393
return CTYPE_UUID;
9494
} else {
95-
char str[50];
96-
sprintf(str, "don't know what type: %d", type);
97-
rb_raise(rb_eStandardError, "%s", str);
98-
return 0;
95+
rb_exc_raise(get_protocol_exception(
96+
INT2FIX(PROTOERR_INVALID_DATA),
97+
rb_sprintf("Unknown compact type: %d", type)
98+
));
99+
return 0; /* unreachable */
99100
}
100101
}
101102

@@ -416,10 +417,11 @@ static int8_t get_ttype(int8_t ctype) {
416417
} else if (ctype == CTYPE_UUID) {
417418
return TTYPE_UUID;
418419
} else {
419-
char str[50];
420-
sprintf(str, "don't know what type: %d", ctype);
421-
rb_raise(rb_eStandardError, "%s", str);
422-
return 0;
420+
rb_exc_raise(get_protocol_exception(
421+
INT2FIX(PROTOERR_INVALID_DATA),
422+
rb_sprintf("Unknown compact type: %d", ctype)
423+
));
424+
return 0; /* unreachable */
423425
}
424426
}
425427

lib/rb/lib/thrift/protocol/compact_protocol.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,22 @@ def self.is_bool_type?(b)
8989

9090
def self.get_ttype(compact_type)
9191
val = COMPACT_TO_TTYPE[compact_type & 0x0f]
92-
raise "don't know what type: #{compact_type & 0x0f}" unless val
93-
val
92+
return val if val
93+
94+
raise ProtocolException.new(
95+
ProtocolException::INVALID_DATA,
96+
"Unknown compact type: #{compact_type & 0x0f}"
97+
)
9498
end
9599

96100
def self.get_compact_type(ttype)
97101
val = TTYPE_TO_COMPACT[ttype]
98-
raise "don't know what type: #{ttype & 0x0f}" unless val
99-
val
102+
return val if val
103+
104+
raise ProtocolException.new(
105+
ProtocolException::INVALID_DATA,
106+
"Unknown compact type: #{ttype}"
107+
)
100108
end
101109
end
102110

lib/rb/lib/thrift/protocol/json_protocol.rb

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -183,42 +183,27 @@ def get_type_name_for_type_id(id)
183183
when Types::UUID
184184
"uid"
185185
else
186-
raise NotImplementedError
186+
raise ProtocolException.new(ProtocolException::INVALID_DATA, "Unknown type id: #{id}")
187187
end
188188
end
189189

190190
def get_type_id_for_type_name(name)
191-
if (name == "tf")
192-
result = Types::BOOL
193-
elsif (name == "i8")
194-
result = Types::BYTE
195-
elsif (name == "i16")
196-
result = Types::I16
197-
elsif (name == "i32")
198-
result = Types::I32
199-
elsif (name == "i64")
200-
result = Types::I64
201-
elsif (name == "dbl")
202-
result = Types::DOUBLE
203-
elsif (name == "str")
204-
result = Types::STRING
205-
elsif (name == "rec")
206-
result = Types::STRUCT
207-
elsif (name == "map")
208-
result = Types::MAP
209-
elsif (name == "set")
210-
result = Types::SET
211-
elsif (name == "lst")
212-
result = Types::LIST
213-
elsif (name == "uid")
214-
result = Types::UUID
191+
case name
192+
when "tf" then Types::BOOL
193+
when "i8" then Types::BYTE
194+
when "i16" then Types::I16
195+
when "i32" then Types::I32
196+
when "i64" then Types::I64
197+
when "dbl" then Types::DOUBLE
198+
when "str" then Types::STRING
199+
when "rec" then Types::STRUCT
200+
when "map" then Types::MAP
201+
when "set" then Types::SET
202+
when "lst" then Types::LIST
203+
when "uid" then Types::UUID
215204
else
216-
result = Types::STOP
205+
raise ProtocolException.new(ProtocolException::INVALID_DATA, "Unknown type name: #{name.inspect}")
217206
end
218-
if (result == Types::STOP)
219-
raise NotImplementedError
220-
end
221-
return result
222207
end
223208

224209
# Static helper functions

lib/rb/spec/compact_protocol_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,35 @@
136136
end
137137
end
138138

139+
it "should reject unknown field and container types as invalid protocol data" do
140+
{
141+
:read_field_begin => [0x1e],
142+
:read_list_begin => [0x1e]
143+
}.each do |reader_method, bytes|
144+
trans = Thrift::MemoryBufferTransport.new(bytes.pack("C*"))
145+
proto = Thrift::CompactProtocol.new(trans)
146+
147+
expect { proto.public_send(reader_method) }.to raise_error(Thrift::ProtocolException, "Unknown compact type: 14") do |error|
148+
expect(error.type).to eq(Thrift::ProtocolException::INVALID_DATA)
149+
end
150+
end
151+
end
152+
153+
it "should report the original unknown type when writing fields and containers" do
154+
{
155+
:write_field_begin => [nil, 99, 1],
156+
:write_list_begin => [99, 1]
157+
}.each do |writer_method, args|
158+
trans = Thrift::MemoryBufferTransport.new
159+
proto = Thrift::CompactProtocol.new(trans)
160+
161+
expect { proto.public_send(writer_method, *args) }.to raise_error(Thrift::ProtocolException, "Unknown compact type: 99") do |error|
162+
expect(error.type).to eq(Thrift::ProtocolException::INVALID_DATA)
163+
end
164+
expect(trans.available).to eq(0)
165+
end
166+
end
167+
139168
it "should decode i32 minima from direct canonical zigzag bytes" do
140169
trans = Thrift::MemoryBufferTransport.new
141170
trans.write(INTEGER_MINIMUM_ENCODINGS[:i32].pack("C*"))

lib/rb/spec/json_protocol_spec.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,8 @@
283283
end
284284

285285
it "should get type name for type id" do
286-
expect { @prot.get_type_name_for_type_id(Thrift::Types::STOP) }.to raise_error(NotImplementedError)
287-
expect { @prot.get_type_name_for_type_id(Thrift::Types::VOID) }.to raise_error(NotImplementedError)
286+
expect { @prot.get_type_name_for_type_id(Thrift::Types::STOP) }.to raise_error(Thrift::ProtocolException, "Unknown type id: 0")
287+
expect { @prot.get_type_name_for_type_id(Thrift::Types::VOID) }.to raise_error(Thrift::ProtocolException, "Unknown type id: 1")
288288
expect(@prot.get_type_name_for_type_id(Thrift::Types::BOOL)).to eq("tf")
289289
expect(@prot.get_type_name_for_type_id(Thrift::Types::BYTE)).to eq("i8")
290290
expect(@prot.get_type_name_for_type_id(Thrift::Types::DOUBLE)).to eq("dbl")
@@ -299,7 +299,7 @@
299299
end
300300

301301
it "should get type id for type name" do
302-
expect { @prot.get_type_id_for_type_name("pp") }.to raise_error(NotImplementedError)
302+
expect { @prot.get_type_id_for_type_name("pp") }.to raise_error(Thrift::ProtocolException, 'Unknown type name: "pp"')
303303
expect(@prot.get_type_id_for_type_name("tf")).to eq(Thrift::Types::BOOL)
304304
expect(@prot.get_type_id_for_type_name("i8")).to eq(Thrift::Types::BYTE)
305305
expect(@prot.get_type_id_for_type_name("dbl")).to eq(Thrift::Types::DOUBLE)
@@ -313,6 +313,20 @@
313313
expect(@prot.get_type_id_for_type_name("lst")).to eq(Thrift::Types::LIST)
314314
end
315315

316+
it "should reject unknown field and container types as invalid protocol data" do
317+
@trans.write('{"1":{"wat":0}}')
318+
@prot.read_struct_begin
319+
expect { @prot.read_field_begin }.to raise_error(Thrift::ProtocolException, 'Unknown type name: "wat"') do |error|
320+
expect(error.type).to eq(Thrift::ProtocolException::INVALID_DATA)
321+
end
322+
323+
trans = Thrift::MemoryBufferTransport.new('["wat",0]')
324+
prot = Thrift::JsonProtocol.new(trans)
325+
expect { prot.read_list_begin }.to raise_error(Thrift::ProtocolException, 'Unknown type name: "wat"') do |error|
326+
expect(error.type).to eq(Thrift::ProtocolException::INVALID_DATA)
327+
end
328+
end
329+
316330
it "should read json syntax char" do
317331
@trans.write('F')
318332
expect { @prot.read_json_syntax_char('G') }.to raise_error(Thrift::ProtocolException)

lib/rb/spec/server_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#
2020
require 'spec_helper'
2121
require 'openssl'
22+
require 'timeout'
2223

2324
describe 'Server' do
2425
describe Thrift::BaseServer do
@@ -49,6 +50,31 @@
4950
end
5051

5152
describe Thrift::SimpleServer do
53+
class EphemeralServerSocket < Thrift::ServerSocket
54+
def initialize(ready)
55+
super('127.0.0.1', 0)
56+
@ready = ready
57+
end
58+
59+
def listen
60+
super
61+
@ready << handle.addr[1]
62+
end
63+
end
64+
65+
class StopAfterVoidHandler
66+
attr_reader :calls
67+
68+
def initialize
69+
@calls = 0
70+
end
71+
72+
def voidMethod
73+
@calls += 1
74+
throw :stop
75+
end
76+
end
77+
5278
before(:each) do
5379
@processor = double("Processor")
5480
@serverTrans = double("ServerTransport")
@@ -106,6 +132,62 @@
106132
expect(@serverTrans).to receive(:close).ordered
107133
expect { @server.serve }.to throw_symbol(:stop)
108134
end
135+
136+
{
137+
Thrift::CompactProtocolFactory.new => proc do
138+
trans = Thrift::MemoryBufferTransport.new
139+
prot = Thrift::CompactProtocol.new(trans)
140+
prot.write_message_begin('unknown', Thrift::MessageTypes::CALL, 1)
141+
trans.write([0x1e, 0].pack('C*'))
142+
trans.read(trans.available)
143+
end,
144+
Thrift::JsonProtocolFactory.new => proc { '[1,"unknown",1,1,{"1":{"wat":0}}]' }
145+
}.each do |protocol_factory, malformed_request|
146+
it "closes a malformed #{protocol_factory} connection and continues accepting clients" do
147+
ready = Queue.new
148+
errors = Queue.new
149+
server_transport = EphemeralServerSocket.new(ready)
150+
handler = StopAfterVoidHandler.new
151+
processor = Thrift::Test::Srv::Processor.new(handler)
152+
server = Thrift::SimpleServer.new(processor, server_transport, nil, protocol_factory)
153+
server_thread = Thread.new do
154+
catch(:stop) { server.serve }
155+
rescue StandardError, ScriptError => error
156+
errors << error
157+
end
158+
server_thread.report_on_exception = false
159+
160+
port = Timeout.timeout(2) { ready.pop }
161+
malformed_client = TCPSocket.new('127.0.0.1', port)
162+
malformed_client.write(malformed_request.call)
163+
malformed_client.close_write
164+
165+
expect(IO.select([malformed_client], nil, nil, 2)).not_to be_nil
166+
peer_closed = begin
167+
malformed_client.readpartial(1)
168+
false
169+
rescue EOFError, Errno::ECONNRESET
170+
true
171+
end
172+
expect(peer_closed).to be(true)
173+
expect(server_thread).to be_alive
174+
175+
valid_transport = Thrift::Socket.new('127.0.0.1', port)
176+
valid_transport.open
177+
valid_protocol = protocol_factory.get_protocol(valid_transport)
178+
Thrift::Test::Srv::Client.new(valid_protocol).send_voidMethod
179+
180+
expect(server_thread.join(2)).to eq(server_thread)
181+
expect(handler.calls).to eq(1)
182+
expect(errors).to be_empty
183+
ensure
184+
malformed_client&.close
185+
valid_transport&.close
186+
server_transport&.close
187+
server_thread&.kill
188+
server_thread&.join
189+
end
190+
end
109191
end
110192

111193
describe Thrift::ThreadedServer do

lib/rb/test/fuzz/fuzz_common.rb

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,11 @@
3131

3232
def ignorable_fuzz_exception?(error)
3333
return true if error.is_a?(Thrift::ProtocolException) ||
34-
error.is_a?(EOFError) ||
35-
error.is_a?(Encoding::UndefinedConversionError)
34+
error.is_a?(EOFError)
3635

3736
[
38-
/don't know what (?:c)?type/,
39-
/Too many fields for union/,
4037
/too big to convert to '(?:int|long)'/,
41-
/bignum too big to convert into 'long'/,
42-
/negative array size/,
43-
/Union fields are not set/
38+
/bignum too big to convert into 'long'/
4439
].any? { |pattern| error.message =~ pattern }
4540
end
4641

0 commit comments

Comments
 (0)