The lengths that libyaml and libfyaml report as size_t are cast to rb_len_t instead of long when they become String lengths. It falls back to long on Ruby versions that do not define HAVE_RB_LEN_T.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
merge_key was added empty in 2025502c while merge handling remained inline in revive_hash, and it has never been called. dump_list was added empty in 59ecddb while array dumping was implemented inline in visit_array_subclass, and it has never been called. Psych visitor dispatch only targets visit_* methods.
https://github.com/ruby/psych/commit/e61b6fd005
The parser calls back into Ruby for every event, so a handler can call
#parse again on the same object. That reinitialised the parser and
re-pointed it at new input while the outer loop was still driving it,
which left the outer loop reading the inner document's freed buffer, or
resuming on a parser with no input at all and tripping libyaml's
read_handler assertion.
Psych::Exception is used so the caller can rescue it, and the in-use
flag is cleared with rb_ensure so a parser stays reusable afterwards.
https://github.com/ruby/psych/commit/5f2e37dc1c
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Once libyaml has set stream_end_produced, every further
yaml_parser_parse succeeds with a zeroed event, so YAML_STREAM_END_EVENT
can no longer be reached and the loop calls handler#empty forever at
full CPU. A parser left in that state should terminate instead.
No test covers this directly because the loop only reaches it through a
reentrant parse, which the parser now rejects outright.
https://github.com/ruby/psych/commit/a667509a4a
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every case of the event switch overwrote the rb_protect state, so only
the second result was ever tested and the state from the event_location
call was dropped. The parse then returned normally with the exception
silently discarded.
https://github.com/ruby/psych/commit/186c3fa343
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace `tool/update-deps` with a Ruby source scanner so dependency
updates no longer require GNU make, a configured build, or compiler
preprocessor output.
Keep compact source mappings and scanner declarations in dependency
files, and expand complete rules into the build directory. Avoid the
temporary fixture headers required by the compiler-driven prototype.
Use the generated rules from GNU make, BSD make, and NMake builds.
Support out-of-tree and read-only source trees, and retain expanded
dependencies shipped in release archives when baseruby is unavailable.
The libfyaml-dev package on Ubuntu is 0.8, which segfaults psych's
emitter, while 0.9.6 (used on macOS via Homebrew) passes the whole suite.
Build the same 0.9.6 release from source in the Linux CI job, and reject
libfyaml older than 0.9 in extconf so users get a clear error instead of
a runtime crash. Also drop an unused variable in set_canonical.
https://github.com/ruby/psych/commit/88ac4a0cc0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reject canonical output with NotImplementedError instead of silently
ignoring the request, and honor the implicit flag in start_sequence and
start_mapping so an implicit tag is not printed as a redundant verbose
tag. Relax the unquoted-boolean dump test to allow an optional document
end marker. Also correct the version docstrings and a stale comment
about how the parser collects diagnostics.
https://github.com/ruby/psych/commit/23f60daff1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mark the reconstructed SyntaxError message UTF-8 instead of US-ASCII, so
a diagnostic that embeds a multibyte snippet of the input does not raise
Encoding::CompatibilityError when concatenated with UTF-8. Add
RB_GC_GUARD for the anchor and tag strings in the emitter (matching the
existing guard on the scalar value) so their C pointers cannot dangle if
a GC runs inside fy_emit_event_create.
https://github.com/ruby/psych/commit/1c73f43df1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The xcalloc'd tag buffers leaked when StringValue, the tuple-length
check, or the emit raised mid-way. Wrap the work in rb_ensure so the
buffers are always freed, and keep the exported directive strings in a
Ruby array so the GC cannot reclaim them while their C pointers are in
use.
https://github.com/ruby/psych/commit/6fdede0177
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
libfyaml only consumes UTF-8, so a UTF-16 IO fed through the chunked
reader reached it as raw bytes and was rejected as invalid UTF-8. When
the IO's external encoding is UTF-16LE/BE, slurp the whole stream and
transcode it first; a 2-byte unit could otherwise straddle a read
boundary. Other non-UTF-8 encodings stay raw and libfyaml rejects them,
matching psych's UTF-8/UTF-16-only IO contract (Shift_JIS still raises).
https://github.com/ruby/psych/commit/4bd1343551
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the tag when plain_implicit or quoted_implicit is set, matching how
libyaml omits a tag that the value resolves to on reload; otherwise nil
emitted as "!<tag:yaml.org,2002:null>" instead of an empty scalar. Honor
the plain hint when choosing the scalar style, and restore the Check_Type
guards on anchor and tag so non-string arguments raise TypeError.
https://github.com/ruby/psych/commit/ff15ddf399
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Create a fresh parser per parse instead of reusing one via
fy_parser_reset(), which left default tag handles unset and rejected
bare ("---"-less) tag-led documents. Recover the real message and
position by switching the parser's own diagnostic object to collect
mode; creating a replacement diag crashes libfyaml 0.9.6. Drop the
spurious empty tag directive libfyaml reports.
https://github.com/ruby/psych/commit/d383361c23
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scalar type resolution happens in ScalarScanner, not the C backend, so
swapping to libfyaml alone still resolved yes/no/on/off to booleans. Key
the boolean set on Psych::BACKEND so the libyaml default keeps the YAML
1.1 set while the experimental libfyaml backend follows 1.2.
https://github.com/ruby/psych/commit/b374869c59
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Built only with --enable-libfyaml; without the flag the default libyaml
backend is unchanged. The parser and emitter are reimplemented against
libfyaml's event API in separate translation units guarded by
PSYCH_USE_LIBFYAML, and the backend is not supported on Windows.
https://github.com/ruby/psych/commit/51f2493a2e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
If pos succeeded but the later seek failed, the rescue silently
discarded the bytes read to check for a BOM. Only the initial pos call
is expected to fail, for non-seekable IOs, before anything is consumed.
https://github.com/ruby/psych/commit/57f34ae625
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
libyaml only discounts the BOM when it detects the stream encoding by
itself. Psych passes the encoding explicitly whenever it is known, and
on that path libyaml counts the BOM as a first-line character, shifting
every token on the first line one column right and silently terminating
a block mapping at the second line.
https://github.com/ruby/psych/issues/331https://github.com/ruby/psych/commit/924c15ed1f
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
!ruby/array, !seq and !ruby/string carry the same exposure just fixed
for the hash tags: the loader allocated the named class and replaced its
contents without checking the class was actually an Array or String
subclass. Apply the same subclass check so a permitted unrelated class
can no longer be allocated and populated through these tags.
https://github.com/ruby/psych/commit/73f0131b13
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
!ruby/hash-with-ivars, !ruby/hash and !map are only emitted for Hash
subclasses, but the loader allocated whatever class the tag named and
populated its ivars directly. That let a permitted non-Hash class be
instantiated with attacker-chosen ivars, bypassing its init_with
validation. Verify the resolved class is a Hash subclass before use.
https://github.com/ruby/psych/commit/b08a71012e
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
safe_load resolved !ruby/encoding directly via ::Encoding.find,
bypassing the permitted_classes check that !ruby/object:Encoding
already honors. Load it through the class loader so Encoding is only
deserialized when permitted.
https://github.com/ruby/psych/commit/f2e4b9d365
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clamping the over-read at exactly the requested size could split a
multibyte character, since the string an IO returns may carry a
non-binary encoding. Round the cut down to the last character boundary
at or before the size so the bytes handed to libyaml are always whole
characters.
https://github.com/ruby/psych/commit/6201ae17c1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
io_reader trusted IO#read to honour its size argument and copied
RSTRING_LEN(string) bytes into libyaml's fixed-capacity buffer. An
IO-like object whose #read over-returns wrote past the buffer end, a
heap out-of-bounds write reachable from Psych.load/safe_load/parse.
Clamp the copied length to the requested size.
https://github.com/ruby/psych/commit/99ecd94560
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes: ruby#685
This feature can easily break how you use other gems like factory_bot or prawn.
https://github.com/ruby/psych/pull/747#issuecomment-3413139525
> But I kind of think we should leave `psych/y` around. If people really want to use it they could require the file.
If you miss the function in Kernel, you can require it interactively or add it to `.irbrc`:
```ruby
require 'psych/y'
```
https://github.com/ruby/psych/commit/f1610b3f05
* rb_struct_initialize() does not accept a Hash, and it's very brittle
to pass `[{...}]` and to rely on that C function using rb_keyword_given_p().
It basically worked accidentally, by having **members in the caller of the caller.
Such logic when Struct#initialize is defined in Ruby (as in TruffleRuby) is basically impossible to implement,
because it's incorrectly treating positional arguments as keyword arguments.
* rb_struct_initialize() is used in CRuby to set members of Data instances in marshal.c (there is no rb_data_initialize() yet).
There, the code passes an Array of members values for Data (and for Struct which are not `keyword_init: true`):
https://github.com/ruby/ruby/blob/48c7f349f68846e10d60ae77ad299a38ee014479/marshal.c#L2150-L2176
So we should do the same in psych.
* Rename to init_data since it's only used for Data.
* See https://github.com/ruby/psych/pull/692#discussion_r2483947279.
https://github.com/ruby/psych/commit/3550148378
If pkg_config returns a truthy value, it found the library and added it
to the global values for the Makefile.
Calling `find_library` after a successful `pkg_config` causes -lyaml to
appear twice in the LIBS variable in the resulting Makefile, and causes
ld on macOS to emit a warning:
$ bundle exec rake compile 2>&1 | grep warning
ld: warning: ignoring duplicate libraries: '-lyaml'
https://github.com/ruby/psych/commit/cb5e3d465c
In Ruby < 3.0, the superclass of StringIO was actually already `Data`,
but it doesn't have the expected shape. So, on these earlier versions it errors:
> NoMethodError: undefined method `members' for #<StringIO:0x00005641dd5f2880>
> vendor/bundle/ruby/2.6.0/gems/psych-5.2.5/lib/psych/visitors/yaml_tree.rb:170:in `visit_Data'
This test doesn't fail on 2.7, presumably because it can pull in a newer `stringio` version.
https://github.com/ruby/psych/commit/0f40f56268