rust-msi icon indicating copy to clipboard operation
rust-msi copied to clipboard

Insert makes table unreadable

Open GrimOutlook opened this issue 4 months ago • 2 comments

Problem

When trying to insert information into a table (that already exists), the table can become unreadable by msiexec.exe. The table that is affected also causes attempting to open the table in orca.exe to result in an error with the message An invalid argument was encountered. (when the table would open properly before the insert) and both InstEd and MSI Viewer give generic errors stating that the MSI couldn't be opened.

Minimum Reproducible Example

This code is also available here and can be run by running cargo run --example mre -- Schema.msi

use std::{fs::File, io::Write, str::FromStr};

use clap::{App, Arg};
use msi::Value;

fn main() {
    let matches = App::new("mre")
        .arg(Arg::with_name("schema").required(true))
        .arg(Arg::with_name("output").required(true).default_value("out.msi"))
        .get_matches();
    let schema = matches.value_of("schema").unwrap();
    let output = matches.value_of("output").unwrap();

    let mut template = File::open(schema).unwrap();
    let mut file = File::options()
        .create(true)
        .read(true)
        .write(true)
        .truncate(true)
        .open(output)
        .expect("Failed to open broken.msi");
   
    // Copy the template data into our new MSI
    std::io::copy(&mut template, &mut file).unwrap();
    // Probably not needed but whatever
    file.flush().unwrap();

    let mut package = msi::Package::open(file).expect("Failed to create MSI");

    // Summary Information
    let sum = package.summary_info_mut();
    // Have to set the PackageCode or we get an even more generic error saying "The parameter is incorrect".
    // This is the only SummaryInfo field not set that appears to be necessary.
    sum.set_uuid(
        *uuid::fmt::Braced::from_str("{11111111-1111-1111-1111-111111111111}")
            .unwrap()
            .as_uuid(),
    ); // PID 9
    package.flush().unwrap();

    // Required Property Information
    package
        .insert_rows(msi::Insert::into("Property").rows(vec![
            vec![Value::from("ProductName"), Value::from("MyProduct")],
            vec![
                Value::from("ProductCode"),
                Value::from("{11111111-1111-1111-1111-111111111113}"),
            ],
            vec![Value::from("ProductVersion"), Value::from("0.0.0")],
            vec![Value::from("ProductLanguage"), Value::from("1033")],
            vec![Value::from("Manufacturer"), Value::from("MyManufacturer")],
        ]))
        .unwrap();

    package.flush().unwrap();
}

Troubleshooting

Install Logging

I tried running msiexec.exe with verbose logs msiexec -L*vx 'C:\TEMP\msi.log' 'C:\Users\User\Downloads\test.msi' and got the output below:

=== Verbose logging started: 12/1/2025  14:24:36  Build type: SHIP UNICODE 5.00.10011.00  Calling process: C:\Windows\System32\msiexec.exe ===
MSI (c) (D0:38) [14:24:36:333]: Font created.  Charset: Req=0, Ret=0, Font: Req=MS Shell Dlg, Ret=MS Shell Dlg

MSI (c) (D0:38) [14:24:36:334]: Font created.  Charset: Req=0, Ret=0, Font: Req=MS Shell Dlg, Ret=MS Shell Dlg

MSI (c) (D0:C0) [14:24:36:349]: Resetting cached policy values
MSI (c) (D0:C0) [14:24:36:349]: Machine policy value 'Debug' is 0
MSI (c) (D0:C0) [14:24:36:349]: ******* RunEngine:
           ******* Product: C:\Users\User\Downloads\test.msi
           ******* Action: 
           ******* CommandLine: **********
MSI (c) (D0:C0) [14:24:36:354]: Machine policy value 'DisableUserInstalls' is 0
MSI (c) (D0:C0) [14:24:36:402]: Access database with Impersonation
MSI (c) (D0:C0) [14:24:36:403]: SOFTWARE RESTRICTION POLICY: Verifying package --> 'C:\Users\User\Downloads\test.msi' against software restriction policy
MSI (c) (D0:C0) [14:24:36:403]: Note: 1: 2262 2: DigitalSignature 3: -2147287038 
MSI (c) (D0:C0) [14:24:36:403]: SOFTWARE RESTRICTION POLICY: C:\Users\User\Downloads\test.msi is not digitally signed
MSI (c) (D0:C0) [14:24:36:404]: SOFTWARE RESTRICTION POLICY: C:\Users\User\Downloads\test.msi is permitted to run at the 'unrestricted' authorization level.
MSI (c) (D0:C0) [14:24:36:427]: Cloaking enabled.
MSI (c) (D0:C0) [14:24:36:427]: Attempting to enable all disabled privileges before calling Install on Server
MSI (c) (D0:C0) [14:24:36:429]: End dialog not enabled
MSI (c) (D0:C0) [14:24:36:429]: Original package ==> C:\Users\User\Downloads\test.msi
MSI (c) (D0:C0) [14:24:36:429]: Package we're running from ==> C:\Users\User\Downloads\test.msi
MSI (c) (D0:C0) [14:24:36:430]: Note: 1: 2211 2:  3: Property 
MSI (c) (D0:C0) [14:24:36:430]: MSI_DBG: Provided descriptor less than minimum size
MSI (c) (D0:C0) [14:24:36:430]: APPCOMPAT: Compatibility mode property overrides found.
MSI (c) (D0:C0) [14:24:36:430]: APPCOMPAT: looking for appcompat database entry with ProductCode ''.
MSI (c) (D0:C0) [14:24:36:430]: APPCOMPAT: no matching ProductCode found in database.
MSI (c) (D0:C0) [14:24:36:433]: MSCOREE not loaded loading copy from system32
MSI (c) (D0:C0) [14:24:36:434]: Machine policy value 'TransformsSecure' is 0
MSI (c) (D0:C0) [14:24:36:434]: User policy value 'TransformsAtSource' is 0
MSI (c) (D0:C0) [14:24:36:435]: APPCOMPAT: looking for appcompat database entry with ProductCode ''.
MSI (c) (D0:C0) [14:24:36:435]: APPCOMPAT: no matching ProductCode found in database.
MSI (c) (D0:C0) [14:24:36:435]: Transforms are not secure.
MSI (c) (D0:C0) [14:24:36:435]: Note: 1: 2211 2:  3: Property 
MSI (c) (D0:C0) [14:24:36:435]: Note: 1: 2262 2: Control 3: -2147287038 
MSI (c) (D0:C0) [14:24:36:435]: PROPERTY CHANGE: Adding MsiLogFileLocation property. Its value is 'C:\TEMP\msi.log'.
MSI (c) (D0:C0) [14:24:36:435]: Command Line: CURRENTDIRECTORY=\\wsl.localhost\dev\home\dom\rust-msi\examples CLIENTUILEVEL=0 CLIENTPROCESSID=22736 
MSI (c) (D0:C0) [14:24:36:435]: PROPERTY CHANGE: Adding PackageCode property. Its value is '{11111111-1111-1111-1111-111111111111}'.
MSI (c) (D0:C0) [14:24:36:435]: Product Code passed to Engine.Initialize:           ''
MSI (c) (D0:C0) [14:24:36:435]: Product Code from property table before transforms: ''
MSI (c) (D0:C0) [14:24:36:435]: Product Code from property table after transforms:  ''
MSI (c) (D0:C0) [14:24:36:435]: Failing install, missing product code
This action is only valid for products that are currently installed.
C:\Users\User\Downloads\test.msi
MSI (c) (D0:C0) [14:24:36:435]: Note: 1: 1708 
MSI (c) (D0:C0) [14:24:36:435]: Product:  -- Installation failed.

