mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
# Description `ContractPaths.Relative` now also compares a path that seems to escape against the root's real location, with every link along it resolved. Links inside the drop are still rejected, because every caller resolves the returned relative path segment by segment. The new contract test `DropReachedThroughALinkedDirectoryIsValidatedAsync` validates a drop through a directory link and fails on every platform without the fix. Its link is created like the one in the existing link test (a junction on Windows, a symlink elsewhere), now through one shared helper, so it needs no privileges and is never skipped. Tests: - On macOS, master passes 53 of 209 contract tests; with the fix 210/210 on net10.0 (Release, 0 skipped). - Fork CI (full matrix incl. macOS) and CodeQL: green on `358eb33d5` (the same change on master `029ac0f33`) — [CI run 36892610480](https://github.com/biancode/UA-.NETStandard/actions/runs/36892610480) (189 jobs), [CodeQL run 36892614293](https://github.com/biancode/UA-.NETStandard/actions/runs/36892614293). The branch is now on a newer master with no overlapping files; the run on the current head is in progress: [CI run 37016188861](https://github.com/biancode/UA-.NETStandard/actions/runs/37016188861). ## Related Issues - Fixes #4639 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. (Affected suites on net10.0 and net8.0 locally on macOS; .NET Framework is covered by the fork CI's Windows net48 legs.) - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
207 lines
8.7 KiB
C#
207 lines
8.7 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
* OPC Foundation MIT License 1.00. See LICENSE.txt in the repository root.
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using NUnit.Framework;
|
|
|
|
namespace Opc.Ua.OneFuzz.Validator.Tests
|
|
{
|
|
public sealed partial class ValidatorContractTests
|
|
{
|
|
[Test]
|
|
public async Task MissingCorpusPathFailsDespiteOtherPublishedSeedsAsync()
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
const string kMissing = "fuzzing/ContractFixture/Corpus/Missing";
|
|
drop.Targets[0]!["corpus"] = ContractDrop.Strings([kMissing]);
|
|
|
|
await AssertPreflightRejectedAsync(drop, "Required path is missing", kMissing).ConfigureAwait(false);
|
|
}
|
|
|
|
[Test]
|
|
public async Task EveryRequiredCorpusBucketMustExistEvenWhenAnotherBucketHasSeedsAsync()
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
Directory.Delete(drop.PathFor(ContractDrop.SecondBucket), recursive: true);
|
|
Assert.That(Directory.GetFiles(drop.PathFor(ContractDrop.FirstBucket)), Is.Not.Empty);
|
|
|
|
await AssertPreflightRejectedAsync(drop, "Required path is missing", ContractDrop.SecondBucket)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
[Test]
|
|
public async Task DropReachedThroughALinkedDirectoryIsValidatedAsync()
|
|
{
|
|
// The host reports dependency paths with links resolved (macOS reports
|
|
// /private/var for /var, where every temporary drop lives), so a drop
|
|
// addressed through a link must still recognise its own files.
|
|
using var drop = new ContractDrop(m_fixture);
|
|
drop.Save();
|
|
string link = Path.Combine(drop.Home, "linked drop");
|
|
await CreateDirectoryLinkAsync(drop, link, drop.Root).ConfigureAwait(false);
|
|
try
|
|
{
|
|
ProcessResult result = await RunAsync(
|
|
drop, "validate", "--drop", link, "--results", drop.Results).ConfigureAwait(false);
|
|
|
|
AssertCompleted(drop, result, hasConfig: true);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(link);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task MissingExactSeedCannotBeReplacedByANeighbouringFileOrAnotherBucketAsync()
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
string missing = ContractDrop.FirstBucket + "/missing.bin";
|
|
drop.Targets[0]!["corpus"] = ContractDrop.Strings([missing, ContractDrop.SecondBucket]);
|
|
|
|
await AssertPreflightRejectedAsync(drop, "Required path is missing", missing).ConfigureAwait(false);
|
|
}
|
|
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public async Task EmptyRequiredCorpusFailsEvenWithOtherNonemptyBucketsAsync(bool emptySubdirectory)
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
string bucket = drop.PathFor(ContractDrop.FirstBucket);
|
|
Directory.Delete(bucket, recursive: true);
|
|
Directory.CreateDirectory(emptySubdirectory ? Path.Combine(bucket, "empty child") : bucket);
|
|
Assert.That(Directory.GetFiles(drop.PathFor(ContractDrop.SecondBucket)), Is.Not.Empty);
|
|
|
|
await AssertPreflightRejectedAsync(
|
|
drop, "Required corpus is empty", ContractDrop.InspectId, ContractDrop.FirstBucket)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
[TestCase(ContractDrop.FirstDictionary)]
|
|
[TestCase(ContractDrop.SecondDictionary)]
|
|
public async Task MissingRequiredSourceDictionaryFailsBeforeReplayAsync(string dictionary)
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
File.Delete(drop.PathFor(dictionary));
|
|
|
|
await AssertPreflightRejectedAsync(drop, "Required path is missing", dictionary).ConfigureAwait(false);
|
|
}
|
|
|
|
[Test]
|
|
public async Task EmptyRequiredDictionaryFailsBeforeReplayAsync()
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
drop.WriteBytes(ContractDrop.FirstDictionary, []);
|
|
|
|
await AssertPreflightRejectedAsync(drop, "Required dictionary is empty", ContractDrop.FirstDictionary)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
[TestCase("../outside.bin")]
|
|
[TestCase("./fuzzing/ContractFixture/Corpus/Inspect")]
|
|
[TestCase("fuzzing/ContractFixture/Corpus/RequiredBucket/../Inspect")]
|
|
[TestCase("/fuzzing/ContractFixture/Corpus/Inspect")]
|
|
[TestCase("C:/outside.bin")]
|
|
[TestCase("fuzzing//ContractFixture/Corpus/Inspect")]
|
|
[TestCase("fuzzing/ContractFixture/Corpus/*")]
|
|
public async Task CorpusPathsMustBePortableWithoutTraversalOrGlobsAsync(string path)
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
drop.Targets[0]!["corpus"] = ContractDrop.Strings([path]);
|
|
|
|
await AssertPreflightRejectedAsync(drop, "Expected a portable relative path", path).ConfigureAwait(false);
|
|
}
|
|
|
|
[TestCase("assembly", "opc.Ua.OneFuzz.TestTarget.dll")]
|
|
[TestCase("corpus", "fuzzing/contractFixture/Corpus/Inspect")]
|
|
[TestCase("dictionaries", "fuzzing/dictionaries/contract-first.dict")]
|
|
public async Task EveryDeclaredPathUsesExactCaseEvenOnWindowsAsync(string field, string path)
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
if (field == "assembly")
|
|
{
|
|
drop.Area[field] = path;
|
|
}
|
|
else
|
|
{
|
|
drop.Targets[0]![field] = ContractDrop.Strings([path]);
|
|
}
|
|
|
|
await AssertPreflightRejectedAsync(drop, "missing or has incorrect case", path).ConfigureAwait(false);
|
|
}
|
|
|
|
[Test]
|
|
public async Task OverlappingCorpusDeclarationsReplayEachPathOnceButDoNotDeduplicateEqualBytesAsync()
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
drop.Targets[0]!["corpus"] = ContractDrop.Strings(
|
|
[
|
|
ContractDrop.FirstBucket,
|
|
ContractDrop.FirstBucket + "/Z-first seed.bin",
|
|
ContractDrop.SecondBucket
|
|
]);
|
|
drop.Save();
|
|
|
|
ProcessResult result = await ValidateAsync(drop).ConfigureAwait(false);
|
|
|
|
// Inspect has three distinct paths (including a zero-byte seed), while
|
|
// Measure must keep both different paths containing the same byte array.
|
|
AssertCompleted(drop, result, hasConfig: true);
|
|
}
|
|
|
|
[TestCase(false)]
|
|
[TestCase(true)]
|
|
public async Task LinkedCorpusDirectoryIsRejectedBothDirectlyAndDuringRecursiveEnumerationAsync(bool nested)
|
|
{
|
|
using var drop = new ContractDrop(m_fixture);
|
|
string relative = nested ? ContractDrop.FirstBucket + "/linked seeds" : "linked corpus";
|
|
string link = drop.PathFor(relative);
|
|
string destination = drop.PathFor(ContractDrop.SecondBucket);
|
|
await CreateDirectoryLinkAsync(drop, link, destination).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
Assert.That(File.GetAttributes(link) & FileAttributes.ReparsePoint,
|
|
Is.EqualTo(FileAttributes.ReparsePoint));
|
|
if (!nested)
|
|
{
|
|
drop.Targets[0]!["corpus"] = ContractDrop.Strings([relative]);
|
|
}
|
|
|
|
await AssertPreflightRejectedAsync(drop, "Links are not allowed in a relocatable drop")
|
|
.ConfigureAwait(false);
|
|
}
|
|
finally
|
|
{
|
|
Directory.Delete(link);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a directory link: an NTFS directory junction on Windows, which
|
|
/// does not require symlink privileges or Developer Mode and still
|
|
/// exercises the reparse-point contract, and a symbolic link elsewhere.
|
|
/// </summary>
|
|
private static async Task CreateDirectoryLinkAsync(ContractDrop drop, string link, string destination)
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
ProcessResult creation = await ProcessRunner.RunAsync(
|
|
Environment.GetEnvironmentVariable("COMSPEC") ?? "cmd.exe",
|
|
["/d", "/c", "mklink", "/J", link, destination],
|
|
drop.Home,
|
|
TimeSpan.FromSeconds(15)).ConfigureAwait(false);
|
|
AssertSuccess(creation);
|
|
}
|
|
else
|
|
{
|
|
Directory.CreateSymbolicLink(link, destination);
|
|
}
|
|
}
|
|
}
|
|
}
|