diff --git a/Source/common/BUILD b/Source/common/BUILD index d741e502..1d2dd42d 100644 --- a/Source/common/BUILD +++ b/Source/common/BUILD @@ -60,6 +60,56 @@ santa_unit_test( ], ) +# This target shouldn't be used directly. +# Use a more specific scoped type instead. +objc_library( + name = "ScopedTypeRef", + hdrs = ["ScopedTypeRef.h"], + visibility = ["//Source/common:__pkg__"], +) + +objc_library( + name = "ScopedCFTypeRef", + hdrs = ["ScopedCFTypeRef.h"], + deps = [ + ":ScopedTypeRef", + ], +) + +santa_unit_test( + name = "ScopedCFTypeRefTest", + srcs = ["ScopedCFTypeRefTest.mm"], + sdk_frameworks = [ + "Security", + ], + deps = [ + ":ScopedCFTypeRef", + ], +) + +objc_library( + name = "ScopedIOObjectRef", + hdrs = ["ScopedIOObjectRef.h"], + sdk_frameworks = [ + "IOKit", + ], + deps = [ + ":ScopedTypeRef", + ], +) + +santa_unit_test( + name = "ScopedIOObjectRefTest", + srcs = ["ScopedIOObjectRefTest.mm"], + sdk_frameworks = [ + "IOKit", + ], + deps = [ + ":ScopedIOObjectRef", + "//Source/santad:EndpointSecuritySerializerUtilities", + ], +) + objc_library( name = "BranchPrediction", hdrs = ["BranchPrediction.h"], @@ -438,6 +488,8 @@ test_suite( ":SNTMetricSetTest", ":SNTRuleTest", ":SantaCacheTest", + ":ScopedCFTypeRefTest", + ":ScopedIOObjectRefTest", ], visibility = ["//:santa_package_group"], ) diff --git a/Source/common/ScopedCFTypeRef.h b/Source/common/ScopedCFTypeRef.h new file mode 100644 index 00000000..86213828 --- /dev/null +++ b/Source/common/ScopedCFTypeRef.h @@ -0,0 +1,29 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__COMMON__SCOPEDCFTYPEREF_H +#define SANTA__COMMON__SCOPEDCFTYPEREF_H + +#include + +#include "Source/common/ScopedTypeRef.h" + +namespace santa::common { + +template +using ScopedCFTypeRef = ScopedTypeRef; + +} // namespace santa::common + +#endif diff --git a/Source/common/ScopedCFTypeRefTest.mm b/Source/common/ScopedCFTypeRefTest.mm new file mode 100644 index 00000000..f82b3b79 --- /dev/null +++ b/Source/common/ScopedCFTypeRefTest.mm @@ -0,0 +1,141 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#include +#import +#include "XCTest/XCTest.h" + +#include "Source/common/ScopedCFTypeRef.h" + +using santa::common::ScopedCFTypeRef; + +@interface ScopedCFTypeRefTest : XCTestCase +@end + +@implementation ScopedCFTypeRefTest + +- (void)testDefaultConstruction { + // Default construction creates wraps a NULL object + ScopedCFTypeRef scopedRef; + XCTAssertFalse(scopedRef.Unsafe()); +} + +- (void)testOperatorBool { + // Operator bool is `false` when object is null + { + ScopedCFTypeRef scopedNullRef; + XCTAssertFalse(scopedNullRef.Unsafe()); + XCTAssertFalse(scopedNullRef); + } + + // Operator bool is `true` when object is NOT null + { + int x = 123; + CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &x); + + ScopedCFTypeRef scopedNumRef = ScopedCFTypeRef::Assume(numRef); + XCTAssertTrue(scopedNumRef.Unsafe()); + XCTAssertTrue(scopedNumRef); + } +} + +// Note that CFMutableArray is used for testing, even when subtypes aren't +// needed, because it is never optimized into immortal constant values, unlike +// other types. +- (void)testAssume { + int want = 123; + int got = 0; + CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks); + + // Baseline state, initial retain count is 1 after object creation + XCTAssertEqual(1, CFGetRetainCount(array)); + + CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want); + CFArrayAppendValue(array, numRef); + CFRelease(numRef); + + XCTAssertEqual(1, CFArrayGetCount(array)); + + { + ScopedCFTypeRef scopedArray = + ScopedCFTypeRef::Assume(array); + + // Ensure ownership was taken, and retain count remains unchanged + XCTAssertTrue(scopedArray.Unsafe()); + XCTAssertEqual(1, CFGetRetainCount(scopedArray.Unsafe())); + + // Make sure the object contains expected contents + CFMutableArrayRef ref = scopedArray.Unsafe(); + XCTAssertEqual(1, CFArrayGetCount(ref)); + XCTAssertTrue( + CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got)); + XCTAssertEqual(want, got); + } +} + +// Note that CFMutableArray is used for testing, even when subtypes aren't +// needed, because it is never optimized into immortal constant values, unlike +// other types. +- (void)testRetain { + int want = 123; + int got = 0; + CFMutableArrayRef array = CFArrayCreateMutable(nullptr, /*capacity=*/0, &kCFTypeArrayCallBacks); + + // Baseline state, initial retain count is 1 after object creation + XCTAssertEqual(1, CFGetRetainCount(array)); + + CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &want); + CFArrayAppendValue(array, numRef); + CFRelease(numRef); + + XCTAssertEqual(1, CFArrayGetCount(array)); + + { + ScopedCFTypeRef scopedArray = + ScopedCFTypeRef::Retain(array); + + // Ensure ownership was taken, and retain count was incremented + XCTAssertTrue(scopedArray.Unsafe()); + XCTAssertEqual(2, CFGetRetainCount(scopedArray.Unsafe())); + + // Make sure the object contains expected contents + CFMutableArrayRef ref = scopedArray.Unsafe(); + XCTAssertEqual(1, CFArrayGetCount(ref)); + XCTAssertTrue( + CFNumberGetValue((CFNumberRef)CFArrayGetValueAtIndex(ref, 0), kCFNumberIntType, &got)); + XCTAssertEqual(want, got); + } + + // The original `array` object should still be valid due to the extra retain. + // Ensure the retain count has decreased since `scopedArray` went out of scope + XCTAssertEqual(1, CFArrayGetCount(array)); +} + +- (void)testInto { + ScopedCFTypeRef scopedURLRef = + ScopedCFTypeRef::Assume(CFURLCreateWithFileSystemPath( + kCFAllocatorDefault, CFSTR("/usr/bin/true"), kCFURLPOSIXPathStyle, YES)); + + ScopedCFTypeRef scopedCodeRef; + XCTAssertFalse(scopedCodeRef); + + SecStaticCodeCreateWithPath(scopedURLRef.Unsafe(), kSecCSDefaultFlags, + scopedCodeRef.InitializeInto()); + + // Ensure the scoped object was initialized + XCTAssertTrue(scopedCodeRef); +} + +@end diff --git a/Source/common/ScopedIOObjectRef.h b/Source/common/ScopedIOObjectRef.h new file mode 100644 index 00000000..a21e09e1 --- /dev/null +++ b/Source/common/ScopedIOObjectRef.h @@ -0,0 +1,30 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__COMMON__SCOPEDIOOBJECTREF_H +#define SANTA__COMMON__SCOPEDIOOBJECTREF_H + +#include + +#include "Source/common/ScopedTypeRef.h" + +namespace santa::common { + +template +using ScopedIOObjectRef = + ScopedTypeRef; + +} + +#endif diff --git a/Source/common/ScopedIOObjectRefTest.mm b/Source/common/ScopedIOObjectRefTest.mm new file mode 100644 index 00000000..52ba946c --- /dev/null +++ b/Source/common/ScopedIOObjectRefTest.mm @@ -0,0 +1,104 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#include +#include +#include +#import + +#include "Source/common/ScopedIOObjectRef.h" +#include "Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h" + +using santa::common::ScopedIOObjectRef; +using santa::santad::logs::endpoint_security::serializers::Utilities::GetDefaultIOKitCommsPort; + +@interface ScopedIOObjectRefTest : XCTestCase +@end + +@implementation ScopedIOObjectRefTest + +- (void)testDefaultConstruction { + // Default construction creates wraps a NULL object + ScopedIOObjectRef scopedRef; + XCTAssertFalse(scopedRef.Unsafe()); +} + +- (void)testOperatorBool { + // Operator bool is `false` when object is null + { + ScopedIOObjectRef scopedNullRef; + XCTAssertFalse(scopedNullRef.Unsafe()); + XCTAssertFalse(scopedNullRef); + } + + // Operator bool is `true` when object is NOT null + { + CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName); + XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict); + + io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict); + + ScopedIOObjectRef scopedServiceRef = + ScopedIOObjectRef::Assume(service); + + XCTAssertTrue(scopedServiceRef.Unsafe()); + XCTAssertTrue(scopedServiceRef); + } +} + +- (void)testAssume { + CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName); + XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict); + + io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict); + + // Baseline state, initial retain count is 1 after object creation + XCTAssertEqual(1, IOObjectGetUserRetainCount(service)); + XCTAssertNotEqual(IO_OBJECT_NULL, service); + + { + ScopedIOObjectRef scopedIORef = ScopedIOObjectRef::Assume(service); + + // Ensure ownership was taken, and retain count remains unchanged + XCTAssertTrue(scopedIORef.Unsafe()); + XCTAssertEqual(1, IOObjectGetUserRetainCount(scopedIORef.Unsafe())); + XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe()); + } +} + +- (void)testRetain { + CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName); + XCTAssertNotEqual((CFMutableDictionaryRef)NULL, matchingDict); + + io_service_t service = IOServiceGetMatchingService(GetDefaultIOKitCommsPort(), matchingDict); + + // Baseline state, initial retain count is 1 after object creation + XCTAssertEqual(1, IOObjectGetUserRetainCount(service)); + XCTAssertNotEqual(IO_OBJECT_NULL, service); + + { + ScopedIOObjectRef scopedIORef = ScopedIOObjectRef::Retain(service); + + // Ensure ownership was taken, and retain count was incremented + XCTAssertTrue(scopedIORef.Unsafe()); + XCTAssertEqual(2, IOObjectGetUserRetainCount(scopedIORef.Unsafe())); + XCTAssertNotEqual(IO_OBJECT_NULL, scopedIORef.Unsafe()); + } + + // The original `service` object should still be valid due to the extra retain. + // Ensure the retain count has decreased since `scopedIORef` went out of scope. + XCTAssertEqual(1, IOObjectGetUserRetainCount(service)); +} + +@end diff --git a/Source/common/ScopedTypeRef.h b/Source/common/ScopedTypeRef.h new file mode 100644 index 00000000..be8e12ba --- /dev/null +++ b/Source/common/ScopedTypeRef.h @@ -0,0 +1,80 @@ +/// Copyright 2023 Google LLC +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// https://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. + +#ifndef SANTA__COMMON__SCOPEDTYPEREF_H +#define SANTA__COMMON__SCOPEDTYPEREF_H + +#include +#include + +namespace santa::common { + +template +class ScopedTypeRef { + public: + ScopedTypeRef() : object_(InvalidV) {} + + // Can be implemented safely, but not currently needed + ScopedTypeRef(ScopedTypeRef&& other) = delete; + ScopedTypeRef& operator=(ScopedTypeRef&& rhs) = delete; + ScopedTypeRef(const ScopedTypeRef& other) = delete; + ScopedTypeRef& operator=(const ScopedTypeRef& other) = delete; + + // Take ownership of a given object + static ScopedTypeRef Assume( + ElementT object) { + return ScopedTypeRef(object); + } + + // Retain and take ownership of a given object + static ScopedTypeRef Retain( + ElementT object) { + if (object) { + RetainFunc(object); + } + return ScopedTypeRef(object); + } + + ~ScopedTypeRef() { + if (object_) { + ReleaseFunc(object_); + object_ = InvalidV; + } + } + + explicit operator bool() { return object_ != InvalidV; } + + ElementT Unsafe() { return object_; } + + // This is to be used only to take ownership of objects that are created by + // pass-by-pointer create functions. The object must not already be valid. + // In non-opt builds, this is enforced by an assert that will terminate the + // process. + ElementT* InitializeInto() { + assert(object_ == InvalidV); + return &object_; + } + + private: + // Not API. + // Use Assume or Retain static methods. + ScopedTypeRef(ElementT object) : object_(object) {} + + ElementT object_; +}; + +} // namespace santa::common + +#endif diff --git a/Source/santad/BUILD b/Source/santad/BUILD index 79d64b27..db940216 100644 --- a/Source/santad/BUILD +++ b/Source/santad/BUILD @@ -21,6 +21,9 @@ objc_library( name = "SNTRuleTable", srcs = ["DataLayer/SNTRuleTable.m"], hdrs = ["DataLayer/SNTRuleTable.h"], + sdk_dylibs = [ + "EndpointSecurity", + ], deps = [ ":SNTDatabaseTable", "//Source/common:Platform", @@ -475,7 +478,6 @@ objc_library( "bsm", ], deps = [ - ":EndpointSecurityEnrichedTypes", ":EndpointSecurityMessage", ":SNTDecisionCache", "//Source/common:SantaCache", diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h b/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h index ada70eb8..3dab190d 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.h @@ -59,6 +59,8 @@ NSString *MountFromName(NSString *path); es_file_t *GetAllowListTargetFile( const santa::santad::event_providers::endpoint_security::Message &msg); +const mach_port_t GetDefaultIOKitCommsPort(); + } // namespace santa::santad::logs::endpoint_security::serializers::Utilities #endif diff --git a/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.mm b/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.mm index fdd0cd72..b2c65e0a 100644 --- a/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.mm +++ b/Source/santad/Logs/EndpointSecurity/Serializers/Utilities.mm @@ -80,7 +80,7 @@ NSString *OriginalPathForTranslocation(const es_process_t *es_proc) { return [origURL path]; } -static inline const mach_port_t GetDefaultIOKitCommsPort() { +const mach_port_t GetDefaultIOKitCommsPort() { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return kIOMasterPortDefault; diff --git a/Source/santad/SNTCompilerController.mm b/Source/santad/SNTCompilerController.mm index 376629d7..e6ac90c6 100644 --- a/Source/santad/SNTCompilerController.mm +++ b/Source/santad/SNTCompilerController.mm @@ -132,7 +132,8 @@ static constexpr std::string_view kIgnoredCompilerProcessPathPrefix = "/dev/"; NSError *error = nil; SNTFileInfo *fi = [[SNTFileInfo alloc] initWithEndpointSecurityFile:targetFile error:&error]; if (error) { - LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | Path: %@ | Error: %@", + LOGD(@"Unable to create SNTFileInfo while attempting to create transitive rule. Event: %d | " + @"Path: %@ | Error: %@", (int)esMsg->event_type, @(targetFile->path.data), error); return; }