Categories: C, ZIP

Debugging Info-ZIP

We have seen that the Windows Info-ZIP build fails when invoked twice. I have created a sample zip file for debugging this issue along with a hexdump of it (I was glad to see that Cygwin includes hexdump). We observed that using the -sd flag showed a failure when reading the archive.

$ zip -usd ziphelp.zip *.txt
sd: Zipfile name 'ziphelp.zip'
sd: Command line read
sd: Reading archive
        zip warning: expected 3 entries but found 0

zip error: Zip file structure invalid (ziphelp.zip)

The sd: Readin archive line is output by the main function in zip.c. Since there is no other output prefixed by sd, execution must be continuing past the subsequent if-statement and into the readzipfile() function on line 4009.

Generate a Debug Zip Binary

Add debug = 1 to the top of win32\makefile.w32 then run these commands to generate PDBs along with the other binaries.

set LOCAL_ZIP=-DDEBUG
nmake -f win32\makefile.w32

Investigating the Failure

A quick way to set up Visual Studio to debug this issue is to create a new C++ console application then change the command, command arguments, and working directory of the project as shown below.

Setting Visual C++ Project Properties for Debugging Zip.exe

Pressing F10 should now start debugging the zip binary. The highlights are:

  1. The main function in zip.c calls readzipfile
  2. readzipfile calls scanzipf_regnew which computes an offset to start looking for the End of Central Directory Record (EOCD) in the zip file by trying to seek 128KiB from the end of the file. In our case, the zip file is less than 128 KiB so scanzipf_regnew seeks to the beginning of the file.
  3. scanzipf_regnew scans for the EOCD record signature (PK56 or hex bytes 50 4b 05 06). There is only 1 EOCD signature in the zip file and it is at offset 0x227C. It then (correctly) computes the offset of the start of the data after the EOCDR signature (0x2280) and seeks to that offset in the file.
  4. 18 bytes are now read from the EOCDR.
  5. The 32-bit offset of the start of the central directory, relative to start of archive is then read from the record and stored in the in_cd_start_offset variable. This is correctly read as 0x218e (the last 4 bytes on line 553 of the zip’s hexdump output).
  6. The number of entries is also read from the EOCDR (which is how zip knows to expect 3 entries).
  7. scanzipf_regnew now seeks to the first CD entry
  8. then looks for the next signature to process. Interestingly, it finds the EOCDR signature (PK56) and breaks out of the loops (signature scan and zipfile disks). Since it did not find any entries, it then displays the error about the expected number of entries vs the number found and returns an error code to the main function in zip.c, which in turn calls ziperr to print an error message and terminate the process.

The question now is why scanzipf_regnew does not find any entries since there are 3 PK12 and 3 PK34 signatures in the file. Let us inspect the file offsets right after the code seeks to in_cd_start_offset (from step 5 above) and immediately before looking for the next signature to process.

 uzoff_t zipoffset_before_finding_signatures =  zftello(in_file);

This shows a value of 0x0000000000002168 which is not the proper offset in the zip file to seek to. Recall that in_cd_start_offset was 0x218e, which is the second-last byte of line 537 in the zipfile’s hexdump output. Could this be an error in the standard library fseek and ftell functions? TODO: why does the scan fail from an earlier starting point?

Pressing F12 to see the definitions of zftello and zfseeko went to the wrong place! These are not the standard library functions being used. Visual Studio was opening their definitions in tailor.h instead of the actual implementations being called. Turns out zftello and zfseeko are functions implemented in win32/win32i64.c. These comments above zftello raise some huge red flags.

/* 64-bit buffered ftello
 *
 * Win32 does not provide a 64-bit buffered
 * ftell (in the published api anyway) so below provides
 * hopefully close version.
 * We have not gotten _telli64 to work with buffered
 * streams.  Below cheats by using fgetpos improperly and
 * may not work on other ports.
 */

zftello’s comments do not sound any more reassuring:

/* 64-bit buffered fseeko
 *
 * Win32 does not provide a 64-bit buffered
 * fseeko so use _lseeki64 and fflush.  Note
 * that SEEK_CUR can lose track of location
 * if fflush is done between the last buffered
 * io and this call.
 */

Looks like this custom seek/tell code is responsible for the incorrect offsets into the zip file! We can work around this by simply removing these custom implementations.

Windows Specific Bug?

So why didn’t Cygwin’s zip.exe have this issue? Running zip.exe -v shows this compiler/OS description:

Compiled with gcc 4.8.3 for Unix (Cygwin) on Jun 23 2014.

The Cygwin OS name in parenthesis is defined in unix/unix.c only if __CYGWIN__ is defined. However, under this condition, the custom zftello and zfseeko implementations will not be included in the zip sources being compiled! Therefore, the issue does not occur in Cygwin’s distributed zip binary.


Categories: C, Cygwin

Testing Info-ZIP

After figuring out how to build the Info-ZIP sources, I had a few commands to test the zip file by creating a few text files to zip.

zip -h > help.txt
zip -h2 > help2.txt
zip -L > license.txt

Unfortunately, the zip -qru ./files.zip -i *.txt command from the OpenJDK is not what we need. To actually create a zip file, use only the -u flag

