Disabling Auto-Updates in the reMarkable Companion App: WinSparkle DLL Stub Method

When deploying the reMarkable Companion App in an enterprise environment, the built-in auto-updater creates a recurring problem. The application uses the WinSparkle framework to check for updates on every launch. When a new version is found, it shows a dialog and attempts to download and install the update — which requires administrative privileges that standard users do not have.

 


 

[Picture 1] The update dialog appears on every launch when a newer version is available.

 

The “Skip this version” button does not work. The update downloads regardless, and when the installer attempts to run it fails for non-admin users, showing an error message. The update dialog then reappears on the next launch.

 


 

[Picture 2] The updater downloads the installer in the background before failing due to missing admin rights.

 

Method Overview

Method

Status

Description

WinSparkle DLL stub

Current

Replaces WinSparkle.dll with a stub to fully disable update checks

Update Dialog Closer

Legacy

PowerShell script that closes update dialogs reactively

 Previous Approach (Legacy)

An earlier workaround using a PowerShell script to reactively close the update dialog is documented in this blog post and is kept in the repository under legacy/update-dialog-closer/ for reference. It works, but the updater itself remains active and the dialog can reappear. The stub method below is cleaner and fully eliminates the problem.

 

Recommended Method: WinSparkle DLL Stub

The reMarkable application loads WinSparkle.dll to handle its update logic. By replacing this DLL with a stub that exports all the expected functions but does nothing, the update mechanism is disabled at the source. The application loads normally, the update check is never initiated, and no dialog ever appears.

The exported functions were identified by inspecting the original DLL using CFF Explorer:

 


 

[Picture 3] CFF Explorer showing the Export Directory of the original WinSparkle.dll. All listed functions are implemented as no-ops in the stub.

 

The stub DLL exports the complete WinSparkle API so the application does not crash on load. All functions are present but perform no actions.

 

Effect

      Update checks are disabled entirely

      No update dialogs are ever shown to the user

      The application continues functioning normally

 

Applicability

This method is not specific to reMarkable. Any Windows application that uses WinSparkle for updates and loads WinSparkle.dll dynamically can have its updater disabled using the same stub, as long as the application does not verify the DLL signature. The exported function names can be verified with CFF Explorer as shown above.

 

Quick Usage

Install the reMarkable Companion App normally, then replace:

 

C:\Program Files\reMarkable\WinSparkle.dll

 

...with the stub DLL from bin/WinSparkle.dll in the repository.

 

Build Script: Build-reMarkable-NoAutoUpdate.ps1

For production use, compiling the stub yourself is recommended over using the precompiled binary. The included PowerShell build script automates the full process:

 

1.    Resolves the latest reMarkable installer URL from winget-pkgs (with fallbacks)

2.    Resolves the latest MinGW compiler build from GitHub releases

3.    Downloads 7-Zip if needed, extracts MinGW

4.    Compiles the stub DLL from the included C source file

5.    Installs reMarkable silently

6.    Backs up the original WinSparkle.dll and replaces it with the stub

 

Run as Administrator in a test VM or packaging environment:

 

.\Build-reMarkable-NoAutoUpdate.ps1

 

Repeated runs are handled gracefully — downloads are skipped if the file already matches the expected URL and size.

 

Enterprise Deployment

7.    Install the reMarkable Companion App silently (via PSADT or similar)

8.    Replace WinSparkle.dll with the stub version

9.    Deliver subsequent updates through your software distribution platform

 

Integrates cleanly with Omnissa Workspace ONE UEM, App Volumes, PSADT, and MSI repackaging pipelines.

 

Repository

https://github.com/altiris4ever/Remarkable-Update-Closer/

 

References

      WinSparkle project

      WinSparkle registry settings

      niXman MinGW builds

      reMarkable Companion App winget manifest

      Previous blog post: Update Dialog Closer (legacy method)

      Legacy scripts in repository

 



How Does App Volumes Direct Mode Work via MSI

In connection with another blog post I’m writing in a series about App Volumes Direct Mode, I wanted to find out exactly how the MSI we create during capture works and what it contains.

In this post I have chosen to use a couple of App Volumes MSI-wrapped packages I made myself using their capture tool.

