Skip to content

[Xamarin.Installer.AndroidSDK] Drop SharpZipLib - #13464

Closed
simonrozsival wants to merge 10 commits into
mainfrom
simonrozsival-replace-sharpziplib-installer
Closed

simonrozsival wants to merge 10 commits into
mainfrom
simonrozsival-replace-sharpziplib-installer

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Context: The Android SDK installer uses SharpZipLib for decompression and metadata that ZipArchiveEntry does not expose on its target framework. A built-in-only extraction path lost extended Unix timestamps and Windows DOS attributes.

Changes: Use ZipArchive for streaming decompression and a bounded central-directory reader for timestamps, creator platform, and external attributes. Remove SharpZipLib from the installer, build tasks, tests, and installer packaging.

Two security review rounds shaped the extraction hardening:

  • ZIP and ZIP64 end records could make the two readers select different central directories. Conflicting or ambiguous records are now rejected.
  • Path containment uses ordinal comparison, and existing reparse points are rejected.
  • Unix staging directories are private, and group/other write bits are removed from regular files.
  • Relative Unix symlinks are preserved when extracting and copying packages. They must resolve inside the install root without climbing above it, and must not be dangling or cyclic. package.xml is never written through a link.
  • Symlinks with absolute targets (e.g. the four bin/as entries in NDK r23b), and all Unix symlink entries on Windows, are stored as regular files containing the target text, as SharpZipLib did.

Compatibility:

  • BZip2, some prefixed/self-extracting archives, and SharpZipLib's local-header validation are not retained. The 560-URL approved-feed audit examined central-directory metadata, not every local header.
  • The CalculateTotalSize public signature changes from ZipFile to ZipArchive.
  • Set-ID, sticky, and group/other write bits are stripped.
  • Relative symlinks that escape, climb above the install root, dangle, or loop are rejected.
  • Existing destination paths must be trusted: path checks cannot prevent concurrent filesystem changes.
  • .NET 10 can accept some malformed ZIP entries that SharpZipLib rejected, so archive checksum verification remains important.

This change does not include the framework retargeting in #12988, which updates some of the same project files. Dependency removal is the goal; no performance improvement is claimed.

Validation on macOS, using a session-local clone of the .NET SDK 12.0.100-alpha.1.26551.101 with private HOME, CLI, NuGet, XDG, and temp state:

  • dotnet test tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj --no-restore -v minimal -p:AndroidToolsDisableMultiTargeting=false -p:DotNetTargetFrameworkVersion=11.0 — 564 passed, 7 skipped (net11.0). An unrelated fake-emulator test failed once, then passed on retry. The permissions fixture covers Unix and DOS creator metadata; native Windows execution was not available locally.
  • dotnet build src/Xamarin.Installer.Build.Tasks/Xamarin.Installer.Build.Tasks.csproj -v quiet — passed.
  • dotnet build src/Xamarin.Installer.AndroidSDK/Xamarin.Installer.AndroidSDK.csproj -v quiet -p:DefineConstants=WINDOWS — passed as a Windows-conditional compile, not Windows execution.
  • A session-local harness extracted and copied three checksum-verified NDK archives with the production code. Linux NDK r23b gave 7,955 files and 25 links, with its 4 absolute-target entries stored as files. Linux NDK r28c gave 8,414 files and 35 links; macOS NDK r28c gave 8,342 files and 23 links.

Native Linux and Windows execution, .NET Framework MSBuild, and full installer packaging remain unvalidated.

Copilot AI balanced review requested due to automatic review settings November 10, 2026 18:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The parser rejects valid ZIP archives containing an optional central-directory digital signature, and the new tests do not exercise the custom ZIP64 parsing path.

2 open issues
What changed in this PR

Replaces SharpZipLib in the Android SDK installer with .NET ZIP APIs, removing the dependency without the framework retargeting planned in #12988.

Changes:

  • Uses streaming extraction with progress reporting and retains the existing path traversal check.
  • Adds central-directory metadata parsing for timestamps and attributes, with extraction and ZIP64 progress tests.
  • Removes SharpZipLib package references, its version property, and installer packaging entries.
