Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 157 additions & 2 deletions GVFS/GVFS.Common/Git/LibGit2Repo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class LibGit2Repo : IDisposable
{
private bool disposedValue = false;

public delegate void MultiVarConfigCallback(string value);

public LibGit2Repo(ITracer tracer, string repoPath)
{
this.Tracer = tracer;
Expand All @@ -23,8 +25,12 @@ public LibGit2Repo(ITracer tracer, string repoPath)
string message = "Couldn't open repo at " + repoPath + ": " + reason;
tracer.RelatedWarning(message);

Native.Shutdown();
throw new InvalidDataException(message);
if (!reason.EndsWith(" is not owned by current user")
|| !CheckSafeDirectoryConfigForCaseSensitivityIssue(tracer, repoPath, out repoHandle))
{
Native.Shutdown();
throw new InvalidDataException(message);
}
}

this.RepoHandle = repoHandle;
Expand Down Expand Up @@ -246,7 +252,64 @@ public virtual string GetConfigString(string name)
{
Native.Config.Free(configHandle);
}
}

public void ForEachMultiVarConfig(string key, MultiVarConfigCallback callback)
{
if (Native.Config.GetConfig(out IntPtr configHandle, this.RepoHandle) != Native.ResultCode.Success)
{
throw new LibGit2Exception($"Failed to get config handle: {Native.GetLastError()}");
}
try
{
ForEachMultiVarConfig(configHandle, key, callback);
}
finally
{
Native.Config.Free(configHandle);
}
}

public static void ForEachMultiVarConfigInGlobalAndSystemConfig(string key, MultiVarConfigCallback callback)
{
if (Native.Config.GetGlobalAndSystemConfig(out IntPtr configHandle) != Native.ResultCode.Success)
{
throw new LibGit2Exception($"Failed to get global and system config handle: {Native.GetLastError()}");
}
try
{
ForEachMultiVarConfig(configHandle, key, callback);
}
finally
{
Native.Config.Free(configHandle);
}
}

private static void ForEachMultiVarConfig(IntPtr configHandle, string key, MultiVarConfigCallback callback)
{
Native.Config.GitConfigMultivarCallback nativeCallback = (entryPtr, payload) =>
{
try
{
var entry = Marshal.PtrToStructure<Native.Config.GitConfigEntry>(entryPtr);
callback(entry.GetValue());
}
catch (Exception)
{
return Native.ResultCode.Failure;
}
return 0;
};
if (Native.Config.GetMultivarForeach(
configHandle,
key,
regex:"",
nativeCallback,
IntPtr.Zero) != Native.ResultCode.Success)
{
throw new LibGit2Exception($"Failed to get multivar config for '{key}': {Native.GetLastError()}");
}
}

/// <summary>
Expand Down Expand Up @@ -302,11 +365,48 @@ protected virtual void Dispose(bool disposing)
}
}

private bool CheckSafeDirectoryConfigForCaseSensitivityIssue(ITracer tracer, string repoPath, out IntPtr repoHandle)
{
/* Libgit2 has a bug where it is case sensitive for safe.directory (especially the
* drive letter) when git.exe isn't. Until a fix can be made and propagated, work
* around it by matching the repo path we request to the configured safe directory.
*
* See https://github.com/libgit2/libgit2/issues/7037
*/
repoHandle = IntPtr.Zero;

string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
{
return path;
}

string normalized = path.Replace('\\', '/').ToUpperInvariant();
return normalized.TrimEnd('/');
}

string normalizedRequestedPath = NormalizePath(repoPath);

string configuredMatchingDirectory = null;
ForEachMultiVarConfigInGlobalAndSystemConfig("safe.directory", (string value) =>
{
string normalizedConfiguredPath = NormalizePath(value);
if (normalizedConfiguredPath == normalizedRequestedPath)
{
configuredMatchingDirectory = value;
}
});

return configuredMatchingDirectory != null && Native.Repo.Open(out repoHandle, configuredMatchingDirectory) == Native.ResultCode.Success;
}

