dbb0850c7935dec65d3394ef1ddcdc10dafac5cf
braney
  Tue Aug 18 10:03:38 2026 -0700
cheapcgi: parse %hh escapes directly instead of with sscanf, refs #37262

cgiDecode and cgiDecodeFull read each %hh escape with sscanf(in, "%2x", &code).
glibc builds a stream over the whole remaining string on every sscanf call, so
each call scans to the terminating null.  That makes the cost of a decode
quadratic in the length of one variable's value.

The cost is real on data we already have.  A saved session in hgcentraltest
holds a single 699 KB hgFind.matches value with 58,930 escapes; decoding it
takes 0.29 s of CPU, and cart.c loadHash does it on every load of that session.
The database cart has no size cap, so this is not bounded by the 1 MB limit on
request input that went in for #37452.  At that 1 MB limit a single request
still costs over 2 s.

Reading the two hex digits directly is a few hundred times faster (430x on the
699 KB value) and never walks past them.  Behavior is unchanged for well-formed
input: verified byte-identical over the top 200 carts of namedSessionDb,
sessionDb and userDb (2.4 million values, 14.7 MB), over an exhaustive sweep of
every "%" plus two arbitrary bytes, and over cgiEncode/cgiDecode round trips of
all 256 byte values.

Decoding now differs only where a "%" is not followed by two hex digits, which
nothing legitimate produces - none of the 659,624 escapes in those carts are
malformed.  The old code was worse there anyway: sscanf skips leading
whitespace, so "% 0Z" decoded to a null byte in the middle of the value and
silently truncated it.  Malformed escapes now yield '?' like other bad input.

Also removes the FAST_CGI_DECODE ifdef added earlier on this ticket.  It never
touched cgiDecode, so it does not describe anything now that the real cost is
fixed.  Its per-variable caps are superseded by the total input cap from
#37452, which aborts with a message rather than dropping a variable silently,
and cgiParseNext's variant silently skipped oversized variables for the
ENCODE/CIRM tag tools that are its only callers.

diff --git src/inc/cheapcgi.h src/inc/cheapcgi.h
index d0f3124b87f..8c24bed5668 100644
--- src/inc/cheapcgi.h
+++ src/inc/cheapcgi.h
@@ -1,39 +1,30 @@
 /* cheapcgi.h - turns variables passed from the web form into
  * something that C understands. 
  * 
  * This file is copyright 2000 Jim Kent, but license is hereby
  * granted for all use - public, private or commercial. */
 
 #ifndef CHEAPCGI_H
 #define CHEAPCGI_H
 
 #include "dystring.h"
 
 #ifndef HASH_H
 #include "hash.h"
 #endif
 
-// #define FAST_CGI_DECODE
-#ifdef FAST_CGI_DECODE
-// 50kB per-variable limit on content length to prevent egregious
-// cart-stuffing, whether intentional or accidental.  5kB limit
-// on variable names for similar reasons.
-#define CGI_VAR_SIZE_LIMIT 50000
-#define CGI_VAR_NAME_LIMIT 5000
-#endif
-
 //============ javascript inline-separation routines ===============
 
 void jsInlineFinish();
 /* finish outputting accumulated inline javascript */
 
 void jsInline(char *javascript);
 /* Add text to output file or memory structure */
 
 void jsInlineF(char *format, ...)
 /* Add javascript text to output file or memory structure */
 #if defined(__GNUC__)
 __attribute__((format(printf, 1, 2)))
 #endif
 ;