User Tools

Site Tools


blog:pushbx:2026:0711_early_july_compression_woes

Early July: Compression woes

2026-07-11

This week I found some bugs in several compression details, and I also found something I thought was a bug but isn't.

lDOS kernel

mvcomp

lDOS boot

lfreedos

testcy

kernwrap

Most of these changes are mirrored in lDebug's mak.sh, either imported from lDebug to kernwrap or the other way around.

lDebug

testrand

This is a test program for inicomp to test maximum block sizes with lots of uncompressible data. The data used was generated from the server's /dev/random device.

LZSA

  • Problem described in ticket #1011. The lzsa packer operated on 64 KiB blocks, but with the frame header taken into account, a compressed block consisting of exactly 64 KiB of compressed data could start eg at offset 6 (of a normalised pointer). Therefore the offset could overflow if not specifically normalised during processing of this block.

The lack of pointer normalisation is a major feature of the LZSA2 depacker, so it was undesirable to work around the problem on our end. Instead, I added the -S switch which makes the packer operate on 65_520 B blocks rather than full 64 KiB ones. Now regardless of the frame header, a normalised address at the start of a block (compressed or uncompressed) won't overflow until after the block is completely processed.

The patch

New commit added:

Add capital -S, smaller blocks (⇐ 65_520 rather than ⇐ 65_536)

This insures that whatever offset 0..15 is in SI after having just processed a frame header (either for a compressed or uncompressed block), adding the length of the source block won't overflow SI past 65_535. This invariant is required to efficiently handle large blocks without intra-block pointer normalisation. With large uncompressible (random data) blocks, lDOS inicomp could fail to depack as SI + length of block could overflow.

My comment on the lzsa Pull Request

inicomp

  • Add _UNSAFE option for LZSA2 depacker. Decreases the amount of buffer size bookkeeping kept track of.
  • Support _N option to make use of lzsa -N switch.
  • For _UNSAFE and _N both selected, completely get rid of buffer lengths bookkeeping.
  • Fix, account for lengths of uncompressed blocks. This never came up because typically there are no uncompressed blocks. The testrand example proved that this fix is needed.
  • Add macros for note section and note incbin. This allows to post-process using convlist.pl so that the listing file of embedded binaries can be listed in a resulting .tls file. The note incbin facility was lifted from instsect, while the note section one was lifted from lDebug.
  • Add scan support to inicomp. This is a scanner that runs ahead of the usual tests of the test program, displaying statistics on the payload such as its depacked size and the "maximum bytes ahead" amount. The latter is equivalent to calculating the allocation size needed to depack, but as opposed to inicomp's stubborn repeated depacking binary search loop, the exact same result is obtained from a single scan run.
  • Support scan mode in LZSA2 depacker. This scanner doesn't require an output buffer, only processing the source data and keeping track of a virtual output pointer that starts out at all-zeroes.
  • Support scan mode in mvcomp depacker. This is relevant because the discussions around the mvcomp packer and the mvsize tool that I contributed produced the algorithm with which the scanner determines the maximum bytes ahead using a single scan run. This is the plussrc and plusdst method, which keeps track of how far into each buffer the source and destination pointers are. The checks are needed after expanding matches that take more space in the decompressed buffer than in the compressed data, precisely when my existing code called check_pointers_not_overlapping, so the scan checks are done in that function.
  • In inicomp.asm pass the buffer to scan function. The existing (LZSA2 and mvcomp) scanners do not need this and will ignore the additional parameters on the stack. However, the buffer is needed for the next scanner to be added.
  • Place the scan binary at origin 0 in the INIT2 section. Needed if the scanner refers to its own code section using offsets rather than only relative branches.
  • Fix LZMA-lzip depacker for small buffer sizes, the bxcx subtraction can check both registers *only* for subtraction borrow (CY). It tried checking for ZR also (jbe), which is invalid, as it branches also if only the upper word is zero. Add work around code to check for cx being zero separately.
  • Drop a forgotten jcxz in the LZMA-lzip depacker. This early oversight did not lead to a disaster (and much more obvious bug) purely due to luck. Bug in hg 158d3d0479fd on 2020-04-12.
  • In mvcomp.asm for _SCAN binary make sure offset 0 is the entrypoint.
  • Support scan mode for LZMA-lzip depacker. Unlike the earlier scanners, this one requires a destination buffer. The reasons are twofold: There's the local variables, some 16 KiB of data structures that are set up dynamically in the destination buffer. These are intrinsically required by the depacker. The other reason is the fact that the depacker "peeks" into the past decompressed data to steer its control flow for depacking subsequent data.
  • Change the error message for lack of scan mode support to a %fatal error. This avoids the spam about labels "changed during code generation" that is produced when using %error