public static class Native
{
public enum ResultCode : int
{
Success = 0,
Failure = -1,
NotFound = -3,
}

Expand Down Expand Up @@ -370,9 +470,64 @@ public static class Config
[DllImport(Git2NativeLibName, EntryPoint = "git_repository_config")]
public static extern ResultCode GetConfig(out IntPtr configHandle, IntPtr repoHandle);

[DllImport(Git2NativeLibName, EntryPoint = "git_config_open_default")]
public static extern ResultCode GetGlobalAndSystemConfig(out IntPtr configHandle);

[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_string")]
public static extern ResultCode GetString(out string value, IntPtr configHandle, string name);

[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_multivar_foreach")]
public static extern ResultCode GetMultivarForeach(
IntPtr configHandle,
string name,
string regex,
GitConfigMultivarCallback callback,
IntPtr payload);

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate ResultCode GitConfigMultivarCallback(
IntPtr entryPtr,
IntPtr payload);

[StructLayout(LayoutKind.Sequential)]
public struct GitConfigEntry
{
public IntPtr Name;
public IntPtr Value;
public IntPtr BackendType;
public IntPtr OriginPath;
public uint IncludeDepth;
public int Level;

public string GetValue()
{
return Value != IntPtr.Zero ? MarshalUtf8String(Value) : null;
}

public string GetName()
{
return Name != IntPtr.Zero ? MarshalUtf8String(Name) : null;
}

private static string MarshalUtf8String(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
{
return null;
}

int length = 0;
while (Marshal.ReadByte(ptr, length) != 0)
{
length++;
}

byte[] buffer = new byte[length];
Marshal.Copy(ptr, buffer, 0, length);
return System.Text.Encoding.UTF8.GetString(buffer);
}
}

[DllImport(Git2NativeLibName, EntryPoint = "git_config_get_bool")]
public static extern ResultCode GetBool(out bool value, IntPtr configHandle, string name);

Expand Down
1 change: 1 addition & 0 deletions GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ProjectReference Include="..\GVFS.NativeTests\GVFS.NativeTests.vcxproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
<ProjectReference Include="..\GVFS.UnitTests\GVFS.UnitTests.csproj" />
<None Include="$(RepoOutPath)GVFS.NativeTests\bin\x64\$(Configuration)\GVFS.NativeTests.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
7 changes: 7 additions & 0 deletions GVFS/GVFS.FunctionalTests/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using GVFS.FunctionalTests.Properties;
using GVFS.FunctionalTests.Tools;
using GVFS.PlatformLoader;
using GVFS.Tests;
using System;
using System.Collections.Generic;
Expand All @@ -13,6 +14,7 @@ public class Program
public static void Main(string[] args)
{
Properties.Settings.Default.Initialize();
GVFSPlatformLoader.Initialize();
Console.WriteLine("Settings.Default.CurrentDirectory: {0}", Settings.Default.CurrentDirectory);
Console.WriteLine("Settings.Default.PathToGit: {0}", Settings.Default.PathToGit);
Console.WriteLine("Settings.Default.PathToGVFS: {0}", Settings.Default.PathToGVFS);
Expand All @@ -21,6 +23,11 @@ public static void Main(string[] args)
NUnitRunner runner = new NUnitRunner(args);
runner.AddGlobalSetupIfNeeded("GVFS.FunctionalTests.GlobalSetup");

if (runner.HasCustomArg("--debug"))
{
Debugger.Launch();
}

if (runner.HasCustomArg("--no-shared-gvfs-cache"))
{
Console.WriteLine("Running without a shared git object cache");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using GVFS.Common;
using GVFS.Common.Git;
using GVFS.Common.Tracing;
using GVFS.FunctionalTests.Tools;
using GVFS.UnitTests.Category;
using NUnit.Framework;
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;

namespace GVFS.FunctionalTests.Tests.EnlistmentPerTestCase
{
[TestFixture]
/* Not inheriting from TestsWithEnlistmentPerTestCase because we don't need to mount
* the repo for this test. */
public class SafeDirectoryOwnershipTests
{
private GVFSEnlistment Enlistment;
private static readonly SecurityIdentifier usersSid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);

[SetUp]
public void SetUp()
{
var enlistmentRoot = GVFSFunctionalTestEnlistment.GetUniqueEnlistmentRoot();
Enlistment = new GVFSEnlistment(
enlistmentRoot,
GVFSTestConfig.RepoToClone,
GVFSPlatform.Instance.GitInstallation.GetInstalledGitBinPath(),
authentication: null);
var process = Enlistment.CreateGitProcess();
Common.Git.GitProcess.Init(Enlistment);
}

[TestCase]
public void RepoOpensIfSafeDirectoryConfigIsSet()
{
var repoDir = this.Enlistment.WorkingDirectoryBackingRoot;
using (var safeDirectoryConfig = WithSafeDirectoryConfig(repoDir))
using (var enlistmentOwner = WithEnlistmentOwner(usersSid))
using (LibGit2Repo repo = new LibGit2Repo(NullTracer.Instance, repoDir))
{
// repo is opened in the constructor
}
}

[TestCase(true)]
[TestCase(false)]
[Category(CategoryConstants.CaseInsensitiveFileSystemOnly)]
public void RepoOpensEvenIfSafeDirectoryConfigIsCaseMismatched(bool upperCase)
{
var repoDir = this.Enlistment.WorkingDirectoryBackingRoot;

if (upperCase)
{
repoDir = repoDir.ToUpperInvariant();
}
else
{
repoDir = repoDir.ToLowerInvariant();
}
using (var safeDirectoryConfig = WithSafeDirectoryConfig(this.Enlistment.WorkingDirectoryBackingRoot))
using (var enlistmentOwner = WithEnlistmentOwner(usersSid))
using (LibGit2Repo repo = new LibGit2Repo(NullTracer.Instance, repoDir))
{
// repo is opened in the constructor
}
}

private class Disposable : IDisposable
{
private readonly Action onDispose;

public Disposable(Action onDispose)
{
this.onDispose = onDispose;
}

public void Dispose()
{
onDispose();
}
}

private IDisposable WithSafeDirectoryConfig(string repoDir)
{
Tools.GitProcess.Invoke(null, $"config --global --add safe.directory \"{repoDir}\"");
return new Disposable(() =>
Tools.GitProcess.Invoke(null, $"config --global --unset safe.directory \"{repoDir}\""));
}

private IDisposable WithEnlistmentOwner(SecurityIdentifier newOwner)
{
var repoDir = this.Enlistment.WorkingDirectoryBackingRoot;
var currentOwner = GetDirectoryOwner(repoDir);

SetDirectoryOwner(repoDir, newOwner);
var updatedOwner = GetDirectoryOwner(repoDir);
return new Disposable(() =>
SetDirectoryOwner(repoDir, currentOwner));
}

private SecurityIdentifier GetDirectoryOwner(string directory)
{
DirectorySecurity repoSecurity = Directory.GetAccessControl(directory);
return (SecurityIdentifier)repoSecurity.GetOwner(typeof(SecurityIdentifier));
}

private void SetDirectoryOwner(string directory, SecurityIdentifier newOwner)
{
using (new PrivilegeEnabler(PrivilegeEnabler.AllowChangeOwnerToGroup))
{
DirectorySecurity repoSecurity = Directory.GetAccessControl(directory);
repoSecurity.SetOwner(newOwner);
Directory.SetAccessControl(directory, repoSecurity);
}
}
}
}
Loading
Loading