After capture, you get a wrapped MSI that contains the App Volumes package (VHD + JSON), either embedded directly or stored inside an internal CAB stream. This also explains why you encounter a CAB file when the package exceeds 2GB.

To get the most out of what I'm writing, you should have some familiarity with how MSI files work. In short, an MSI is a database with tables and logic that specifies where and how things should end up on the system. Custom Actions are the logic that performs custom operations within an MSI.

This may not be rocket science, but I believe you need the right tools and a decent understanding of MSI to follow the flow. This was previously a gap in my knowledge, but after attending a packaging course with the MasterPackager team, I have filled in those gaps.

Tools needed to extract all files from the MSI

1.      MSI editor - I recommend MasterPackager, but there are other alternatives (worse imo)

2.      LessMsi - "This is a utility with a graphical user interface and a command line interface that can be used to view and extract the contents of an MSI file". Had to use the command line to extract content from the Binary table. Covered later in this document: lessmsi x <msiFileName> [<outputDir>]

3.      Dependencies - a rewrite of the legacy Dependency Walker tool shipped with Windows SDKs, but whose development stopped around 2006. Dependencies can help Windows developers troubleshoot DLL load dependency issues.

4.      ILSpy - the open-source .NET assembly browser and decompiler.

5.      7-Zip - because it can often open and extract parts of an MSI file.

Easiest way to extract VHD and JSON files

If you only need to get the raw files from the MSI, you only need 7-Zip.

If the package is under 2GB, these files are located directly in the root of the MSI and can easily be extracted with 7-Zip:









[ Picture 1 ]

For packages larger than 2GB, extraction is a bit trickier. Here is an example of such a package I created.
































[ Picture 2 ]

The .VHD and .JSON files in this package are stored inside an internal .CAB file, which in this case has been given Chinese characters as the filename. Since the file is so large, it is easy to identify the .CAB file by its size. I then extract this file to disk using 7-Zip.










[ Picture 3 ]

Then I open "{㭄㫂㩆㢂-㭉㥊-㮄㢎-㥉㠉-㢊㪈㦊㤎㥁㥊}䆾䅤" again in 7-Zip, where I find the files and extract them to disk.

Analysing the MSI file

When running msiexec /i "name.msi" /qn and msiexec /x "name.msi" /qn, it is evident that the MSI runs App Volumes-specific commands against the agent, which publishes and unpublishes the application via the App Volumes Agent.

To get a clearer picture of what is actually happening, I opened the MSI in Master Packager:











[ Picture 4 ]

We can see it contains:

         0 files in the File table

         1 registry value

         1 Feature

         No shortcuts

         UpgradeCode defined

         24 properties

         5 Custom Actions

This indicates that this is not a classic wrapper MSI that typically wraps an EXE installer inside MSI format for standardized deployment. The App Volumes package is somewhat different - there is no EXE here, but rather a VHD and some agent calls. It is therefore an unusual variant of the wrapper concept, but the principle is the same: the actual payload is handled outside Windows Installer's File table.

Looking at the Feature in the MSI











[ Picture 5 ]

Here I can see the registry area where App Volumes stores the product code when the package is installed locally:

Software\Omnissa\AppVolumes\OfflineStore\MsiPackages\ [ProductCode]

I already knew this from working extensively with App Volumes Direct Mode.

We can also see that it links to the registry entry. MSI features can be conditionally installed or excluded using ADDLOCAL / REMOVE or feature conditions, so I am unsure whether they have used that approach here. Perhaps it is easier to script it this way since it is a Property that can be fetched dynamically.











[ Picture 6 ]

Here we can clearly see that "installappPackage", "uninstallappPackage", "upgradeappPackage" and "getoldinstallerproperties" are run via custom action.












[ Picture 7 ]

The App Volumes-related custom actions are scheduled very early in the InstallExecuteSequence. In this package, GetOldInstallerProperties, UpgradeAppPackage, and UninstallAppPackage run before InstallInitialize (around sequence 1401–1404), while InstallAppPackage runs immediately after InstallInitialize at sequence 1501.

