Version in base suite: 1.34-2+deb13u2 Base version: libyaml-syck-perl_1.34-2+deb13u2 Target version: libyaml-syck-perl_1.34-2+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/liby/libyaml-syck-perl/libyaml-syck-perl_1.34-2+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/liby/libyaml-syck-perl/libyaml-syck-perl_1.34-2+deb13u3.dsc changelog | 13 patches/Fix-four-libsyck-memory-safety-CVEs-reachable-from-Y.patch | 545 ++++++++++ patches/fix-prevent-buffer-underflow-in-base60-sexagesimal-p.patch | 145 ++ patches/fix-prevent-memory-leaks-when-Load-LoadJSON-croak-on.patch | 116 ++ patches/rebase-apply-review-feedback-on-69.patch | 59 + patches/series | 4 6 files changed, 882 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpnloc1rqc/libyaml-syck-perl_1.34-2+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpnloc1rqc/libyaml-syck-perl_1.34-2+deb13u3.dsc: no acceptable signature found diff -Nru libyaml-syck-perl-1.34/debian/changelog libyaml-syck-perl-1.34/debian/changelog --- libyaml-syck-perl-1.34/debian/changelog 2026-03-21 18:13:45.000000000 +0000 +++ libyaml-syck-perl-1.34/debian/changelog 2026-08-08 09:20:57.000000000 +0000 @@ -1,3 +1,16 @@ +libyaml-syck-perl (1.34-2+deb13u3) trixie-security; urgency=high + + * Non-maintainer upload by the Security Team. + * fix: prevent buffer underflow in base60 (sexagesimal) parsing + (CVE-2026-5089) + * rebase: apply review feedback + * fix: prevent memory leaks when Load/LoadJSON croak on parse errors + * Fix four libsyck memory-safety CVEs reachable from YAML::Syck::Load() + (CVE-2026-57075, CVE-2026-57076, CVE-2026-57077, CVE-2026-13713) + (Closes: #1142267) + + -- Salvatore Bonaccorso Sat, 08 Aug 2026 11:20:57 +0200 + libyaml-syck-perl (1.34-2+deb13u2) trixie-security; urgency=high * Non-maintainer upload by the Security Team. diff -Nru libyaml-syck-perl-1.34/debian/patches/Fix-four-libsyck-memory-safety-CVEs-reachable-from-Y.patch libyaml-syck-perl-1.34/debian/patches/Fix-four-libsyck-memory-safety-CVEs-reachable-from-Y.patch --- libyaml-syck-perl-1.34/debian/patches/Fix-four-libsyck-memory-safety-CVEs-reachable-from-Y.patch 1970-01-01 00:00:00.000000000 +0000 +++ libyaml-syck-perl-1.34/debian/patches/Fix-four-libsyck-memory-safety-CVEs-reachable-from-Y.patch 2026-08-08 09:20:57.000000000 +0000 @@ -0,0 +1,545 @@ +From: Todd Rinaldo +Date: Mon, 13 Jul 2026 09:20:12 -0500 +Subject: Fix four libsyck memory-safety CVEs reachable from YAML::Syck::Load() +Origin: https://github.com/toddr/YAML-Syck/commit/44c90a109ec3215ee7ce747bd11209835e123d8b +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-57076 +Bug-Debian: https://bugs.debian.org/1142267 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-57075 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-13713 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-57077 + +Reported by Paul Johnson via the CPANSec coordinated-disclosure process. +All four defects live in the bundled libsyck C library, are reachable from +the default YAML::Syck::Load() path on untrusted input with no special flags +(!!binary, anchors and block scalars are all on the default path), and are +present through 1.46. Each has a small, self-contained fix plus a regression +test under t/cve-*.t. + +== The defects and fixes == + +CVE-2026-57075 - CWE-125 Out-of-bounds Read (base64 decoder) + emitter.c: syck_base64dec() indexed the 256-entry static b64_xtable with a + plain signed char, e.g. `b64_xtable[(int)s[0]]`. Where char is signed + (x86-64, arm64) any !!binary byte >= 0x80 sign-extends to a NEGATIVE index + and reads before the table. Non-crashing, but the read value can surface in + the decoded !!binary result. + Fix: cast each of the four index sites to (unsigned char) so a high byte is + always a 0-255 index. b64_xtable[(int)b64_table[i]] at setup is left as-is; + that table is pure ASCII. + +CVE-2026-57076 - CWE-416 Use After Free (anchor key string) + handler.c: an anchor name allocated by syck_strndup was used both as + node->anchor (owned by the node, freed by syck_free_node) AND as the key in + p->anchors. Freeing the node freed the table key, leaving a dangling key; an + anchor redefinition then had st_delete/st_strcmp compare against freed + memory. + Fix: give the anchors and bad_anchors tables sole ownership of their key + strings. Each table key becomes a private syck_strndup() copy, always a + distinct allocation from any node->anchor, and syck_st_free_nodes() frees the + key when its entry leaves the table at teardown. Because no pointer is then + both a table key and a node->anchor, freeing a node can no longer dangle a + key. (syck_.c, handler.c) + +CVE-2026-57077 - CWE-125 Out-of-bounds Read (lexer newline scan) + token.c: newline_len()/is_newline() dereferenced *ptr (and *(ptr+1) for + \r\n) with no NUL terminator or bounds guarantee. During block-scalar lexing + at a document boundary the scan ran one byte past the heap lexer buffer. + This is an incomplete-fix follow-on to CVE-2025-11683, on a lexer path the + earlier fix did not cover. + Fix: give is_newline()/newline_len() an explicit `limit` parameter and check + it before every read, then pass the live bound at each call site (YYLIMIT, or + the scalar-buffer end for the one backward chomp walk, which also gains an + explicit `fc >= ptr` lower-bound guard). Changing the helper signatures makes + the compiler flag every call site, so none is missed - the failure mode that + left CVE-2025-11683 incomplete. + +CVE-2026-13713 - CWE-416 Use After Free / CWE-415 Double Free (anchor node) + handler.c: when an anchor name is redefined or removed, + syck_hdlr_remove_anchor / syck_hdlr_add_anchor freed the SyckNode stored + under that name (syck_free_node). That node can still be live on the parser's + value stack, so syck_hdlr_add_node reaches it again and frees it a second + time. On a normal build the 48-byte node chunk is freed twice and the + interpreter aborts - a remote-crash DoS from a 7-byte input. + Fix: do not free an evicted anchor node inline. Add a parser-owned "retired" + table; syck_retire_node() moves the evicted node there and syck_st_free() + frees the table at teardown. Retired nodes keep node->anchor != NULL, so + syck_hdlr_add_node never re-frees them, and each node is evicted at most once, + so there is no double free at teardown. (handler.c, syck_.c, syck.h) + +NOTE: the two anchor fixes are ordered and interdependent. CVE-2026-13713's +fix relies on CVE-2026-57076's: only once table keys are private copies +distinct from node->anchor is it safe to defer freeing an evicted node to +teardown. Applied together here. + +== Regression tests (t/cve-*.t, one file per CVE) == + +Each test Loads its documented trigger in-process (one CVE per file, so a +crash only takes down that file, which the harness reports as failed) and +wraps Load() in eval{} so the patched behaviour - an ordinary parse-error +croak - passes while an uncatchable C-level abort fails the file. Verified in +both directions on a normal build (no ASan): + + cve-2026-13713 : unpatched -> file aborts (SIGABRT/SIGTRAP); patched -> pass. + Provable on any build (it crashes). + cve-2026-57075 : unpatched -> high-bit !!binary decodes to leaked bytes + ("A" on the author's build) instead of ""; patched -> "". + Provable via output on any build. + cve-2026-57076 : non-crashing UAF - silent on a normal build; proven by the + ASan CI job (heap-use-after-free READ in st_strcmp). + cve-2026-57077 : one-byte over-read - silent on a normal build; proven by the + ASan CI job (heap-buffer-overflow READ in newline_len). + +== Documentation and packaging == + +- CLAUDE.md: new "Security & CVE Work" section documenting the policy - one + t/cve--.t per CVE, made to fail provably without the fix where + possible, in-process (no forked perl; note on why fork() is unsafe on + Windows), with silent defects proven by the ASan job. +- MANIFEST: add the four t/cve-*.t files. + +== Verification == + +- Full suite passes with the fixes: 72 files / 1176 tests (AUTOMATED_TESTING=1, + leak tests active); no RSS growth over 200k iterations of the anchor paths. +- The asan CI job (added separately in .github/workflows/testsuite.yml) + confirmed all three ASan-visible defects on the unpatched tree with stack + traces matching the report, and goes green with these fixes. + +Credit: Paul Johnson , via CPANSec. + +Co-Authored-By: Claude Opus 4.8 (1M context) +--- + .github/workflows/testsuite.yml | 16 +++--------- + CLAUDE.md | 34 ++++++++++++++++++++++++++ + MANIFEST | 4 +++ + emitter.c | 8 +++--- + handler.c | 34 ++++++++++++++++++++++---- + syck.h | 2 ++ + syck_.c | 17 +++++++++++++ + t/cve-2026-13713-anchor-node-uaf.t | 33 +++++++++++++++++++++++++ + t/cve-2026-57075-base64-oob-read.t | 32 ++++++++++++++++++++++++ + t/cve-2026-57076-anchor-key-uaf.t | 29 ++++++++++++++++++++++ + t/cve-2026-57077-newline-oob-read.t | 29 ++++++++++++++++++++++ + token.c | 38 ++++++++++++++++------------- + 12 files changed, 237 insertions(+), 39 deletions(-) + create mode 100644 t/cve-2026-13713-anchor-node-uaf.t + create mode 100644 t/cve-2026-57075-base64-oob-read.t + create mode 100644 t/cve-2026-57076-anchor-key-uaf.t + create mode 100644 t/cve-2026-57077-newline-oob-read.t + +--- a/MANIFEST ++++ b/MANIFEST +@@ -36,6 +36,10 @@ t/bug/doesnt-stringify.t + t/bug/rt-41141.t + t/bug/rt-49404-double_free.t + t/bug/rt-54167.t ++t/cve-2026-13713-anchor-node-uaf.t ++t/cve-2026-57075-base64-oob-read.t ++t/cve-2026-57076-anchor-key-uaf.t ++t/cve-2026-57077-newline-oob-read.t + t/gh-132-base60-safety.t + t/json-basic.t + t/json-circular-ref.t +--- a/emitter.c ++++ b/emitter.c +@@ -82,10 +82,10 @@ syck_base64dec( char *s, long len, long + while (s < send) { + while (s < send && (s[0] == '\r' || s[0] == '\n')) { s++; } + if (s >= send) break; +- if ((a = b64_xtable[(int)s[0]]) == -1) break; +- if ((b = b64_xtable[(int)s[1]]) == -1) break; +- if ((c = b64_xtable[(int)s[2]]) == -1) break; +- if ((d = b64_xtable[(int)s[3]]) == -1) break; ++ if ((a = b64_xtable[(unsigned char)s[0]]) == -1) break; ++ if ((b = b64_xtable[(unsigned char)s[1]]) == -1) break; ++ if ((c = b64_xtable[(unsigned char)s[2]]) == -1) break; ++ if ((d = b64_xtable[(unsigned char)s[3]]) == -1) break; + *end++ = a << 2 | b >> 4; + *end++ = b << 4 | c >> 2; + *end++ = c << 6 | d; +--- a/handler.c ++++ b/handler.c +@@ -27,6 +27,21 @@ syck_hdlr_add_node( SyckParser *p, SyckN + return id; + } + ++/* ++ * A node evicted from the anchors table by redefinition may still be live on ++ * the parser's value stack, so it cannot be freed here. Keep it for teardown. ++ */ ++void ++syck_retire_node( SyckParser *p, SyckNode *n ) ++{ ++ if ( p->retired == NULL ) ++ { ++ p->retired = st_init_numtable(); ++ } ++ st_insert( p->retired, (st_data_t)( p->retired->num_entries + 1 ), ++ (st_data_t)n ); ++} ++ + SyckNode * + syck_hdlr_add_anchor( SyckParser *p, char *a, SyckNode *n ) + { +@@ -64,10 +79,15 @@ syck_hdlr_add_anchor( SyckParser *p, cha + { + if ( ntmp != (void *)1 ) + { +- syck_free_node( ntmp ); ++ syck_retire_node( p, ntmp ); + } ++ st_insert( p->anchors, (st_data_t)a, (st_data_t)n ); ++ } ++ else ++ { ++ st_insert( p->anchors, (st_data_t)syck_strndup( a, strlen( a ) ), ++ (st_data_t)n ); + } +- st_insert( p->anchors, (st_data_t)a, (st_data_t)n ); + return n; + } + +@@ -84,10 +104,12 @@ syck_hdlr_remove_anchor( SyckParser *p, + { + if ( ntmp != (void *)1 ) + { +- syck_free_node( ntmp ); ++ syck_retire_node( p, ntmp ); + } ++ S_FREE( atmp ); + } +- st_insert( p->anchors, (st_data_t)a, (st_data_t)1 ); ++ st_insert( p->anchors, (st_data_t)syck_strndup( a, strlen( a ) ), ++ (st_data_t)1 ); + } + + SyckNode * +@@ -113,7 +135,9 @@ syck_hdlr_get_anchor( SyckParser *p, cha + if ( ! st_lookup( p->bad_anchors, (st_data_t)a, (st_data_t *)&n ) ) + { + n = (p->bad_anchor_handler)( p, a ); +- st_insert( p->bad_anchors, (st_data_t)a, (st_data_t)n ); ++ st_insert( p->bad_anchors, ++ (st_data_t)syck_strndup( a, strlen( a ) ), ++ (st_data_t)n ); + } + } + } +--- a/syck.h ++++ b/syck.h +@@ -272,6 +272,8 @@ struct _syck_parser { + } io; + /* Symbol table for anchors */ + st_table *anchors, *bad_anchors; ++ /* Nodes evicted from anchors by redefinition, freed at teardown */ ++ st_table *retired; + /* Optional symbol table for SYMIDs */ + st_table *syms; + /* Levels of indentation */ +--- a/syck_.c ++++ b/syck_.c +@@ -166,6 +166,7 @@ syck_new_parser() + p->syms = NULL; + p->anchors = NULL; + p->bad_anchors = NULL; ++ p->retired = NULL; + p->implicit_typing = 1; + p->taguri_expansion = 0; + p->bufsize = SYCK_BUFFERSIZE; +@@ -199,7 +200,16 @@ enum st_retval + syck_st_free_nodes( st_data_t key, st_data_t value, st_data_t arg ) + { + SyckNode *n = (SyckNode *)value; ++ char *k = (char *)key; + if ( n != (void *)1 ) syck_free_node( n ); ++ S_FREE( k ); /* the anchor tables own their key strings */ ++ return ST_CONTINUE; ++} ++ ++enum st_retval ++syck_st_free_retired( st_data_t key, st_data_t value, st_data_t arg ) ++{ ++ syck_free_node( (SyckNode *)value ); + return ST_CONTINUE; + } + +@@ -222,6 +232,13 @@ syck_st_free( SyckParser *p ) + st_free_table( p->bad_anchors ); + p->bad_anchors = NULL; + } ++ ++ if ( p->retired != NULL ) ++ { ++ st_foreach( p->retired, syck_st_free_retired, 0 ); ++ st_free_table( p->retired ); ++ p->retired = NULL; ++ } + } + + void +--- /dev/null ++++ b/t/cve-2026-13713-anchor-node-uaf.t +@@ -0,0 +1,33 @@ ++#!perl ++ ++# CVE-2026-13713 - CWE-416 (Use After Free) / CWE-415 (Double Free) ++# ++# In the bundled libsyck, when an anchor name is redefined or removed, ++# syck_hdlr_remove_anchor / syck_hdlr_add_anchor freed the SyckNode that was ++# stored under that name (syck_free_node). That node could still be live on ++# the parser's value stack, so the grammar reached it again in ++# syck_hdlr_add_node (handler.c:17) and freed the same node a second time. ++# ++# Trigger (7 bytes): YAML::Syck::Load("[&a\x01\n&a") ++# ++# This is the one defect of the four that faults WITHOUT AddressSanitizer: ++# on a normal build the 48-byte node chunk is freed twice and the interpreter ++# aborts (heap corruption -> SIGABRT/SIGTRAP). So against an unpatched libsyck ++# this whole test file crashes and the harness reports it as failed ++# ("Dubious, test returned ... wstat ..."). Once patched, Load() raises an ++# ordinary parse error (caught below) and the file passes cleanly. ++ ++use strict; ++use warnings; ++use Test::More tests => 1; ++use YAML::Syck (); ++ ++# The unpatched behaviour is a C-level abort() that eval cannot catch: it takes ++# the whole file down before the assertion below and the harness reports the ++# file as failed. Patched, Load() croaks on the malformed input, so we assert ++# it died with the expected parse error (verifying the graceful failure, not ++# merely that the process survived). ++eval { YAML::Syck::Load("[&a\x01\n&a") }; ++ ++like( $@, qr/syntax error/, ++ 'CVE-2026-13713: Load("[&a\\x01\\n&a") croaked with a parse error, not a double-free crash' ); +--- /dev/null ++++ b/t/cve-2026-57075-base64-oob-read.t +@@ -0,0 +1,32 @@ ++#!perl ++ ++# CVE-2026-57075 - CWE-125 (Out-of-bounds Read) ++# ++# syck_base64dec (emitter.c) indexed the 256-entry static b64_xtable with a ++# plain signed char from the input: ++# if ((a = b64_xtable[(int)s[0]]) == -1) break; ++# Any !!binary byte >= 0x80 sign-extends to a NEGATIVE index and reads before ++# the table. The fix casts each index to (unsigned char). ++# ++# Trigger: YAML::Syck::Load("--- !!binary \x80\x80\x80\x80") ++# ++# The read is non-crashing and its negative index lands in live static memory ++# adjacent to b64_xtable rather than an AddressSanitizer redzone, so it neither ++# crashes nor trips ASan through Load() (see the CPANSec report). It is still ++# observable at runtime: with the fix every byte >= 0x80 maps to -1, so the ++# decoder rejects the payload and yields an EMPTY string deterministically; ++# without the fix the negative index reads adjacent memory and that garbage can ++# surface in the decoded result (on the author's build it decoded to "A" rather ++# than ""). So this assertion fails on an unpatched build. The exact unpatched ++# value is layout-dependent, but the patched value is always "". ++ ++use strict; ++use warnings; ++use Test::More tests => 1; ++use YAML::Syck (); ++ ++my $decoded = eval { YAML::Syck::Load("--- !!binary \x80\x80\x80\x80") }; ++ ++is( $decoded, '', ++'CVE-2026-57075: high-bit-only !!binary decodes to empty string, no OOB leak' ++); +--- /dev/null ++++ b/t/cve-2026-57076-anchor-key-uaf.t +@@ -0,0 +1,29 @@ ++#!perl ++ ++# CVE-2026-57076 - CWE-416 (Use After Free) ++# ++# An anchor name string allocated by syck_strndup was freed in ++# syck_hdlr_add_anchor (handler.c:43) while still stored as a live KEY in the ++# parser's anchors table. On an anchor redefinition, syck_hdlr_remove_anchor ++# calls st_delete, whose st_strcmp then compares against that freed key. ++# ++# Trigger (9 bytes): YAML::Syck::Load("- &a&V\n&a") ++# ++# This is a NON-crashing use-after-free: on a normal build the freed key is ++# still readable and nothing faults, so this file cannot fail a plain build - ++# it just confirms Load() completes. The defect is PROVEN by the ASan CI job ++# (the `asan` job in .github/workflows/testsuite.yml), where this same Load ++# aborts the process with: ++# heap-use-after-free READ in st_strcmp, freed by syck_hdlr_add_anchor ++# and the harness reports this file as failed. ++ ++use strict; ++use warnings; ++use Test::More tests => 1; ++use YAML::Syck (); ++ ++# Under ASan (unpatched) this Load aborts the process; on a normal build it ++# returns/croaks harmlessly. eval keeps a normal parse error from failing us. ++eval { YAML::Syck::Load("- &a&V\n&a") }; ++ ++pass('CVE-2026-57076: Load("- &a&V\\n&a") completed without an ASan fault'); +--- /dev/null ++++ b/t/cve-2026-57077-newline-oob-read.t +@@ -0,0 +1,29 @@ ++#!perl ++ ++# CVE-2026-57077 - CWE-125 (Out-of-bounds Read) ++# ++# newline_len()/is_newline() (token.c) dereferenced *ptr (and *(ptr+1) for ++# \r\n) with no NUL terminator or bounds guarantee. During block-scalar ++# lexing at a document boundary the scan pointer ran one byte past the heap ++# lexer buffer. This is an incomplete-fix follow-on to CVE-2025-11683, on a ++# lexer path that earlier fix did not cover. ++# ++# Trigger (6 bytes): YAML::Syck::Load("|\n---\n") ++# ++# A one-byte over-read that does not crash a normal build, so this file cannot ++# fail a plain build - it just confirms Load() completes. The defect is PROVEN ++# by the ASan CI job (the `asan` job in .github/workflows/testsuite.yml), where ++# this same Load aborts the process with: ++# heap-buffer-overflow READ in newline_len ++# and the harness reports this file as failed. ++ ++use strict; ++use warnings; ++use Test::More tests => 1; ++use YAML::Syck (); ++ ++# Under ASan (unpatched) this Load aborts the process; on a normal build it ++# returns harmlessly. eval keeps a normal parse error from failing us. ++eval { YAML::Syck::Load("|\n---\n") }; ++ ++pass('CVE-2026-57077: Load("|\\n---\\n") completed without an ASan fault'); +--- a/token.c ++++ b/token.c +@@ -39,7 +39,7 @@ + /* + * Track line numbers + */ +-#define NEWLINE(ptr) YYLINEPTR = ptr + newline_len(ptr); if ( YYLINEPTR > YYLINECTPTR ) { YYLINE++; YYLINECTPTR = YYLINEPTR; } ++#define NEWLINE(ptr) YYLINEPTR = ptr + newline_len(ptr, YYLIMIT); if ( YYLINEPTR > YYLINECTPTR ) { YYLINE++; YYLINECTPTR = YYLINEPTR; } + + /* + * I like seeing the level operations as macros... +@@ -132,7 +132,7 @@ + + /* concat the inline characters to the plain scalar */ + #define PLAIN_NOT_INL() \ +- if ( *(YYCURSOR - 1) == ' ' || is_newline( YYCURSOR - 1 ) ) \ ++ if ( *(YYCURSOR - 1) == ' ' || is_newline( YYCURSOR - 1, YYLIMIT ) ) \ + { \ + YYCURSOR--; \ + } \ +@@ -176,7 +176,8 @@ + if ( nlDoWhat != NL_KEEP ) \ + { \ + char *fc = n->data.str->ptr + n->data.str->len - 1; \ +- while ( is_newline( fc ) ) fc--; \ ++ while ( fc >= n->data.str->ptr && \ ++ is_newline( fc, n->data.str->ptr + n->data.str->len ) ) fc--; \ + if ( nlDoWhat != NL_CHOMP && fc < n->data.str->ptr + n->data.str->len - 1 ) \ + fc += 1; \ + n->data.str->len = fc - n->data.str->ptr + 1; \ +@@ -194,7 +195,7 @@ + NEWLINE(indent); \ + while ( indent < YYCURSOR ) \ + { \ +- if ( is_newline( ++indent ) ) \ ++ if ( is_newline( ++indent, YYLIMIT ) ) \ + { \ + NEWLINE(indent); \ + } \ +@@ -237,8 +238,8 @@ SyckParser *syck_parser_ptr = NULL; + */ + void eat_comments( SyckParser * ); + char escape_seq( char ); +-int is_newline( char *ptr ); +-int newline_len( char *ptr ); ++int is_newline( char *ptr, char *limit ); ++int newline_len( char *ptr, char *limit ); + int sycklex_yaml_utf8( YYSTYPE *, SyckParser * ); + int sycklex_bytecode_utf8( YYSTYPE *, SyckParser * ); + int syckwrap(); +@@ -892,7 +893,7 @@ yy70: + ++YYCURSOR; + yy71: + #line 482 "token.re" +- { if ( is_newline( YYCURSOR - 1 ) ) ++ { if ( is_newline( YYCURSOR - 1, YYLIMIT ) ) + { + YYCURSOR--; + } +@@ -1081,7 +1082,7 @@ yy82: + #line 444 "token.re" + { ENSURE_YAML_IOPEN(lvl, YYTOKEN - YYLINEPTR, 1); + FORCE_NEXT_TOKEN(YAML_IOPEN); +- if ( *YYCURSOR == '#' || is_newline( YYCURSOR ) || is_newline( YYCURSOR - 1 ) ) ++ if ( *YYCURSOR == '#' || is_newline( YYCURSOR, YYLIMIT ) || is_newline( YYCURSOR - 1, YYLIMIT ) ) + { + YYCURSOR--; + ADD_LEVEL((YYTOKEN + 1) - YYLINEPTR, syck_lvl_seq); +@@ -1603,7 +1604,7 @@ yy113: + + while ( YYTOKEN < YYCURSOR ) + { +- int nl_len = newline_len( YYTOKEN++ ); ++ int nl_len = newline_len( YYTOKEN++, YYLIMIT ); + if ( nl_len ) + { + nl_count++; +@@ -1847,7 +1848,7 @@ yy147: + + while ( YYTOKEN < YYCURSOR ) + { +- int nl_len = newline_len( YYTOKEN++ ); ++ int nl_len = newline_len( YYTOKEN++, YYLIMIT ); + if ( nl_len ) + { + nl_count++; +@@ -2010,7 +2011,7 @@ yy163: + { + while ( YYTOKEN < YYCURSOR ) + { +- int nl_len = newline_len( YYTOKEN++ ); ++ int nl_len = newline_len( YYTOKEN++, YYLIMIT ); + if ( nl_len ) + { + nl_count++; +@@ -2540,7 +2541,7 @@ yy208: + pacer = YYTOKEN; + while ( pacer < YYCURSOR ) + { +- int nl_len = newline_len( pacer++ ); ++ int nl_len = newline_len( pacer++, YYLIMIT ); + if ( nl_len ) + { + nl_count++; +@@ -2807,18 +2808,21 @@ escape_seq( char ch ) + } + + int +-is_newline( char *ptr ) ++is_newline( char *ptr, char *limit ) + { +- return newline_len( ptr ); ++ return newline_len( ptr, limit ); + } + + int +-newline_len( char *ptr ) ++newline_len( char *ptr, char *limit ) + { ++ if ( ptr >= limit ) ++ return 0; ++ + if ( *ptr == '\n' ) + return 1; +- +- if ( *ptr == '\r' && *( ptr + 1 ) == '\n' ) ++ ++ if ( *ptr == '\r' && ptr + 1 < limit && *( ptr + 1 ) == '\n' ) + return 2; + + return 0; diff -Nru libyaml-syck-perl-1.34/debian/patches/fix-prevent-buffer-underflow-in-base60-sexagesimal-p.patch libyaml-syck-perl-1.34/debian/patches/fix-prevent-buffer-underflow-in-base60-sexagesimal-p.patch --- libyaml-syck-perl-1.34/debian/patches/fix-prevent-buffer-underflow-in-base60-sexagesimal-p.patch 1970-01-01 00:00:00.000000000 +0000 +++ libyaml-syck-perl-1.34/debian/patches/fix-prevent-buffer-underflow-in-base60-sexagesimal-p.patch 2026-08-08 09:20:57.000000000 +0000 @@ -0,0 +1,145 @@ +From: Toddr Bot +Date: Fri, 20 Mar 2026 03:08:35 +0000 +Subject: fix: prevent buffer underflow in base60 (sexagesimal) parsing +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Origin: https://github.com/toddr/YAML-Syck/commit/208a4d3bd1b5cdb4a791a6e3905bd6bd45e9d005 +Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-5089 + +The inner while loop in int#base60 and float#base60 parsing could +decrement a pointer past the start of the string buffer when processing +the leftmost segment (no colon found). The subsequent dereference +(*colon) then read one byte before the allocated buffer — undefined +behavior that could corrupt data or crash under ASan/Valgrind. + +Fix: change `colon >= ptr` to `colon > ptr` so the pointer stops at the +buffer start, and restructure the post-loop logic to handle the +no-colon (final segment) case explicitly without dereferencing +out-of-bounds memory. + +Co-Authored-By: Claude Opus 4.6 +--- + MANIFEST | 1 + + perl_syck.h | 28 ++++++++++++++++++---------- + t/gh-132-base60-safety.t | 40 ++++++++++++++++++++++++++++++++++++++++ + 3 files changed, 59 insertions(+), 10 deletions(-) + create mode 100644 t/gh-132-base60-safety.t + +diff --git a/MANIFEST b/MANIFEST +index d09cd4607369..1006d8f5b239 100644 +--- a/MANIFEST ++++ b/MANIFEST +@@ -36,6 +36,7 @@ t/bug/doesnt-stringify.t + t/bug/rt-41141.t + t/bug/rt-49404-double_free.t + t/bug/rt-54167.t ++t/gh-132-base60-safety.t + t/json-basic.t + t/json-circular-ref.t + t/json-crlf.t +diff --git a/perl_syck.h b/perl_syck.h +index f4bdb381be61..0b8807987d45 100644 +--- a/perl_syck.h ++++ b/perl_syck.h +@@ -195,16 +195,20 @@ yaml_syck_parser_handler + { + NV bnum = 0; + char *colon = end - 1; +- while ( colon >= ptr && *colon != ':' ) ++ while ( colon > ptr && *colon != ':' ) + { + colon--; + } +- if ( *colon == ':' ) *colon = '\0'; +- +- bnum = strtod( colon + 1, NULL ); ++ if ( *colon == ':' ) { ++ *colon = '\0'; ++ bnum = strtod( colon + 1, NULL ); ++ end = colon; ++ } else { ++ bnum = strtod( ptr, NULL ); ++ end = ptr; ++ } + total += bnum * sixty; + sixty *= 60; +- end = colon; + } + sv = newSVnv(total); + #ifdef NV_NAN +@@ -233,16 +237,20 @@ yaml_syck_parser_handler + { + long bnum = 0; + char *colon = end - 1; +- while ( colon >= ptr && *colon != ':' ) ++ while ( colon > ptr && *colon != ':' ) + { + colon--; + } +- if ( *colon == ':' ) *colon = '\0'; +- +- bnum = strtol( colon + 1, NULL, 10 ); ++ if ( *colon == ':' ) { ++ *colon = '\0'; ++ bnum = strtol( colon + 1, NULL, 10 ); ++ end = colon; ++ } else { ++ bnum = strtol( ptr, NULL, 10 ); ++ end = ptr; ++ } + total += bnum * sixty; + sixty *= 60; +- end = colon; + } + sv = newSVuv(total); + } else if (strEQ( id, "int#hex" )) { +diff --git a/t/gh-132-base60-safety.t b/t/gh-132-base60-safety.t +new file mode 100644 +index 000000000000..8e3d29c3ad66 +--- /dev/null ++++ b/t/gh-132-base60-safety.t +@@ -0,0 +1,40 @@ ++use strict; ++use warnings; ++ ++use FindBin; ++BEGIN { push @INC, $FindBin::Bin } ++ ++use TestYAML (); ++use Test::More tests => 10; ++use YAML::Syck qw(Load); ++ ++# GH #132 - base60 (sexagesimal) parsing safety ++# The parser walked a pointer before the start of the buffer when ++# processing the leftmost segment of a base-60 value. Verify that ++# int#base60 and float#base60 produce correct results and do not ++# crash or read out-of-bounds. ++ ++$YAML::Syck::ImplicitTyping = 1; ++ ++# --- int#base60 --- ++is( Load("--- 1:0:0\n"), 3600, "int base60: 1:0:0 = 3600" ); ++is( Load("--- 1:30:45\n"), 5445, "int base60: 1:30:45 = 5445" ); ++is( Load("--- 0:30\n"), 30, "int base60: 0:30 = 30" ); ++is( Load("--- 0:0\n"), 0, "int base60: 0:0 = 0" ); ++ ++# --- float#base60 --- ++is( Load("--- 1:30:45.5\n"), 5445.5, "float base60: 1:30:45.5 = 5445.5" ); ++is( Load("--- 0:0.5\n"), 0.5, "float base60: 0:0.5 = 0.5" ); ++ ++# --- edge cases: single-colon values --- ++is( Load("--- 59:59\n"), 3599, "int base60: 59:59 = 3599" ); ++ ++# --- multi-segment --- ++is( Load("--- 1:2:3:4\n"), 223384, "int base60: 1:2:3:4 = 1*216000+2*3600+3*60+4" ); ++ ++# --- negative base60 (parsed as string, not base60 - verify no crash) --- ++my $neg = Load("--- -1:30\n"); ++ok( defined $neg, "negative sexagesimal loads without crash" ); ++ ++# --- large segment count (stress the loop) --- ++is( Load("--- 1:0:0:0\n"), 216000, "int base60: 1:0:0:0 = 216000" ); +-- +2.53.0 + diff -Nru libyaml-syck-perl-1.34/debian/patches/fix-prevent-memory-leaks-when-Load-LoadJSON-croak-on.patch libyaml-syck-perl-1.34/debian/patches/fix-prevent-memory-leaks-when-Load-LoadJSON-croak-on.patch --- libyaml-syck-perl-1.34/debian/patches/fix-prevent-memory-leaks-when-Load-LoadJSON-croak-on.patch 1970-01-01 00:00:00.000000000 +0000 +++ libyaml-syck-perl-1.34/debian/patches/fix-prevent-memory-leaks-when-Load-LoadJSON-croak-on.patch 2026-08-08 09:20:57.000000000 +0000 @@ -0,0 +1,116 @@ +From: Toddr Bot +Date: Tue, 14 Apr 2026 05:47:45 +0000 +Subject: fix: prevent memory leaks when Load/LoadJSON croak on parse errors +Origin: https://github.com/toddr/YAML-Syck/commit/bcc6cb7b7d8ffc4b06868525e839f4824e38bcf1 + +The parser error handler calls croak() which longjmps past the normal +cleanup path, leaking the SyckParser and (in JSON mode) the preprocessed +string buffer. In list context, the temporary AV also leaked. + +Fix: +- Add SAVEDESTRUCTOR_X for the SyckParser, mirroring the emitter pattern +- Use SAVEFREEPV for the JSON preprocessed string (replaces manual Safefree) +- Mortalize the list-context AV so scope unwinding frees it on croak + +Verified with 10,000 consecutive parse error iterations without crash. + +Co-Authored-By: Claude Opus 4.6 +--- + perl_syck.h | 37 +++++++++++++++++++++++++++++++------ + 1 file changed, 31 insertions(+), 6 deletions(-) + +diff --git a/perl_syck.h b/perl_syck.h +index ad19f9b3d421..ecc656317baf 100644 +--- a/perl_syck.h ++++ b/perl_syck.h +@@ -55,7 +55,7 @@ static enum scalar_style yaml_quote_style = scalar_none; + # define SEQ_NONE seq_none + # define MAP_NONE map_none + #ifdef SvUTF8 +-# define IS_UTF8(x) (SvUTF8(sv)) ++# define IS_UTF8(x) (SvUTF8(x)) + #else + # define IS_UTF8(x) (FALSE) + #endif +@@ -869,6 +869,22 @@ void perl_json_postprocess(SV *sv) { + } + #endif + ++/* Destructor for SAVEDESTRUCTOR_X: frees parser on croak. ++ * Registered after syck_new_parser() so Perl's scope unwinding handles ++ * cleanup even when croak() longjmps past the normal return path. ++ * Guarded because perl_syck.h is included twice (YAML and JSON modes). */ ++#ifndef CLEANUP_PARSER_DEFINED ++#define CLEANUP_PARSER_DEFINED ++static void ++cleanup_parser(pTHX_ void *p) { ++ SyckParser **pp = (SyckParser **)p; ++ if (*pp != NULL) { ++ syck_free_parser(*pp); ++ *pp = NULL; ++ } ++} ++#endif ++ + #ifdef YAML_IS_JSON + static SV * LoadJSON (char *s) { + #else +@@ -894,6 +910,7 @@ static SV * LoadYAML (char *s) { + + #ifdef YAML_IS_JSON + s = perl_json_preprocess(s); ++ SAVEFREEPV(s); /* freed at LEAVE — also on croak */ + #else + /* Special preprocessing to maintain compat with YAML.pm <= 0.35 */ + if (strnEQ( s, "--- #YAML:1.0", 13)) { +@@ -902,6 +919,11 @@ static SV * LoadYAML (char *s) { + #endif + + parser = syck_new_parser(); ++ ++ /* Register destructor so croak() in parser callbacks (error_handler, ++ * parser_handler code-loading) won't leak the SyckParser. */ ++ SAVEDESTRUCTOR_X(cleanup_parser, &parser); ++ + syck_parser_str_auto(parser, s, NULL); + syck_parser_handler(parser, PERL_SYCK_PARSER_HANDLER); + syck_parser_error_handler(parser, perl_syck_error_handler); +@@ -921,7 +943,9 @@ static SV * LoadYAML (char *s) { + if (GIMME_V == G_ARRAY) { + SYMID prev_v = 0; + +- obj = (SV*)newAV(); ++ /* Mortalize the AV so croak() during syck_parse() won't leak it. ++ * Use newRV_inc to compensate — the mortal entry decrements at LEAVE. */ ++ obj = (SV*)sv_2mortal((SV*)newAV()); + while ((v = syck_parse(parser)) && (v != prev_v)) { + SV *cur = &PL_sv_undef; + if (!syck_lookup_sym(parser, v, (char **)&cur)) { +@@ -933,7 +957,7 @@ static SV * LoadYAML (char *s) { + + prev_v = v; + } +- obj = newRV_noinc(obj); ++ obj = newRV_inc(obj); + } + else + #endif +@@ -944,11 +968,12 @@ static SV * LoadYAML (char *s) { + } + } + ++ /* Normal path: free parser now and NULL the pointer so the ++ * SAVEDESTRUCTOR_X callback (at LEAVE) becomes a no-op. */ + syck_free_parser(parser); ++ parser = NULL; + +-#ifdef YAML_IS_JSON +- Safefree(s); +-#endif ++ /* In JSON mode, SAVEFREEPV(s) frees the preprocessed string at LEAVE. */ + + FREETMPS; LEAVE; + +-- +2.53.0 + diff -Nru libyaml-syck-perl-1.34/debian/patches/rebase-apply-review-feedback-on-69.patch libyaml-syck-perl-1.34/debian/patches/rebase-apply-review-feedback-on-69.patch --- libyaml-syck-perl-1.34/debian/patches/rebase-apply-review-feedback-on-69.patch 1970-01-01 00:00:00.000000000 +0000 +++ libyaml-syck-perl-1.34/debian/patches/rebase-apply-review-feedback-on-69.patch 2026-08-08 09:20:57.000000000 +0000 @@ -0,0 +1,59 @@ +From: Toddr Bot +Date: Sat, 14 Mar 2026 18:38:42 +0000 +Subject: rebase: apply review feedback on #69 +Origin: https://github.com/toddr/YAML-Syck/commit/936b68c43544597663f8cb584aeffa7e08b85eed + +--- + emitter.c | 2 +- + syck_.c | 6 +++--- + token.c | 2 +- + 3 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/emitter.c b/emitter.c +index 2365c9f4d237..726150ac863c 100644 +--- a/emitter.c ++++ b/emitter.c +@@ -109,7 +109,7 @@ syck_base64dec( char *s, long len, long *out_len ) + * Allocate an emitter + */ + SyckEmitter * +-syck_new_emitter() ++syck_new_emitter(void) + { + SyckEmitter *e; + e = S_ALLOC( SyckEmitter ); +diff --git a/syck_.c b/syck_.c +index 4670b5495733..ce0a8cb564b5 100644 +--- a/syck_.c ++++ b/syck_.c +@@ -195,11 +195,11 @@ syck_lookup_sym( SyckParser *p, SYMID id, char **data ) + return st_lookup( p->syms, id, (st_data_t *)data ); + } + +-int +-syck_st_free_nodes( char *key, SyckNode *n, char *arg ) ++enum st_retval ++syck_st_free_nodes( st_data_t key, st_data_t value, st_data_t arg ) + { ++ SyckNode *n = (SyckNode *)value; + if ( n != (void *)1 ) syck_free_node( n ); +- n = NULL; + return ST_CONTINUE; + } + +diff --git a/token.c b/token.c +index 53dc0805284a..401f9ae9b8f4 100644 +--- a/token.c ++++ b/token.c +@@ -2825,7 +2825,7 @@ newline_len( char *ptr ) + } + + int +-syckwrap() ++syckwrap(void) + { + return 1; + } +-- +2.53.0 + diff -Nru libyaml-syck-perl-1.34/debian/patches/series libyaml-syck-perl-1.34/debian/patches/series --- libyaml-syck-perl-1.34/debian/patches/series 2026-03-21 18:13:45.000000000 +0000 +++ libyaml-syck-perl-1.34/debian/patches/series 2026-08-08 09:20:57.000000000 +0000 @@ -1,3 +1,7 @@ disable-compiler-check.patch Address-memory-corruption-leading-to-str-value-being.patch fix-address-all-4-C-layer-audit-findings-from-issue-.patch +fix-prevent-buffer-underflow-in-base60-sexagesimal-p.patch +rebase-apply-review-feedback-on-69.patch +fix-prevent-memory-leaks-when-Load-LoadJSON-croak-on.patch +Fix-four-libsyck-memory-safety-CVEs-reachable-from-Y.patch