Dmytro Shteflyuk created THRIFT-6124:
----------------------------------------
Summary: Ruby deserializer retains stale values when reusing
target objects
Key: THRIFT-6124
URL: https://issues.apache.org/jira/browse/THRIFT-6124
Project: Thrift
Issue Type: Bug
Components: Ruby - Library
Reporter: Dmytro Shteflyuk
Assignee: Dmytro Shteflyuk
h3. Problem
The Ruby {{Deserializer}} API accepts an existing generated object as its
target. Struct and union readers currently assign only fields that are present
in the incoming payload; they do not clear values left by an earlier
deserialization.
For structs, an optional field omitted from the next payload therefore keeps
its previous value. A field whose incoming wire type does not match the
generated definition can have the same result. Generated defaults also retain
caller-modified values instead of being restored before the next message is
read.
For unions, an unknown or mismatched field is skipped. If the target already
contains a variant, the old discriminator and value remain in place, so
validation can succeed even though the new payload did not contain that variant.
h3. Client impact
Applications that reuse generated Ruby objects to reduce allocations can
observe values that were not present in the latest message. Required-field
validation can also be satisfied by a value retained from an earlier message. A
reused union can represent an unknown newer field as an unrelated variant from
the preceding message.
The behavior is present in both the pure-Ruby and native readers.
h3. Reproduction
Given these generated types:
{code:thrift}
struct Record {
1: optional string note
}
union Choice {
1: string text
}
{code}
Reusing one {{Record}} instance retains the first message's optional value:
{code:ruby}
serializer = Thrift::Serializer.new
deserializer = Thrift::Deserializer.new
target = Record.new
deserializer.deserialize(
target,
serializer.serialize(Record.new(note: "old"))
)
puts target.note
# old
deserializer.deserialize(
target,
serializer.serialize(Record.new)
)
puts target.note.inspect
# "old", although the second payload omitted the field
{code}
The equivalent union case occurs when a reused {{Choice}} containing {{text}}
reads a payload containing only an unknown field: the previous {{text}}
discriminator and value remain set.
h3. Expected behavior
Every read should begin from the same field state as a newly initialized
target: declared defaults restored, other struct fields unset, and the union
discriminator and value cleared.
Absent, unknown, or type-mismatched fields must not expose values from an
earlier message. Required-field validation must evaluate only the current
message. If reading fails partway through, values that existed before that read
must not survive; the target should not be treated as a successfully
deserialized value.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)