Fix two issues in TOAST decompression.
authorTom Lane <[email protected]>
Sun, 1 Nov 2020 23:38:42 +0000 (18:38 -0500)
committerTom Lane <[email protected]>
Sun, 1 Nov 2020 23:38:42 +0000 (18:38 -0500)
pglz_maximum_compressed_size() potentially underestimated the amount
of compressed data required to produce N bytes of decompressed data;
this is a fault in commit 11a078cf8.

Separately from that, pglz_decompress() failed to protect itself
against corrupt compressed data, particularly off == 0 in a match
tag.  Commit c60e520f6 turned such a situation into an infinite loop,
where before it'd just have resulted in garbage output.

The combination of these two bugs seems like it may explain bug #16694
from Tom Vijlbrief, though it's impossible to be quite sure without
direct inspection of the failing session.  (One needs to assume that
the pglz_maximum_compressed_size() bug caused us to fail to fetch the
second byte of a match tag, and what happened to be there instead was
a zero.  The reported infinite loop is hard to explain without off == 0,
though.)

Aside from fixing the bugs, rewrite associated comments for more
clarity.

Back-patch to v13 where both these commits landed.

Discussion: https://postgr.es/m/16694-f107871e499ec114@postgresql.org

src/common/pg_lzcompress.c

index d24d4803a9839fff48c36d46aadc5854fd5fad66..79747767ce08a0144b85d3496d8def6b74071825 100644 (file)
@@ -710,7 +710,6 @@ pglz_decompress(const char *source, int32 slen, char *dest,
 
        for (ctrlc = 0; ctrlc < 8 && sp < srcend && dp < destend; ctrlc++)
        {
-
            if (ctrl & 1)
            {
                /*
@@ -732,41 +731,62 @@ pglz_decompress(const char *source, int32 slen, char *dest,
                    len += *sp++;
 
                /*
-                * Now we copy the bytes specified by the tag from OUTPUT to
-                * OUTPUT (copy len bytes from dp - off to dp). The copied
-                * areas could overlap, to preven possible uncertainty, we
-                * copy only non-overlapping regions.
+                * Check for corrupt data: if we fell off the end of the
+                * source, or if we obtained off = 0, we have problems.  (We
+                * must check this, else we risk an infinite loop below in the
+                * face of corrupt data.)
+                */
+               if (sp > srcend || off == 0)
+                   break;
+
+               /*
+                * Don't emit more data than requested.
                 */
                len = Min(len, destend - dp);
+
+               /*
+                * Now we copy the bytes specified by the tag from OUTPUT to
+                * OUTPUT (copy len bytes from dp - off to dp).  The copied
+                * areas could overlap, so to avoid undefined behavior in
+                * memcpy(), be careful to copy only non-overlapping regions.
+                *
+                * Note that we cannot use memmove() instead, since while its
+                * behavior is well-defined, it's also not what we want.
+                */
                while (off < len)
                {
-                   /*---------
-                    * When offset is smaller than length - source and
-                    * destination regions overlap. memmove() is resolving
-                    * this overlap in an incompatible way with pglz. Thus we
-                    * resort to memcpy()-ing non-overlapping regions.
-                    *
-                    * Consider input: 112341234123412341234
-                    * At byte 5       here ^ we have match with length 16 and
-                    * offset 4.       11234M(len=16, off=4)
-                    * We are decoding first period of match and rewrite match
-                    *                 112341234M(len=12, off=8)
-                    *
-                    * The same match is now at position 9, it points to the
-                    * same start byte of output, but from another position:
-                    * the offset is doubled.
-                    *
-                    * We iterate through this offset growth until we can
-                    * proceed to usual memcpy(). If we would try to decode
-                    * the match at byte 5 (len=16, off=4) by memmove() we
-                    * would issue memmove(5, 1, 16) which would produce
-                    * 112341234XXXXXXXXXXXX, where series of X is 12
-                    * undefined bytes, that were at bytes [5:17].
-                    *---------
+                   /*
+                    * We can safely copy "off" bytes since that clearly
+                    * results in non-overlapping source and destination.
                     */
                    memcpy(dp, dp - off, off);
                    len -= off;
                    dp += off;
+
+                   /*----------
+                    * This bit is less obvious: we can double "off" after
+                    * each such step.  Consider this raw input:
+                    *      112341234123412341234
+                    * This will be encoded as 5 literal bytes "11234" and
+                    * then a match tag with length 16 and offset 4.  After
+                    * memcpy'ing the first 4 bytes, we will have emitted
+                    *      112341234
+                    * so we can double "off" to 8, then after the next step
+                    * we have emitted
+                    *      11234123412341234
+                    * Then we can double "off" again, after which it is more
+                    * than the remaining "len" so we fall out of this loop
+                    * and finish with a non-overlapping copy of the
+                    * remainder.  In general, a match tag with off < len
+                    * implies that the decoded data has a repeat length of
+                    * "off".  We can handle 1, 2, 4, etc repetitions of the
+                    * repeated string per memcpy until we get to a situation
+                    * where the final copy step is non-overlapping.
+                    *
+                    * (Another way to understand this is that we are keeping
+                    * the copy source point dp - off the same throughout.)
+                    *----------
+                    */
                    off += off;
                }
                memcpy(dp, dp - off, len);
@@ -820,21 +840,32 @@ pglz_decompress(const char *source, int32 slen, char *dest,
 int32
 pglz_maximum_compressed_size(int32 rawsize, int32 total_compressed_size)
 {
-   int32       compressed_size;
+   int64       compressed_size;
 
    /*
-    * pglz uses one control bit per byte, so we need (rawsize * 9) bits. We
-    * care about bytes though, so we add 7 to make sure we include the last
-    * incomplete byte (integer division rounds down).
+    * pglz uses one control bit per byte, so if the entire desired prefix is
+    * represented as literal bytes, we'll need (rawsize * 9) bits.  We care
+    * about bytes though, so be sure to round up not down.
     *
-    * XXX Use int64 to prevent overflow during calculation.
+    * Use int64 here to prevent overflow during calculation.
+    */
+   compressed_size = ((int64) rawsize * 9 + 7) / 8;
+
+   /*
+    * The above fails to account for a corner case: we could have compressed
+    * data that starts with N-1 or N-2 literal bytes and then has a match tag
+    * of 2 or 3 bytes.  It's therefore possible that we need to fetch 1 or 2
+    * more bytes in order to have the whole match tag.  (Match tags earlier
+    * in the compressed data don't cause a problem, since they should
+    * represent more decompressed bytes than they occupy themselves.)
     */
-   compressed_size = (int32) ((int64) rawsize * 9 + 7) / 8;
+   compressed_size += 2;
 
    /*
     * Maximum compressed size can't be larger than total compressed size.
+    * (This also ensures that our result fits in int32.)
     */
    compressed_size = Min(compressed_size, total_compressed_size);
 
-   return compressed_size;
+   return (int32) compressed_size;
 }