zip -u files.zip *.txt

To test that the files were zipped successfully, unzip the files and compare them to the original files. Here’s the whole script for this:

echo "---Creating temp directory---"
mkdir temp; cd temp

echo "---Creating text files---"
zip -h > help.txt
zip -h2 > help2.txt
zip -L > license.txt

echo "---Adding text files to a new repo---"
git init
git add *.txt
git commit -m "Add original text files"

echo "---Zipping text files---"
zip -u files.zip *.txt

echo "---Removing text files---"
rm *.txt

echo "---Unzipping text files---"
unzip files.zip

echo "---Checking unzipped files---"
git diff

When using the zip binary for Windows, something strange happens when running the zip command a 2nd time:

$ zip -u files.zip *.txt
        zip warning: files.zip not found or empty
  adding: help.txt (176 bytes security) (deflated 49%)
  adding: help2.txt (176 bytes security) (deflated 62%)
  adding: license.txt (176 bytes security) (deflated 54%)

$ zip -u files.zip *.txt
        zip warning: expected 3 entries but found 0

zip error: Zip file structure invalid (files.zip)

Info-ZIP supports a -sd flag that shows diagnostic information while it runs. It reveals that something is going wrong when reading the archive.

$ zip -usd files.zip *.txt
sd: Zipfile name 'files.zip'
sd: Command line read
sd: Reading archive
        zip warning: expected 3 entries but found 0

zip error: Zip file structure invalid (files.zip)

This is filed as Running zip twice fails with invalid file structure error · Issue #35 · swesonga/Info-ZIP (github.com). Observe that this doesn’t happen when using the original zip binary that shipped with Cygwin:

$ zip.original -usd files.zip *.txt
sd: Zipfile name 'files.zip'
sd: Command line read
sd: Reading archive
sd: Scanning files
sd: Applying filters
sd: Checking dups
sd: Scanning files to update
sd: fcount = 0

Why is this bug only in the Windows build?


Categories: C, Cygwin, Windows

Windows vs Cygwin File Paths

In the last post, I described how to build the Info-ZIP sources. When using the resulting zip binaries in Cygwin, some important path handling issues come up. The paths passed to the zip binary when building the OpenJDK in Cygwin use forward slashes. The Cygwin User’s Guide has a section on File Access that outlines the support for POSIX and Win32-style file paths.

The Windows file system APIs support forward slashes in file paths. The zip source code uses the fopen CRT function, which eventually ends up calling CreateFileW. The CreateFileW docs state that you may use either forward slashes (/) or backslashes (\) in the lpFileName parameter. The translation of paths from Win32 to NT happens in a function called RtlDosPathNameToRelativeNtPathName_U as discussed in the Definitive Guide on Win32 to NT Path Conversion. Since this is a built-in Windows function, it does not support the /cygdrive/ style prefixes. Running the simple test program argtofile in Cygwin easily demonstrates this.

The /cygdrive/ prefixes will therefore not work for programs compiled for Windows (such as the zip binary directly compiled using Visual C++). Therefore, the cygpath command is necessary to translate these paths to Win32-style file paths. To peek into how cygpath works, we can take advantage of the fact that the source code for the cygpath utility is available online. I found it easier to browse the sources after cloning the repo:

git clone https://cygwin.com/git/newlib-cygwin.git

The scenario of interest is what happens when cygpath -u ~ is invoked. In this case, we want to see how the “/cygdrive/” string is prefixed to the computed path.

  1. Execution flows from main > action > do_pathconv
  2. do_pathconv calls cygwin_conv_path which
  3. calls the conv_to_posix_path method of the mount_table which then
  4. normalizes the path by calling normalize_win32_path
  5. before finally iterating through the mount items to find the path’s prefix in the mount table.

