Skip to content

Make sure that the signing of .js and .vbs files with the AzureSignToolSigner uses STA context.#881

Open
dlemstra wants to merge 3 commits intodotnet:mainfrom
dlemstra:js-vbs-in-sta-thread
Open

Make sure that the signing of .js and .vbs files with the AzureSignToolSigner uses STA context.#881
dlemstra wants to merge 3 commits intodotnet:mainfrom
dlemstra:js-vbs-in-sta-thread

Conversation

@dlemstra
Copy link
Copy Markdown
Collaborator

Fixes #880

@dlemstra dlemstra requested a review from a team as a code owner May 23, 2025 16:01
@dlemstra dlemstra force-pushed the js-vbs-in-sta-thread branch from a71f3e7 to 4504884 Compare May 24, 2025 07:45

// loop through all of the files here, looking for appx/eappx
// mark each as being signed and strip appx
await Parallel.ForEachAsync(files, async (file, state) =>
Copy link
Copy Markdown
Collaborator

@dtivel dtivel Jul 27, 2025

Choose a reason for hiding this comment

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

I tested the fix by signing a single .js file. The STA signing above succeeded, but this failed because it tries to sign all files on an MTA thread, which was the original problem.

This statement should sign all files that don't have a file extension in StaThreadExtensions.

We should have test coverage.

{
internal sealed class AzureSignToolSigner : IAzureSignToolDataFormatSigner
{
private static readonly string[] StaThreadExtensions = [".js", ".vbs"];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Comparisons should be case-insensitive.

Suggested change
private static readonly string[] StaThreadExtensions = [".js", ".vbs"];
private static readonly HashSet<string> StaThreadExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".js",
".vbs"
};

return false;
}

private static Task<bool> RunOnStaThread(Func<Task> func)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
private static Task<bool> RunOnStaThread(Func<Task> func)
private static Task<bool> RunOnStaThreadAsync(Func<Task> func)

FileInfo[] staThreadFiles = [.. files.Where(file => StaThreadExtensions.Contains(file.Extension))];
foreach (FileInfo file in staThreadFiles)
{
await RunOnStaThread(() => SignAsync(signer, file, options));
Copy link
Copy Markdown
Collaborator

@dtivel dtivel Jul 27, 2025

Choose a reason for hiding this comment

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

This only works because the actual signing operation happens to execute synchronously before any await calls. SignAsync(...) does contain an await Thread.Delay(...), but it only executes on retry. With the current implementation, only the first try has any chance of success while subsequent retries are guaranteed to fail because execution will resume on the thread pool (MTA).

The proper fix involves setting a synchronization context on the STA thread so that all continuations resume on the STA thread.

https://devblogs.microsoft.com/dotnet/await-synchronizationcontext-and-console-apps/
https://devblogs.microsoft.com/dotnet/await-synchronizationcontext-and-console-apps-part-2/

dtivel added a commit that referenced this pull request Mar 27, 2026
Address review concerns on PR #881:

- Run only the synchronous SignFile COM call on a dedicated STA thread
  (via RunOnStaThread<T>), not the entire async SignAsync. This avoids
  the need for a custom SynchronizationContext and ensures retries also
  run on STA threads.

- Partition STA-required files (.js, .vbs) from the Parallel.ForEachAsync
  loop. STA files are signed sequentially to avoid blocking ThreadPool
  threads via thread.Join(). Non-STA files continue to be signed in
  parallel as before.

- Use HashSet<string> with StringComparer.OrdinalIgnoreCase for
  case-insensitive extension matching.

- Unseal AzureSignToolSigner and extract SignFileCore as internal virtual
  to enable test substitution for apartment state verification.

- Add tests proving .vbs/.js files are signed on STA threads, .dll files
  on MTA threads, and mixed batches are correctly partitioned.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dtivel
Copy link
Copy Markdown
Collaborator

dtivel commented Mar 27, 2026

@dlemstra, I addressed the feedback in this commit.

Do you want to apply this commit and rebase on latest main?

dlemstra and others added 3 commits April 6, 2026 13:02
Address review concerns on PR dotnet#881:

- Run only the synchronous SignFile COM call on a dedicated STA thread
  (via RunOnStaThread<T>), not the entire async SignAsync. This avoids
  the need for a custom SynchronizationContext and ensures retries also
  run on STA threads.

- Partition STA-required files (.js, .vbs) from the Parallel.ForEachAsync
  loop. STA files are signed sequentially to avoid blocking ThreadPool
  threads via thread.Join(). Non-STA files continue to be signed in
  parallel as before.

- Use HashSet<string> with StringComparer.OrdinalIgnoreCase for
  case-insensitive extension matching.

- Unseal AzureSignToolSigner and extract SignFileCore as internal virtual
  to enable test substitution for apartment state verification.

- Add tests proving .vbs/.js files are signed on STA threads, .dll files
  on MTA threads, and mixed batches are correctly partitioned.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dlemstra dlemstra force-pushed the js-vbs-in-sta-thread branch from 4504884 to 4dec1fa Compare April 6, 2026 11:02
@dlemstra
Copy link
Copy Markdown
Collaborator Author

dlemstra commented Apr 6, 2026

Thanks for helping out with this PR @dtivel! I have updated the pull request.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

VBS files inside nupkg fail to sign

2 participants