File Description
tests/​Xamarin.Android.Tools.AndroidSdk-Tests/​Xamarin.Android.Tools.AndroidSdk-Tests.csproj Removes the SharpZipLib test dependency.
tests/​Xamarin.Android.Tools.AndroidSdk-Tests/​AndroidSdkInstallerTests.cs Adds extraction, metadata, traversal, and ZIP64 progress tests.
src/​Xamarin.Installer.Build.Tasks/​Xamarin.Installer.Build.Tasks.csproj Removes SharpZipLib package and symbol-copy references.
src/​Xamarin.Installer.AndroidSDK/​Xamarin.Installer.AndroidSDK/​DirectorySizeMonitoringTimer.cs Uses .NET ZIP APIs for size and entry counts.
src/​Xamarin.Installer.AndroidSDK/​Xamarin.Installer.AndroidSDK/​Common/​ZipCentralDirectory.cs Reads supplementary ZIP metadata and ZIP64 directory locations.
src/​Xamarin.Installer.AndroidSDK/​Xamarin.Installer.AndroidSDK/​Common/​Helper.cs Migrates extraction and metadata restoration; strips special Unix permission bits.
src/​Xamarin.Installer.AndroidSDK/​Xamarin.Installer.AndroidSDK.csproj Removes the installer’s SharpZipLib dependency.
src/​Xamarin.Android.Build.Tasks/​Tests/​Xamarin.Android.Build.Tests/​Xamarin.Android.Build.Tests.csproj Removes the build-test SharpZipLib reference.
Directory.Build.props Removes the SharpZipLib version property.
build-tools/​installers/​create-installers.targets Stops packaging SharpZipLib binaries and symbols.

Review effort: Balanced