InstallInitialize (seq 1500) marks the beginning of the transactional execution phase. It is worth noting that actions scheduled before InstallInitialize in the Execute sequence still run as immediate actions and are typically used for detection, property setup, and general flow control before the transactional phase begins.

Standard file-copying actions such as InstallFiles occur much later in the sequence (typically around sequence ~4000). However, this MSI does not contain any payload in the File table. Instead, the early custom actions orchestrate the deployment by communicating with the App Volumes Agent, which performs the actual attach and mount of the VHD package.













[Picture 8 ]

This is not a classic "file-copying" MSI.

These are the relevant properties:

         DiskFileName = ILSpy_Installer_9.1.0.7988-x64.vhd

         AppPackageId

         AppVolumeId

         MetadataFileName

         DELIVERYTYPE = OnDemand

What does DELIVERYTYPE = OnDemand mean?

As previously explained, this is a "wrapper" MSI that installs the App Volumes VHD package, not a traditional MSI with a File table payload. The MSI functions as a delivery mechanism with Custom Actions that call the App Volumes agent - it does not contain any application files directly.

DELIVERYTYPE is a public property (uppercase = public), which means it can be controlled from the command line during installation. It is also listed in SecureCustomProperties, which ensures the property is propagated from the client process to the elevated Windows Installer service during execution. This is required whenever an installation runs elevated — the Windows Installer client process and the elevated system process are separate, and public properties do not automatically cross that boundary.

In practice, this means you can control the attach mode (Classic vs OnDemand) directly from the command line. If no value is supplied, the package installs using the default value OnDemand, based on my testing and vendor documentation:

msiexec /i "package.msi" /qn DELIVERYTYPE=Classic











[ Picture 9 ]

Here we can see that InstallAppPackage runs as Immediate (right away).

To understand what is actually being executed, look at the following MSI tables:











[ Picture 10 ]

1.      CustomAction table: find the row where Source = CustomActionBinary (or Target points to the entry point).

2.      The Type column: determines whether it is a DLL CA, Immediate/Deferred, etc.

3.      InstallExecuteSequence: when it is triggered.

Extracting content from the Binary table in the MSI file

Custom Action binary files are stored in the MSI's Binary table. That is why we need to dig into that table to retrieve CustomActionBinary.dll - it is not stored as a regular file in the package, but as a raw binary stream in the database.

There are certainly many tools and methods available, but in my example I used the previously mentioned "Lessmsi-v2.12.5":

$msi = ".\lessmsi-v2.12.5\ILSpy_Installer_9.1.0.7988-x64.msi"

$outputDir = "C:\temp\extracted\"

New-Item -ItemType Directory -Force -Path $outputDir | Out-Null

$installer = New-Object -ComObject WindowsInstaller.Installer

$db = $installer.OpenDatabase($msi, 0)

$db.Export("Binary", $outputDir, "Binary.idt")

The files are written to the folder C:\temp\extracted

I get two .IBD files. This is not a real file format - the extension was simply invented when I dumped the files. I could have called them anything at all; the extension has nothing to do with the file contents. That is exactly why we need to check the magic bytes to find out what we actually have.

To determine the actual file type, I wrote the following small PowerShell script:

$path = "C:\Temp\extracted\1\Binary\CustomActionBinary.ibd"

$bytes = [System.IO.File]::ReadAllBytes($path)

"{0:X2} {1:X2}" -f $bytes[0], $bytes[1]

The result was: 4D 5A

If you get 4D 5A → rename to .dll first, because Custom Actions are most often .DLL files.

Inspecting the DLL with Dependencies

In the "Dependencies" program I can see what CustomActionBinary.dll does:












[ Picture 11 ]

Under Functions we can see all the functions listed - this is where the "installer magic" happens.

Since a .DLL, like .EXE files, is compiled code, we would need a decompiler to see further details. However, I stop here since I already know which commands are executed from my own experience with App Volumes command lines.

In essence, the MSI is not an installer in the traditional sense — it is purely a deployment trigger. The actual work follows a simple chain:

MSI → Custom Action DLL → App Volumes Agent → mount VHD → app available

The MSI just pulls the lever. The App Volumes Agent does the rest.