Also searching for the cygpath \s*( regex leads to the vcygpath function in winsup/utils/path.cc. That appears to be more directly related to the cygpath command (how?). Searching for the \"cygdrive\" regex also reveals that this is a magic string used in many places in the codebase.

All this shows that there is indeed some complexity behind maintaining the POSIX/Win32-style file path mapping in Cygwin but it should be possible to add some basic logic to the Windows Info-ZIP build to handle /cygdrive/ prefixes in its file arguments. The question I have at this point is how does compiling the zip binaries for the Cygwin environment (the shipping configuration) result in proper handling of POSIX-style filenames?


Categories: C, Java

Building Info-ZIP Source Code

I have been working on building the OpenJDK for the Windows ARM64 platform. The make images command has been failing in Cygwin with errors such as:

Creating java.se.jmod
zip I/O error: Device or resource busy
zip error: Could not create output file (/cygdrive/d/dev/repos/jdk/build/windows-aarch64-server-release/support/src.zip)
make[4]: *** [ZipSource.gmk:79: /cygdrive/d/dev/repos/jdk/build/windows-aarch64-server-release/support/src.zip] Error 1
make[4]: *** Deleting file '/cygdrive/d/dev/repos/jdk/build/windows-aarch64-server-release/support/src.zip'
make[3]: *** [ZipSource.gmk:93: zip] Error 2
make[2]: *** [make/Main.gmk:389: zip-source] Error 2
make[2]: *** Waiting for unfinished jobs....

ERROR: Build failed for target 'images' in configuration 'windows-aarch64-server-release' (exit code 2)

One of the troubleshooting approaches I tried was to build an instrumented zip executable to replace Cygwin’s zip.exe. I started by searching for the Cygwin zip package. Someone was kind enough to have a link to the Cygwin zip package’s official page which in turn linked to the Cygwin zip packaging repository. This repo’s tree view shows a single file with source URIs for zip and that’s how I learned that this zip utility was Info-ZIP. The Zip 3.0 page has a link to Info-ZIP’s SourceForge site, from which the zip sources can be downloaded. I used curl in the Windows Terminal as follows:

curl -Lo zip30.tar.gz https://sourceforge.net/projects/infozip/files/Zip%203.x%20%28latest%29/3.0/zip30.tar.gz/download
tar xvf zip30.tar.gz
cd ./zip30
git init; git add *; git commit -m "Commit original Info-ZIP sources"

Now that we have the sources, let’s see how to build them. The scenario I’m working on is Windows specific so we need Visual Studio 2019 with the Desktop Development with C++ workload installed. I’ll be building a 32-bit zip executable. Launch the x86 Native Tools Command Prompt for VS 2019 and change to the zip30 source directory to start building. Some digging around reveals a makefile with build instructions (that seem one directory off). Here’s the command to build a 32-bit executable from the sources (note that building fails due to various errors that need to be addressed):

nmake -f win32\makefile.w32

Carriage Return (CR) Name Collisions

The first error is this rather cryptic mess of syntax errors:

Microsoft (R) Program Maintenance Utility Version 14.29.30133.0
Copyright (C) Microsoft Corporation.  All rights reserved.

        cl -nologo -c -W3 -O2 -DWIN32 -DASM_CRC -ML  zip.c
cl : Command line warning D9002 : ignoring unknown option '-ML'
zip.c
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18822): error C2143: syntax error: missing ':' before 'constant'
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18822): error C2143: syntax error: missing ';' before ':'
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18822): error C2059: syntax error: ':'
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18823): error C2143: syntax error: missing '{' before ':'
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18823): error C2059: syntax error: ':'
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18824): error C2059: syntax error: '}'
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18825): error C2059: syntax error: '}'
C:\Program Files (x86)\Windows Kits\10\include\10.0.19041.0\um\winnt.h(18826): error C2059: syntax error: '}'
zip.c(5746): warning C4267: '=': conversion from 'size_t' to 'ush', possible loss of data
zip.c(5838): warning C4267: '=': conversion from 'size_t' to 'ush', possible loss of data
NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.29.30133\bin\HostX86\x86\cl.EXE"' : return code '0x2'
Stop.

Some head banging (and not looking at the precompiler output from cl -c -W3 -O2 -DWIN32 -DASM_CRC /P zip.c carefully) leads to c – Compiler errors in WINNT.H after retargeting solution to latest SDK (10.0.18362) – Stack Overflow where the solution is rather simple: do not use the name CR to define the carriage return since it maps to a bitfield in one of the structs in winnt.h. See Rename CR identifier to avoid collisions with ARM64 structs in winnt.h by swesonga · Pull Request #2 · swesonga/Info-ZIP (github.com)

Outdated Linker Flags

The next error is a complaint about the /OPT:NOWIN98 flag.


        link -nologo user32.lib advapi32.lib /OPT:NOWIN98 /INCREMENTAL:NO /PDB:zip.pdb  /RELEASE zip.obj crypt.obj ttyio.obj zipfile.obj zipup.obj fileio.obj util.obj  crc32.obj crci386c.obj globals.obj deflate.obj trees.obj match32.obj win32.obj win32zip.obj nt.obj win32i64.obj zip.res
LINK : fatal error LNK1117: syntax error in option 'OPT:NOWIN98'
NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.29.30133\bin\HostX86\x86\link.EXE"' : return code '0x45d'
Stop.

Turns out this option was removed in Visual Studio 2010 as per the Microsoft C/C++ change history since the linker no longer supports optimizing for Windows 98. This is clearly a safe flag to remove from the linker flags in win32\makefile.w32.

Update the Branding

  1. Change the VERSION string from “3.0” to “3.0-ioHardenedZIP”
  2. Update the REVDATE from “July 5th 2008” to the current date (“December 18th 2021” in my case)
  3. Update the about text to indicate that it is a custom build.

Testing the Zip Build

The sources should now build successfully in the x86 Native Tools Command Prompt for VS 2019. The OpenJDK build uses the -qru flags for creating zip files so we can easily test the zip executable by creating a zip of the Info-ZIP help and license text.

zip -h > help.txt
zip -h2 > help2.txt
zip -L > license.txt
zip -qru ./files.zip -i *.txt

We need to verify whether the zip was correctly created. Saving this for another day.

Building on Linux/macOS

[Update 2021-12-22] To build Info-ZIP on macOS, the way the memset function is detected needs to be fixed. Info-ZIP for either Linux or macOS can then be built using this command:

make -f unix/Makefile generic