lmacros

ETA:

  • Disable label-redef-late warning. This addresses the same problem as the last inicomp change, but instructs the assembler to silence this warning. As mentioned in the comment, we previously used grep commands to filter out the uninteresting warnings.

I commented on a 2024 SvarDOS bugz thread discussing the mvcomp format, as well as the three methods to determine "max bytes ahead" or the "allocation" required for depacking with destination starting below and overlapping the source. As previously discussed on the blog.

2. I also added a maxbytesahead calculation to the first depack run. The resulting maxunitsahead should match what the stubborn mvsize run calculates. That means if everything works we don't have to do the entire run any longer, just the first depack call would suffice.

I just updated my inicomp LZSA2, mvcomp, and LZMA-lzip depackers with a "scan" mode. This returns four results:

  • Success or error status
  • Progress ticks (how often the counter variable is incremented)
  • maxbytesahead
  • Depacked size

The maxbytesahead calculation uses my technique from mvsize mentioned above. The scan caller can trivially calculate the maxparasahead (just divide by 16, rounding up) and then the allocation needed (maxparasahead plus compressed payload size plus init1 depacker size), which absent bugs exactly matches the stubborn binary search result that is found by repeatedly running the depacker.

Out of these methods, mvcomp and LZSA2 never refer to the depacked data except for window matches that generate more depacked data. Therefore, their scanners do not actually need any destination buffer memory and will work on virtual pointers. LZMA-lzip however, I found it does "peek" into previously depacked data to control the decompression, so its scanner has to actually depack the data.

Here's an example:

~/proj/testrand$ INICOMP_WINNER=smallest INICOMP_METHOD='lzsa2 mvcomp lzd' use_build_decomp_test_size=1 use_build_decomp_scan=1 use_build_decomp_test_file=1 use_build_decomp_test_xms=1 ./mak.sh -DEMPTY

This uses the testrand, kernwrap, lmacros, ldosboot, inicomp, and scanptab repos from our hgweb

And here's the parts of the example output that are interesting:

About to Execute : ttestr.com
Note: Scan depack=48 pack=49 ahead=4,para=1 alloc=84 tick=2
Info: 1SSSSSSF
84
About to Execute : ttestr.com c
Note: Scan depack=48 pack=49 ahead=4,para=1 alloc=84 tick=2
0
      0 progress dots, method            lzsa2
...
About to Execute : ttestr.com
Note: Scan depack=48 pack=38 ahead=10,para=1 alloc=59 tick=4
Info: 1SSSSSSF
59
About to Execute : ttestr.com c
Note: Scan depack=48 pack=38 ahead=10,para=1 alloc=59 tick=4
0
      0 progress dots, method           mvcomp
...
About to Execute : ttestr.com
Note: Scan depack=48 pack=74 ahead=14688,para=918 alloc=1099 tick=36
Info: 1SFFSFFFSSSF
1099
About to Execute : ttestr.com c
Note: Scan depack=48 pack=74 ahead=14688,para=918 alloc=1099 tick=36
0
      0 progress dots, method              lzd

(LZMA-lzip has several KiB of local variables that it puts into the depack buffer, hence the large maxbytesahead.)

Note how the alloc= exactly matches the number displayed after the Info: 1… line.

Discussion

E. C. MaslochE. C. Masloch, 2026-07-11 20:35:24 +0200 Jul Sat

While working on the LZMA-lzip scanner, I also found a bug in dosemu2: Under certain conditions, tracing a retn instruction will run one instruction too many.

You could leave a comment if you were logged in.
blog/pushbx/2026/0711_early_july_compression_woes.txt · Last modified: 2026-07-11 20:33:58 +0200 Jul Sat by ecm