Skip to content
Merged
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
38 changes: 34 additions & 4 deletions Assets/Tests/Editor/BridgeTransportEndpointTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -59,11 +60,31 @@ public void CreateProjectIpc_WhenCanonicalRootsFromSharedContract_MatchExpectedE
}
}

/// <summary>
/// Verifies the contract permits a symlinked parent but never a symlinked endpoint directory.
/// </summary>
[Test]
public void UnixSecurityPolicy_WhenRead_DefinesParentAndEndpointSymlinkBoundary()
{
JObject contract = ReadEndpointContract();
JObject policy = contract.Value<JObject>("unixSecurityPolicy");

Assert.That(policy, Is.Not.Null);
Assert.That(policy.Value<string>("parentPath"), Is.EqualTo("/tmp"));
Assert.That(policy.Value<bool>("parentMayBeSymbolicLink"), Is.True);
Assert.That(policy.Value<uint>("resolvedParentOwnerUid"), Is.EqualTo(0));
Assert.That(policy.Value<bool>("resolvedParentRequiresStickyBit"), Is.True);
Assert.That(policy.Value<bool>("endpointDirectoryMayBeSymbolicLink"), Is.False);
Assert.That(policy.Value<string>("endpointDirectoryMode"), Is.EqualTo("0700"));
Assert.That(
policy.Value<JArray>("endpointDirectoryRejectedSpecialModes")?.Values<string>(),
Is.EqualTo(new[] { "04700", "02700" }));
Assert.That(policy.Value<string>("socketMode"), Is.EqualTo("0600"));
}

private static IEnumerable<JObject> ReadEndpointContractCases()
{
string projectRoot = UnityCliLoopPathResolver.GetProjectRoot();
string json = File.ReadAllText(Path.Combine(projectRoot, SharedEndpointContractPath));
JObject contract = JObject.Parse(json);
JObject contract = ReadEndpointContract();
JArray cases = contract.Value<JArray>("cases");
Assert.That(cases, Is.Not.Null.And.Not.Empty);
foreach (JToken caseToken in cases)
Expand All @@ -72,6 +93,13 @@ private static IEnumerable<JObject> ReadEndpointContractCases()
}
}

private static JObject ReadEndpointContract()
{
string projectRoot = UnityCliLoopPathResolver.GetProjectRoot();
string json = File.ReadAllText(Path.Combine(projectRoot, SharedEndpointContractPath));
return JObject.Parse(json);
}

private static bool CanExerciseProjectRootOnCurrentPlatform(string projectRoot)
{
bool looksLikeWindowsPath = projectRoot.Length >= 2 && projectRoot[1] == ':';
Expand All @@ -90,7 +118,9 @@ private static string ExpectedEndpointPathForCurrentPlatform(JObject contractCas
return contractCase.Value<string>("windowsPipePath");
}

return contractCase.Value<string>("unixSocketPath");
string template = contractCase.Value<string>("unixSocketPathTemplate");
uint effectiveUserId = new UnixNativeFileSystem().GetEffectiveUserId();
return template.Replace("<UID>", effectiveUserId.ToString(CultureInfo.InvariantCulture));
}
}
}
61 changes: 61 additions & 0 deletions Assets/Tests/Editor/BridgeTransportListenerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.IO;
using System.Threading;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

using io.github.hatayama.UnityCliLoop.Infrastructure;

Expand Down Expand Up @@ -68,6 +70,65 @@ public void AcceptClient_AfterStop_ThrowsObjectDisposedException()
Assert.Throws<ObjectDisposedException>(
() => listener.AcceptClient(CancellationToken.None));
}

/// <summary>
/// Verifies listener startup leaves an untrusted regular file untouched instead of deleting it.
/// </summary>
[Test]
public void Start_WhenEndpointPathIsRegularFile_FailsWithoutDeletingFile()
{
string endpointDirectory = Path.GetDirectoryName(_endpoint.Path);
UnixEndpointSecurityResult directoryResult = new UnixEndpointSecurityPolicy(new UnixNativeFileSystem())
.EnsureEndpointDirectory(endpointDirectory);
Assert.That(directoryResult.Success, Is.True, directoryResult.ErrorMessage);
File.WriteAllText(_endpoint.Path, "untrusted");
UnixDomainSocketBridgeTransportListener listener = new(_endpoint);

Assert.Throws<IOException>(() => listener.Start());
Assert.That(File.Exists(_endpoint.Path), Is.True);
}

/// <summary>
/// Verifies listener startup uses a 0700 directory and creates a 0600 Unix socket.
/// </summary>
[Test]
public void Start_WhenSuccessful_UsesOwnerOnlyDirectoryAndSocket()
{
UnixDomainSocketBridgeTransportListener listener = new(_endpoint);
listener.Start();
UnixNativeFileSystem fileSystem = new();

UnixFileMetadata directoryMetadata = fileSystem.ReadMetadata(
Path.GetDirectoryName(_endpoint.Path),
followSymbolicLinks: false);
UnixFileMetadata socketMetadata = fileSystem.ReadMetadata(
_endpoint.Path,
followSymbolicLinks: false);

Assert.That(directoryMetadata.Kind, Is.EqualTo(UnixFileKind.Directory));
Assert.That(directoryMetadata.Mode, Is.EqualTo(0x1C0));
Assert.That(socketMetadata.Kind, Is.EqualTo(UnixFileKind.Socket));
Assert.That(socketMetadata.Mode, Is.EqualTo(0x180));
listener.Stop();
}

/// <summary>
/// Verifies teardown warns and preserves an untrusted replacement instead of throwing or deleting it.
/// </summary>
[Test]
public void Stop_WhenSocketWasReplacedWithRegularFile_WarnsAndPreservesFile()
{
UnixDomainSocketBridgeTransportListener listener = new(_endpoint);
listener.Start();
File.Delete(_endpoint.Path);
File.WriteAllText(_endpoint.Path, "replacement");
LogAssert.Expect(
LogType.Warning,
new System.Text.RegularExpressions.Regex("Refusing to remove untrusted existing Unix endpoint"));

Assert.DoesNotThrow(() => listener.Stop());
Assert.That(File.Exists(_endpoint.Path), Is.True);
}
}

/// <summary>
Expand Down
Loading