Skip to content
Merged
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
3 changes: 3 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Revision history for Perl extension JavaScript::Minifier::XS.
- Fixes CVE-2026-56017, which caused Perl to SEGFAULT when calling
minify(). Thanks to CPANSec for raising the issue, and providing a
prototype fix.
- Fixes CVE-2026-56018, caused by a memory leak in minify() where each
tokenized Node's "contents" buffer were not properly freed, resulting in a
memory leak on every call.
- Updated author tests for "does the JS still compile?", to use "node"
instead of "jsl".

Expand Down
12 changes: 9 additions & 3 deletions XS.xs
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ Node* JsPruneNodes(Node *head) {
* ****************************************************************************
*/
char* JsMinify(const char* string) {
char* results;
char* results = NULL;
JsDoc doc;

/* initialize our JS document object */
Expand All @@ -716,12 +716,12 @@ char* JsMinify(const char* string) {

/* PASS 1: tokenize JS into a list of nodes */
Node* head = JsTokenizeString(&doc, string);
if (!head) return NULL;
if (!head) goto cleanup;
/* PASS 2: collapse nodes */
JsCollapseNodes(head);
/* PASS 3: prune nodes */
head = JsPruneNodes(head);
if (!head) return NULL;
if (!head) goto cleanup;
/* PASS 4: re-assemble JS into single string */
{
Node* curr;
Expand All @@ -741,10 +741,16 @@ char* JsMinify(const char* string) {
*ptr = 0;
}
/* free memory used by the NodeSets */
cleanup:
{
NodeSet* curr = doc.head_set;
while (curr) {
NodeSet* next = curr->next;
/* free each node's contents buffer before freeing the set */
size_t idx;
for (idx=0; idx < curr->next_node; idx++)
JsClearNodeContents( &curr->nodes[idx] );
/* free the set, now that it's empty */
Safefree(curr);
curr = next;
}
Expand Down
1 change: 1 addition & 0 deletions cpanfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ author_requires 'File::Slurp';
author_requires 'File::Which';
author_requires 'IPC::Run';
author_requires 'JavaScript::Minifier';
author_requires 'Linux::Smaps';
author_requires 'Number::Format';
author_requires 'Test::LeakTrace';
66 changes: 66 additions & 0 deletions xt/author/leaks-xs.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/perl

use strict;
use warnings;
use Test::More;
use JavaScript::Minifier::XS qw(minify);

BEGIN {
eval "use Linux::Smaps";
plan skip_all => "Linux::Smaps required for XS leak testing" if $@;
}
use Linux::Smaps;

###############################################################################
my $ITERS_WARMUP = 2_000;
my $ITERS_TESTING = 50_000;

###############################################################################
# A small snippet exercising several token types: identifiers, whitespace,
# sigils, a literal, and a comment. Each becomes a node whose content could
# leak.
my $js = <<'END_JS';
var foo = 1; // a comment
function bar() {
return foo + "baz";
}
END_JS

###############################################################################
# Sanity check: minify actually does something.
ok minify($js), 'minify() returned minified JS';

###############################################################################
# Warm things up. Runs a handful of iterations so that our memory allocator
# can reach a steady state.
minify($js) for (1 .. $ITERS_WARMUP);

###############################################################################
# Measure RSS growth over repeated calls to the minifier. If the XS code is
# leaking any memory, our RSS should grow.
my $smaps = Linux::Smaps->new;

my $rss_before = $smaps->update->rss;
minify($js) for (1 .. $ITERS_TESTING);
my $rss_after = $smaps->update->rss;

my $rss_growth = $rss_after - $rss_before;
note sprintf(
"RSS before: %d KB, after: %d KB, growth: %d KB over %d calls (%.3f KB/call)",
$rss_before,
$rss_after,
$rss_growth,
$ITERS_TESTING,
$rss_growth / $ITERS_TESTING,
);

###############################################################################
# Allow for some memory allocator noise and fragmentation.
#
# If total growth exceeds this threshold, odds are high that we're leaking.
my $THRESHOLD_KB = 4_000;
cmp_ok $rss_growth, '<', $THRESHOLD_KB,
"minify() does not leak memory (RSS growth $rss_growth KB < $THRESHOLD_KB KB)";

###############################################################################
done_testing();
Loading