MSI (c) (D0:C0) [14:24:36:436]: Windows Installer installed the product. Product Name: . Product Version: . Product Language: . Manufacturer: . Installation success or error status: 1605.

MSI (c) (D0:C0) [14:24:36:437]: MainEngineThread is returning 1605
=== Verbose logging stopped: 12/1/2025  14:24:36 ===

I did try running the same command in a regular PowerShell prompt rather than from WSL and got the same result.

MSIInfo.exe

Running the command MsiInfo.exe [MSI_PATH] /D for both Schema.msi and test.msi both show no StringPool errors, and adding \B to the command shows that the specified property information was appended to the end of the StringPool and I can't see any other oddities of note with a diff.

msitools

The information is readable both by the rust-msi crate and the msitools' msiinfo command. Running the command msiinfo export -s test.msi Property results in the expected output shown below:

Property        Value
s72     l0
Property        Property
Manufacturer    MyManufacturer
ProductCode     {11111111-1111-1111-1111-111111111113}
ProductLanguage 1033
ProductName     MyProduct
ProductVersion  0.0.0

Thoughts

I'm still unsure whether this is actually an issue in the rust-msi crate or the rust-cfb crate, though I'm leaning towards the former. I tries parsing through the data using a CFB parser I made in 010 Editor to see if I could find where the encoding is going wrong while comparing it to the unchanged Schema.msi, and while there aren't that many differences, nothing too obvious stuck out.

I figured I would ask here before I continued any further to see if you had any insight as to what might be happening. If not I'll have to continue trying to reverse engineer how the StringPool and table information is laid out in the CFB streams since I could only find the CFB and PropertySet documents and couldn't find any documentation that mentions StringPools at all on Microsoft's end.

GrimOutlook avatar Dec 01 '25 21:12 GrimOutlook

I made a small example in that same branch that dumps the all the streams in the MSI using the rust-cfb crate and compared the output between the default Schema.msi and the one with the Property table additions from rust-msi and it appears the _StringData, _StringPool and _SummaryInformation stream have all been changed (as well as a new _Property stream being created but that's expected) so the issue likely lies in one of those streams, though it could also have to do with a mismatch in parsing between how rust-cfb and windows application like msiexec and orca read the files.

GrimOutlook avatar Dec 02 '25 19:12 GrimOutlook

Thanks for the detailed write-up. I don't have any immediate guesses about what's going on, so your investigation is appreciated.

I'm not aware of any public documentation of StringPool either, unfortunately. I did my best to reverse-engineer it by looking at other open-source implementations and testing on a few real MSI files, but even so there were some bugs with it that were only fixed just recently. I wouldn't be surprised if there were other bugs lurking still.

If I recall correctly, I used msitools as a reference for some of that reverse engineering. So it seems quite possible that msitools and rust-msi might have some bug in common, which if so might explain why they can both read the modified file even though some other tools can't.

mdsteele avatar Dec 02 '25 23:12 mdsteele

Yeah, I was afraid of that 🥲

I'll keep investigating and if I manage to find a fix I'll submit a pull request.

GrimOutlook avatar Dec 04 '25 13:12 GrimOutlook

I'm starting to lean towards it being more likely caused by the rust-cfb crate. In that same branch I made an example called duplicate that tries to as closely duplicate an msitools generated MSI and the differences I've found so far is: The order that the table rows are stored seems to always be alphabetically for the primary key (or maybe the first column?) no matter the order I add them and when examining the resulting MSI's binary in a hex editor, the MSI produced by the duplicate example (e.g. made using rust-msi) has 10 extra sectors at the end of the file all zero'd out. I don't remember seeing in any of the docs where there would be a requirement for padding sectors at the end of a CFB but I could have just missed it.

GrimOutlook avatar Dec 10 '25 13:12 GrimOutlook