Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

env: properly handle nulls in REG_SZ strings #16190

Merged
merged 5 commits into from
Oct 19, 2023
Merged

Conversation

lhecker
Copy link
Member

@lhecker lhecker commented Oct 18, 2023

eb871bf fails to properly handle REG_SZ strings, which are documented as
being null-terminated and length restricted.
wcsnlen is the perfect fit for handling this situation as it returns
the position of the first \0, or the given length parameter.

As a drive by improvement, this also drops some redundant code:

  • to_environment_strings_w which is the same as to_string
  • Retrieving USERNAME/USERDOMAIN via LookupAccountSidW and
    COMPUTERNAME via GetComputerNameW is not necessary as the
    variables are "volatile" and I believe there's generally no
    expectation that they change unless you log in again.

Closes #16051

Validation Steps Performed

  • Run this in PowerShell to insert a env value with \0:
    $hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
      [Microsoft.Win32.RegistryHive]::LocalMachine,
      0
    )
    $key = $hklm.OpenSubKey(
      'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
      $true
    )
    $key.SetValue('test', "foo`0bar")
  • All EnvTests still pass ✅
  • (Don't forget to remove the above value again!)

@microsoft-github-policy-service microsoft-github-policy-service bot added Issue-Bug It either shouldn't be doing this or needs an investigation. Priority-0 Bugs that we consider release-blocking/recall-class (P0) Product-Terminal The new Windows Terminal. labels Oct 18, 2023
Comment on lines -260 to -261
strip_trailing_null(accountName);
strip_trailing_null(userDomain);
Copy link
Member Author

@lhecker lhecker Oct 18, 2023

Choose a reason for hiding this comment

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

My intention was to just remove these two lines here, but then I realized that it doesn't actually seem to do anything, because HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment contains a USERNAME value (with the value-value of SYSTEM) which immediately overwrites this. Reading more into volatile environment variables, I've realized that this code can be replaced with one that simply copies them from the stale (process) environment block, because they are immutable across a login.

Copy link
Member

Choose a reason for hiding this comment

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

clever. this means it is no longer a clone of RegenerateUserEnvironment, but that's OK.

Comment on lines 433 to 439
// The difference is that we don't
// * handle autoexec.bat (intentionally unsupported).
// * call LookupAccountSidW to get the USERNAME/USERDOMAIN variables
// These are available as standard environment variables and they're "volatile"
// (= computed dynamically at each login and then constant until logout).
// We don't expect our process to impersonate another user so we can simplify this.
// * call GetComputerNameW to get the COMPUTERNAME variable, for the same reason.

Choose a reason for hiding this comment

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

This description of "volatile" seems a bit off, if it covers CLIENTNAME, which I'd expect to change if the user reconnects to the same Remote Desktop session from a different client computer.

I have assumed that the "HKEY_CURRENT_USER\Volatile Environment" key is created with RegCreateKeyExW REG_OPTION_VOLATILE or the NT API equivalent, so that it is never saved to the user profile, but I haven't verified whether this is actually true.

Copy link
Member Author

@lhecker lhecker Oct 18, 2023

Choose a reason for hiding this comment

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

That's basically what I meant, but I tried writing it in a way that doesn't assume the reader knows what any of this means.

I think CLIENTNAME is a bit special in this regard, because it's not even stored in the Volatile Environment key directly, but rather in it's child key where the key name is your session ID, starting from 1. To me it seems like it is still a constant across an entire session, because every time you login or connect remotely the session ID changes and a new child key is created.

How would you express the comment?

Copy link

@KalleOlaviNiemitalo KalleOlaviNiemitalo Oct 18, 2023

Choose a reason for hiding this comment

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

I tried it just now: disconnected from the session without logging out, and reconnected to the same session from a different client. In this case, "Volatile Environment" still had a subkey with the same session ID as its name, but the data of the "CLIENTNAME" value was updated. And Registry Editor was still running so it had to be the same session.

How would you express the comment?

I would not try to explain what "volatile" means.

            // * call LookupAccountSidW to get the USERNAME/USERDOMAIN variables
            //   Windows sets these as values of the "Volatile Environment" key at each login.
            //   We don't expect our process to impersonate another user so we can simplify this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for testing this! I guess in hindsight this is kind of obvious, since RDP allows reconnects... I've never really thought about how that would work. I hope it properly emits a WM_SETTINGCHANGE event, whenever this happens, so that it still works if we don't regenerate the environment every time.

Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

Sure let's give it a shot. I think I follow

@DHowett
Copy link
Member

DHowett commented Oct 18, 2023

This is a very long change for what it purports to be. Can you point out the specific part that is the fix, and what is a drive-by?

Should we have a smaller, targeted version for 1.18 and 1.19?

@lhecker
Copy link
Member Author

lhecker commented Oct 18, 2023

This is a very long change for what it purports to be. Can you point out the specific part that is the fix, and what is a drive-by?

The only drive-by is the removal of get_computer_name(), all the other changes are necessary. I removed get_computer_name() because its functionality was very similar to how get_user_name_and_domain() worked. Can you spot what get_user_name_and_domain() did wrong? 😄

Solution: ZFQA qbrfa'g ernyyl zragvba vg fcrpvsvpnyyl, ohg YbbxhcNppbhagFvqJ gerngf gur nppbhagAnzrFvmr naq hfreQbznvaFvmr cnenzrgref nf va-bhg barf rira vs gur gjb jpune cbvagref nera'g ahyycge. Gung vf, nsgre gur frpbaq pnyy gb YbbxhcNppbhagFvqJ, jr unq gb pnyy erfvmr() ba gur gjb jfgevat'f gb cebcreyl gevz bs genvyvat ahyyf naq fgevc_genvyvat_ahyy bayl fgevcf 1 genvyvat ahyy.


til::env environment;
auto zeroEnvMap = wil::scope_exit([&]() noexcept {
environment.clear();
Copy link
Member

Choose a reason for hiding this comment

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

Why did we used to do this, anyway?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure! Maybe security? But that wouldn't have worked, since the creation of the many std::wstring instances during regenerate() would leave tons of "unsafely" freed memory locations around anyways.

Comment on lines -260 to -261
strip_trailing_null(accountName);
strip_trailing_null(userDomain);
Copy link
Member

Choose a reason for hiding this comment

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

clever. this means it is no longer a clone of RegenerateUserEnvironment, but that's OK.

data = expand_environment_strings(data.data());
}

// Because Registry data may or may not be null terminated... check if we've managed
// to store an extra null in the wstring by telling it to create itself from pointer and size.
Copy link
Member

Choose a reason for hiding this comment

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

this seems like a willy way to handle that!

auto expectedIt = expected.as_map().begin();
auto actualIt = actual.as_map().begin();

for (; expectedIt != expectedEnd; ++expectedIt, ++actualIt)
Copy link
Member

Choose a reason for hiding this comment

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

Careful! The original check is order-independent. This check is not. It was explicitly written this way because the test was failing.

It's just test code, it's allowed to be inefficient.

Copy link
Member Author

Choose a reason for hiding this comment

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

Did it use an unordered_map before? Right now it uses a map and the iteration order of the two instances should match 1:1.

I made this change because I found the old code very confusing while testing my changes. This doesn't work:

const auto actualValue = actual.as_map()[expectedKey];
VERIFY_IS_TRUE(actual.as_map().find(expectedKey) != actual.as_map().end());

The first line ensures that the second line always succeeds. This code is also somewhat confusing to me:

VERIFY_ARE_EQUAL(expectedValue, actual.as_map()[expectedKey]);

because it doesn't properly work in case either of the two values is an empty string.

In short, my intention was to remove the use of operator[] to increase my confidence that I didn't break anything.

Copy link
Member

Choose a reason for hiding this comment

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

AH. Thanks.

@microsoft-github-policy-service microsoft-github-policy-service bot added Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something and removed Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something labels Oct 18, 2023
@DHowett DHowett changed the title Properly handle nulls in REG_SZ strings env: properly handle nulls in REG_SZ strings Oct 19, 2023
@DHowett DHowett merged commit 64b5b28 into main Oct 19, 2023
17 checks passed
@DHowett DHowett deleted the dev/lhecker/16051-env-nulls branch October 19, 2023 00:46
DHowett pushed a commit that referenced this pull request Oct 26, 2023
eb871bf fails to properly handle REG_SZ strings, which are documented as
being null-terminated _and_ length restricted.
`wcsnlen` is the perfect fit for handling this situation as it returns
the position of the first \0, or the given length parameter.

As a drive by improvement, this also drops some redundant code:
* `to_environment_strings_w` which is the same as `to_string`
* Retrieving `USERNAME`/`USERDOMAIN` via `LookupAccountSidW` and
  `COMPUTERNAME` via `GetComputerNameW` is not necessary as the
  variables are "volatile" and I believe there's generally no
  expectation that they change unless you log in again.

Closes #16051

## Validation Steps Performed
* Run this in PowerShell to insert a env value with \0:
  ```pwsh
  $hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
    [Microsoft.Win32.RegistryHive]::LocalMachine,
    0
  )
  $key = $hklm.OpenSubKey(
    'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
    $true
  )
  $key.SetValue('test', "foo`0bar")
  ```
* All `EnvTests` still pass ✅
* (Don't forget to remove the above value again!)

(cherry picked from commit 64b5b28)
Service-Card-Id: 90879164
Service-Version: 1.19
DHowett pushed a commit that referenced this pull request Nov 7, 2023
eb871bf fails to properly handle REG_SZ strings, which are documented as
being null-terminated _and_ length restricted.
`wcsnlen` is the perfect fit for handling this situation as it returns
the position of the first \0, or the given length parameter.

As a drive by improvement, this also drops some redundant code:
* `to_environment_strings_w` which is the same as `to_string`
* Retrieving `USERNAME`/`USERDOMAIN` via `LookupAccountSidW` and
  `COMPUTERNAME` via `GetComputerNameW` is not necessary as the
  variables are "volatile" and I believe there's generally no
  expectation that they change unless you log in again.

Closes #16051

## Validation Steps Performed
* Run this in PowerShell to insert a env value with \0:
  ```pwsh
  $hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
    [Microsoft.Win32.RegistryHive]::LocalMachine,
    0
  )
  $key = $hklm.OpenSubKey(
    'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
    $true
  )
  $key.SetValue('test', "foo`0bar")
  ```
* All `EnvTests` still pass ✅
* (Don't forget to remove the above value again!)

(cherry picked from commit 64b5b28)
Service-Card-Id: 90879163
Service-Version: 1.18
DHowett pushed a commit that referenced this pull request Nov 7, 2023
eb871bf fails to properly handle REG_SZ strings, which are documented as
being null-terminated _and_ length restricted.
`wcsnlen` is the perfect fit for handling this situation as it returns
the position of the first \0, or the given length parameter.

As a drive by improvement, this also drops some redundant code:
* `to_environment_strings_w` which is the same as `to_string`
* Retrieving `USERNAME`/`USERDOMAIN` via `LookupAccountSidW` and
  `COMPUTERNAME` via `GetComputerNameW` is not necessary as the
  variables are "volatile" and I believe there's generally no
  expectation that they change unless you log in again.

Closes #16051

## Validation Steps Performed
* Run this in PowerShell to insert a env value with \0:
  ```pwsh
  $hklm = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
    [Microsoft.Win32.RegistryHive]::LocalMachine,
    0
  )
  $key = $hklm.OpenSubKey(
    'SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
    $true
  )
  $key.SetValue('test', "foo`0bar")
  ```
* All `EnvTests` still pass ✅
* (Don't forget to remove the above value again!)

(cherry picked from commit 64b5b28)
Service-Card-Id: 90879163
Service-Version: 1.18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Bug It either shouldn't be doing this or needs an investigation. Priority-0 Bugs that we consider release-blocking/recall-class (P0) Product-Terminal The new Windows Terminal.
Projects
4 participants