Comment on lines +58 to +62
while (reader.BaseStream.Position < centralDirectoryEnd) {
if (centralDirectoryEnd - reader.BaseStream.Position < CentralDirectoryHeaderSize)
throw new InvalidDataException ("Unexpected end of ZIP central directory.");
if (entries.Count == expectedEntryCount)
throw new InvalidDataException ("ZIP central directory contains too many entries.");
Comment on lines +47 to +48
if (centralDirectorySize == uint.MaxValue || centralDirectoryOffset == uint.MaxValue)
ReadZip64CentralDirectoryLocation (reader, endOfCentralDirectoryPosition, out centralDirectoryOffset, out centralDirectorySize);
simonrozsival and others added 5 commits November 10, 2026 22:05
Xamarin.Installer.AndroidSDK used ICSharpCode.SharpZipLib only to
extract SDK package archives.  Use System.IO.Compression.ZipArchive
for decompression, plus a small bounds-checked central directory
reader (ZipCentralDirectory) to recover the metadata that the
netstandard2.0 ZipArchive API doesn't expose:

  * "version made by" host system (DOS attributes on Windows only for
    DOS/NTFS-created entries, as before),
  * external file attributes (Unix permission bits for chmod),
  * extended Unix timestamp (0x5455) mtime, and SharpZipLib's
    entry-name decoding and directory detection.

Entries are cross-checked against ZipArchive (count and sizes) before
extraction.  Zip-slip and long-path handling are unchanged.

Remove the SharpZipLib PackageReference, installer packaging entries
and the SharpZipLibVersion property.

Verified on 7 real SDK archives (build-tools r36 mac/windows,
platform-tools r37.0.1, platform-37.2, cmake 4.1.2, cmdline-tools,
source-37.2): identical file contents, sizes, modes, attributes and
mtimes vs. main.  Extraction is 1.2-2.3x faster on .NET 10.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Extract entry data with a validating reader instead of ZipArchive so
the installer keeps SharpZipLib 1.3.3's extract-time checks: local
header signature, version/flags, local vs. central size consistency
(including ZIP64 and data descriptors), encrypted entries and truncated
deflate data. Entry output must also match the central directory size.

The validation logic is derived from SharpZipLib (MIT), so add its
notice to THIRD-PARTY-NOTICES.TXT.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Context: db33386

The earlier SharpZipLib replacement implemented a separate ZIP reader to reproduce metadata and local-header validation.  This substantially increases the amount of archive-handling code maintained by the installer.

Use System.IO.Compression.ZipArchive directly for extraction and entry counting, retaining the existing parent-traversal check.  Delete an existing destination before extraction and restore Unix permissions using Mono.Unix so duplicate entries, mode-zero entries, and files lacking owner-read permissions retain the expected modes.  Strip set-ID and sticky permission bits.  Access the entry attributes through a cached property lookup because the netstandard2.0 reference API does not expose them.

Restore coverage for data descriptors and ZIP64 entry counts.  Remove the custom reader and its SharpZipLib-derived code and notices.

This is not a behavior-equivalent replacement: SharpZipLib's local-header checks and support for some ZIP variants are no longer preserved, Windows DOS attributes are not restored, and the public CalculateTotalSize parameter type changes.  These compatibility differences require review before the change is shipped.

Eight counterbalanced rounds on seven SDK archives favored the candidate median for six archives; CMake was about 9% slower on the shared host.  The extracted files matched the baseline in content, mode, attributes, and modification time on macOS, excluding nondeterministic directory timestamps.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Some Android system-image archives consist almost entirely of one multi-gigabyte file. Reporting progress only after each entry is extracted leaves the installer apparently stalled for most of the operation.

Read each ZipArchiveEntry through the built-in stream API and copy with a reusable buffer, reporting progress as data is written. Keep the existing destination-file and Unix permission handling without introducing a ZIP metadata reader.

A 32 MiB one-entry repro reports intermediate progress again; the previous candidate reported only 0% and 100%. The existing installer tests pass, and seven SDK archives match baseline extraction contents and metadata on macOS.

This replacement remains conditional: ZipArchive does not expose extended Unix timestamps, and a checksum-verified approved Google archive extracts with different timestamps than SharpZipLib.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use ZipArchive for streaming extraction and read only central-directory metadata that ZipArchiveEntry does not expose. Preserve extended timestamps, creator-specific attributes, Unix permissions, and DOS directory markers while keeping the existing traversal guard and progress reporting.

BZip2, prefixed ZIP, and local-header validation support from SharpZipLib is not retained; the approved Android SDK feed archives do not use those features.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsival force-pushed the simonrozsival-replace-sharpziplib-installer branch from c67efaa to 2b81f2f Compare November 10, 2026 21:07
simonrozsival and others added 5 commits November 10, 2026 22:48
Context: #13452

The rebase conflict resolution restored Polly package references even though main had already replaced the installer retry policy. Remove those references and the unused version property; the retry implementation remains unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Context: #13464

ZIP64 and comment records can cause ZipArchive and the metadata reader to select different central directories. Reject conflicting records before applying attributes to extracted files, and use ordinal path containment with checks for existing reparse points.

The NDK archives contain Unix symbolic links. Extract validated in-tree links after regular files and preserve them when copying the component. Use private Unix staging directories and remove group and other write permissions from extracted regular files.

These checks require a trusted destination: concurrent changes to an existing extraction directory can still race path validation. Some previously accepted archive layouts and symbolic links are now rejected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Context: #13464

A second security review of 1f5f04d found four issues in the new symbolic link support:

  * NDK r23b (23.1.7779620) failed to install on macOS and Linux. Its four `bin/as` entries are symlinks to absolute paths on Google's build machines, which the new validation rejected. Entries with absolute targets are once again stored as regular files containing the target text, as SharpZipLib did.
  * A relative target could climb above the temporary install root with leading `..` and re-enter through the root's archive-controlled name. Once the component was moved, the link pointed into a sibling SDK component, and writing `package.xml` through it overwrote that component's file. Leading `..` segments may no longer climb above the install root, and `GeneratePackageXml()` deletes an existing link before writing.
  * The missing/cyclic target check never fired, because `File.Exists()` uses `lstat()` on Unix. Use `stat()`, which follows the link.
  * Windows threw `PlatformNotSupportedException` for Unix symlink entries. These are again stored as regular files, as before.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Context: #13464

The ZIP tests added by this PR had grown to 21 cases with repeated temp-directory and archive boilerplate. Keep only tests that guard behavior this PR changes, and consolidate overlapping ones:

  * Fold the set-ID, group/other write, and unspecified-mode cases into `UnzipPreservesContentsTimestampsAndPermissions`.
  * Merge the `../` and case-variant traversal tests into `UnzipRejectsPathsOutsideDestination`.
  * Fold the absolute symlink target (NDK r23b) case into `UnzipAndMovePreserveUnixSymbolicLinks`.
  * Merge the valid ZIP64, conflicting ZIP64, and end-record-in-comment cases into `UnzipRequiresUnambiguousCentralDirectory`.
  * Drop the reflection-based `TemporaryDirectory` test, the empty-archive top-level symlink edge case, and the reflection added to `CalculateMoveProgressSplitSupportsZip64Archives`.

Unix-only tests now set modes through `ZipArchiveEntry.ExternalAttributes` instead of patching the central directory. Reverting the permission mask or the ordinal containment check still fails the consolidated tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Associate the all-bits-set unspecified mode with Unix ZIP metadata instead of treating it as DOS directory attributes. Exercise a normal DOS entry as well.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #13464

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code assessment: no merge-blocking defects found. Reviewed the diff and all 15 changed files, including extraction callers, package relocation/cleanup, target-framework constraints, dependency removal, and the new fixtures. Findings: 0 errors, 0 warnings, 2 non-blocking suggestions, posted inline for actual ZIP64 count-overflow extraction coverage and Windows handling of Unix-symlink entries.

The bounded metadata reader, explicit permission handling, and extraction/move regression tests are useful improvements. The dependency removal is consistent across the affected projects and installer file list; the PR description explicitly identifies the intentional compatibility changes.

⚠️ Verification incomplete: all 40 GitHub checks returned for the current head report success, including the aggregate dotnet-android check for Azure build 1674857. Direct Azure build-result/timeline inspection was blocked because az devops invoke requires login in this environment, so I cannot independently confirm the Azure result. A focused dotnet test attempt for AndroidSdkInstallerTests, using net10.0 overrides for the available SDK, failed during CLI initialization with an IOException creating the NuGet-Migrations mutex, before tests ran. These are review-environment limits, not demonstrated PR failures.

Generated by Android PR Reviewer for #13464 · copilot · gpt61 · 50.4 AIC · ⌖ 4.89 AIC · ⊞ 20.2K
Comment /review to run again

zip.CreateEntry ($"f{i}");
}

