Tag Archives: NTFS

Corrupt Windows 7 NTFS junction points

I encountered an unusual problem recently – all Windows 7 workstations which had been built with a Microsoft Select Agreement Volume License version of Windows 7 Professional RTM using an unattended install, not via sysprep, had some sort of damage to their legacy filesystem junction points. This had prevented the installer for Kaspersky EndPoint Protection 8 and its Network Agent version 9 from running, though earlier versions had been fine. The error took Kaspersky support a very long time to pin down (several months in fact, despite them having detailed MSI installer logs), and it eventually transpired that many of the links to maintain legacy OS compatibility like C:\Documents and Settings -> C:\Users, or C:\Users\All Users -> C:\ProgramData on these affected systems were resolving to some kind of temporary mounted WIM image path, within the folder C:\Users\ADMINI~1\AppData\Local\Temp\mnt\wim.

This folder no longer existed, and nor was there any phantom mounted WIM image, so any attempt to access the damaged links would fail (in Kaspersky’s case the issue was C:\ProgramData\Application Data). I still have no idea what may have caused this. More recently the unattended install I designed uses Windows 7 Enterprise SP1, with no changes to the core build scripting, and systems built from this do not exhibit this issue. This might suggest it was a problem with Windows itself, and if so then my script to fix the damage could be useful for others.

The repair script requires SetACL.exe which is an extremely versatile tool, but which is syntactically very difficult to use! I compared the ACLs on a clean system, noted the link type (they’re not all junctions, there is one symlink), and whether or not there were deny permissions which prevent recursion on links which resolve to their parent folder e.g. C:\ProgramData\Application Data -> C:\ProgramData. The links are deleted and recreated, but only on systems that are detected to need the fix (see the highlighted line for that logic). If you set line 6 to “set DEBUG=echo” you can test the output before actually invoking the repair commands.

@echo off

:: Windows 7 junction point/symlink fix script
:: patters 13/03/2012

set DEBUG=

dir /aL C:\ProgramData | find /I "C:\Users\ADMINI~1\AppData\Local\Temp\mnt\wim\" && (
  call :junction /J "C:\Documents and Settings" "C:\Users" deny
  call :junction /J "C:\ProgramData\Application Data" "C:\ProgramData" deny
  call :junction /J "C:\ProgramData\Desktop" "C:\Users\Public\Desktop" deny
  call :junction /J "C:\ProgramData\Documents" "C:\Users\Public\Documents" deny
  call :junction /J "C:\ProgramData\Favorites" "C:\Users\Public\Favorites" deny
  call :junction /J "C:\ProgramData\Start Menu" "C:\ProgramData\Microsoft\Windows\Start Menu" nodeny
  call :junction /J "C:\ProgramData\Templates" "C:\ProgramData\Microsoft\Windows\Templates" deny
  call :junction /D "C:\Users\All Users" "C:\ProgramData" deny
  call :junction /J "C:\Users\All Users\Application Data" "C:\ProgramData" deny
  call :junction /J "C:\Users\All Users\Desktop" "C:\Users\Public\Desktop" deny
  call :junction /J "C:\Users\All Users\Documents" "C:\Users\Public\Documents" deny
  call :junction /J "C:\Users\All Users\Favorites" "C:\Users\Public\Favorites" deny
  call :junction /J "C:\Users\All Users\Start Menu" "C:\ProgramData\Microsoft\Windows\Start Menu" nodeny
  call :junction /J "C:\Users\All Users\Templates" "C:\ProgramData\Microsoft\Windows\Templates" deny
  call :junction /J "C:\Users\Public\Documents\My Music" "C:\Users\Public\Music" deny
  call :junction /J "C:\Users\Public\Documents\My Pictures" "C:\Users\Public\Pictures" deny
  call :junction /J "C:\Users\Public\Documents\My Videos" "C:\Users\Public\Videos" deny
  call :junction /J "C:\Users\Default User" "C:\Users\Default" deny
  call :junction /J "C:\Users\Default\Application Data" "C:\Users\Default\AppData\Roaming" deny
  call :junction /J "C:\Users\Default\Cookies" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Cookies" deny
  call :junction /J "C:\Users\Default\Local Settings" "C:\Users\Default\AppData\Local" deny
  call :junction /J "C:\Users\Default\My Documents" "C:\Users\Default\Documents" deny
  call :junction /J "C:\Users\Default\NetHood" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Network Shortcuts" deny
  call :junction /J "C:\Users\Default\PrintHood" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Printer Shortcuts" deny
  call :junction /J "C:\Users\Default\Recent" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Recent" deny
  call :junction /J "C:\Users\Default\SendTo" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\SendTo" deny
  call :junction /J "C:\Users\Default\Start Menu" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu" deny
  call :junction /J "C:\Users\Default\Templates" "C:\Users\Default\AppData\Roaming\Microsoft\Windows\Templates" deny
  call :junction /J "C:\Users\Default\Documents\My Music" "C:\Users\Default\Music" deny
  call :junction /J "C:\Users\Default\Documents\My Pictures" "C:\Users\Default\Pictures" deny
  call :junction /J "C:\Users\Default\Documents\My Videos" "C:\Users\Default\Videos" deny
  call :junction /J "C:\Users\Default\AppData\Local\Application Data" "C:\Users\Default\AppData\Local" deny
  call :junction /J "C:\Users\Default\AppData\Local\Temporary Internet Files" "C:\Users\Default\AppData\Local\Microsoft\Windows\Temporary Internet Files" deny
) || echo Legacy filesystem junction points/symlinks are fine.

::odd permissions for this one, so I'm leaving it out
::call :junction /J "C:\Users\Default\AppData\Local\History" "C:\Users\Default\AppData\Local\Microsoft\Windows\History" deny

goto :eof

:: %1 = type (junction or directory symlink)
:: %2 = junction/symlink path
:: %3 = target path
:: %4 = set the deny permission or not

::delete old junction point
%DEBUG% rmdir "%~2"

::create new junction point
%DEBUG% mklink %1 "%~2" "%~3"

::set owner to SYSTEM
%DEBUG% setacl -on "%~2" -ot file -actn setowner -ownr "n:SYSTEM"

:: we need to stop inheritance of permissions before we make changes. This must be done with
:: a separate commandline entry owing to the order in which SetACL.exe processes its arguments.
%DEBUG% setacl -on "%~2" -ot file -actn setprot -op "dacl:p_c;sacl:p_c"

::clear ACL and set permissions
%DEBUG% setacl -on "%~2" -ot file -actn clear -clr "dacl,sacl" -actn ace -ace "n:Everyone;i:np;p:read_ex" -actn ace -ace "n:SYSTEM;i:np;p:full" -actn ace -ace "n:Administrators;i:np;p:full"

::add directory listing deny permission for recursive paths if needed
if "%4"=="deny" %DEBUG% setacl -on "%~2" -ot file -actn ace -ace "n:Everyone;s:n;m:deny;i:np;p:list_dir"