Assert.AreEqual (50, DirectorySizeMonitoringTimer.CalculateMoveProgressSplit (temp.ZipPath));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 Testing — Consider also covering extraction of a ZIP64 archive whose actual entry count exceeds 65,535. This test exercises only CalculateMoveProgressSplit, which uses the framework reader; UnzipRequiresUnambiguousCentralDirectory exercises the new metadata reader with a one-entry ZIP64 fixture. Neither currently checks that ZipCentralDirectory.Read accepts the real count-overflow case and keeps its metadata aligned with ZipArchive.Entries.

{Rule: Test edge cases}

}
#if !WINDOWS
// On Windows, and for absolute targets, keep the previous behavior: store the target text as a regular file.
if (metadata.IsUnixSymlink && !Platform.IsWindows) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 Testing — Could you add a Windows-only regression test for the documented Unix-symlink fallback? Force creator platform 3 and symlink mode with the existing SetUnixAttributes helper, extract on Windows, and assert that the result is a regular file containing the target text. The current symlink tests skip Windows, and archives created by WriteEntry on Windows have Windows creator metadata, so those fixtures would not establish this compatibility behavior without the explicit metadata override.

{Rule: Test platform-specific behavior}

@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Nov 11, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

@dalexsoto review

@dalexsoto dalexsoto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 15 changed files statically at head 094deee, covering ZIP/ZIP64 central-directory parsing bounds, path containment and symlink/reparse-point handling, permission stripping, temp-directory handling, callers of the changed CalculateTotalSize signature, target-framework API availability, packaging/csproj removals and tests, plus a separate final search for additional blockers. No blocking issue found; the documented compatibility tradeoffs are accepted. Non-blocking note: the strict parser rejects the rare central-directory digital-signature record (SecureZIP-signed archives), which is a possible follow-up. Static review only: no code was executed, so Windows, native Linux and .NET Framework MSBuild behavior are not independently validated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants