// ---------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // ---------------------------------------------------------------------------- namespace Microsoft.Singularity.Security { using System; using System.Text; using System.Text.RegularExpressions; using System.Collections; using Microsoft.Contracts; using Microsoft.Singularity.Security; using Microsoft.Singularity.Security.AccessControl; using Microsoft.Singularity.Channels; /// /// Access control implementation core. /// public interface IAclCoreSupport { string Expand(string! path); } public class AclCore { public enum CacheLevel {All, Regex, Expn, None} private Cache! cache; private AclConverter! converter; private Principal self = Principal.Self(); private ulong KernelPrincipalID = 1; private bool securityDisabled = false; private string coreName; private CacheLevel cacheLevel = CacheLevel.All; private const int AclCacheMaxEntries = 200; private const int AclCacheExpirySeconds = 15 * 60; private const int AclCachePrunePercent = 20; private const int ExpnCacheMaxEntries = 100; private const int ExpnCacheExpirySeconds = 60 * 60; private ulong nfast = 0; private ulong nregex = 0; private ulong nexpn = 0; private ulong nfull = 0; private ulong nfail = 0; #if DO_TIMING private Statistics fastStats = new Statistics("Acl fast ok:"); private Statistics regexStats = new Statistics("Acl rgxp ok:"); private Statistics expnStats = new Statistics("Acl expn ok:"); private Statistics fullStats = new Statistics("Acl full ok:"); private Statistics failureStats = new Statistics("Acl fail: "); #endif #if !SINGULARITY_PROCESS public static Principal EndpointPeer(Endpoint*! in ExHeap ep) { return PrincipalImpl.MakePrincipal(ep->PeerPrincipalHandle.val); } #else public static Principal EndpointPeer(Endpoint*! in ExHeap ep) { return Principal.EndpointPeer(ep); } #endif [NotDelayed] public AclCore(string _coreName, IAclCoreSupport _support) { this(_coreName, _support, AclCacheMaxEntries, AclCacheExpirySeconds, ExpnCacheMaxEntries, ExpnCacheExpirySeconds); } [NotDelayed] public AclCore(string _coreName, IAclCoreSupport _support, int aclCacheMaxEntries, int aclCacheExpirySeconds, int expnCacheMaxEntries, int expnCacheExpirySeconds) { // // This implementation consists of two caches, a cache of regular expression object // (indexed by ACL) and a cache of expansion subexpressions (indexed by expr name). // Both caches are objects of type Cache and can be enabled, disabled and flushed // through the methods of the cache object. // this.coreName = _coreName; this.cache = new Cache(aclCacheMaxEntries, aclCacheExpirySeconds, AclCachePrunePercent, "Acl cache: "); this.converter = new AclConverter(_support, expnCacheMaxEntries, expnCacheExpirySeconds); base(); #if !SINGULARITY_PROCESS PrincipalImpl.RegisterAclCore(this); #endif } public void SetCacheLevel (CacheLevel val) { cacheLevel = val; } public bool Disable { set { securityDisabled = value; } } public void DumpStats(StringBuilder! sb) { if (coreName != null) sb.AppendFormat("[{0}]\n", coreName); #if DO_TIMING fastStats.DumpStats(sb); regexStats.DumpStats(sb); gCacheStats.DumpStats(sb); fullStats.DumpStats(sb); failureStats.DumpStats(sb); #endif sb.AppendFormat("Acl checks: fast:{0}, regex:{1}, expn:{2}, full:{3}, fail:{4}\n", nfast, nregex, nexpn, nfull, nfail); cache.DumpStats(sb); converter.DumpStats(sb); } public void ClearStats() { #if DO_TIMING fastStats.Clear(); regexStats.Clear(); fullStats.Clear(); expnStats.Clear(); failureStats.Clear(); #endif nfast = 0; nregex = 0; nexpn = 0; nfull = 0; nfail = 0; cache.ResetStats(); converter.ClearStats(); } public void FlushCache() { cache.Prune(true); converter.FlushCache(); } public AclConverter! Converter { get { return converter; }} // Returns true if id doesn't require an access check, that is the id gets // access to everything. For now the security service and the kernel always // has access later, we might choose to allow the security service to have // access, but not necessarily the kernel. In this case, we'd have // to allow the security service to deliver upcalls for expansion information // with a non-kernel principalId. enum Result {Failure, Full, Expn, Regex, Fast} public bool CheckAccess(string acl, AccessMode mode, Principal principal) { if (securityDisabled) return true; if (principal.Equal(self)) return true; return CheckAccessBody(acl, mode, principal); } // call here to avoid "securityDisabled" and "self" tests above public bool CheckAccessBody(string acl, AccessMode mode, Principal principal) { if (acl == null) return false; #if DO_TIMING ulong start = Processor.CycleCount; #endif Result res = Result.Failure; try { // check if we have the acl in the cache string principalStr = null; ulong prinPerm = principal.Val; if (mode != null) prinPerm = prinPerm + ((ulong)(mode.Num) << 48); CacheEntry ce = (CacheEntry) cache.GetEntry(acl); if (ce != null && !ce.Expired) { if (ce.Eval(prinPerm) && cacheLevel == CacheLevel.All) { res = Result.Fast; } else { principalStr = PrincipalAndMode((!)principal.GetName(), mode); if (ce.FullEval(principalStr, prinPerm) && cacheLevel <= CacheLevel.Regex) { res = Result.Regex; } } } bool secondPass = (cacheLevel==CacheLevel.None); while (res == Result.Failure) { // Not in the cache, no regex match, or expired: go and construct it. // Take one pass using the expansion cache, otherwise (second pass) start from scratch. // Thus, we cache positive results, but refresh to avoid false negatives. DateTime minExpiry; string! expansion = converter.Convert(acl, secondPass, out minExpiry); if (ce != null && ce.Expansion == expansion) { // Old expansion was the same, update expiry to save existing cached // regex evaluations. ce.ResetExpiry(cache, minExpiry); } if (principalStr == null) principalStr = PrincipalAndMode((!)principal.GetName(), mode); ce = new CacheEntry(cache, expansion, minExpiry); cache.AddEntry(acl, ce); if (ce.FullEval(principalStr, prinPerm)) { if (secondPass) res = Result.Full; else res = Result.Expn; } else if (secondPass) break; else secondPass = true; } } catch (Exception e) { DebugStub.Print("Exception: {0}", __arglist(e.Message)); res = Result.Failure; } #if DO_TIMING diff = Processor.CycleCount - start; switch (res) { case Result.Fast: fastStats.Add(diff); nfast++; break; case Result.Regex: regexStats.Add(diff); nregex++; break; case Result.Expn: expnStats.Add(diff); nexpn++; break; case Result.Full: fullStats.Add(diff); nfull++; break; case Result.Failure: failureStats.Add(diff); nfail++; break; } #else switch (res) { case Result.Fast: nfast++; break; case Result.Regex: nregex++; break; case Result.Expn: nexpn++; break; case Result.Full: nfull++; break; case Result.Failure: nfail++; break; } #endif return (res != Result.Failure); } // This is for testing, so that we can supply principalNames without requiring // they be existing in the kernel service. This should be very similar to the // CheckAccess method above. Do not mix the two!!! Caller should maintain a // 1-to-1 mapping between id's and principal names. public bool CheckAccess(string acl, AccessMode mode, ulong principalId, string! principalName) { if (securityDisabled) return true; if (acl == null) return false; #if !SINGULARITY_PROCESS if (principalId == self.Val) return true; #else if (principalId == KernelPrincipalID) return true; #endif #if DO_TIMING ulong start = Processor.CycleCount; #endif Result res = Result.Failure; try { // check if we have the acl in the cache string principalStr = null; ulong prinPerm = principalId; if (mode != null) prinPerm = prinPerm + ((ulong)(mode.Num) << 48); CacheEntry ce = (CacheEntry) cache.GetEntry(acl); if (ce != null && !ce.Expired) { if (ce.Eval(prinPerm) && cacheLevel == CacheLevel.All) { res = Result.Fast; } else { principalStr = PrincipalAndMode(principalName, mode); if (ce.FullEval(principalStr, prinPerm) && cacheLevel <= CacheLevel.Regex) { res = Result.Regex; } } } bool secondPass = (cacheLevel==CacheLevel.None); while (res == Result.Failure) { // Not in the cache, no regex match, or expired: go and construct it. // Take one pass using the expansionCache, otherwise (second pass) start from scratch. // Thus, we cache positive results, but refresh to avoid false negatives. DateTime minExpiry; string! expansion = converter.Convert(acl, secondPass, out minExpiry); if (ce != null && ce.Expansion == expansion) { // Old expansion was the same, update expiry to save existing cached // regex evaluations. ce.ResetExpiry(cache, minExpiry); } if (principalStr == null) principalStr = PrincipalAndMode(principalName, mode); ce = new CacheEntry(cache, expansion, minExpiry); cache.AddEntry(acl, ce); if (ce.FullEval(principalStr, prinPerm)) { if (secondPass) res = Result.Full; else res = Result.Expn; } else if (secondPass) break; else secondPass = true; } } catch (Exception e) { DebugStub.Print("Exception: {0}", __arglist(e.Message)); res = Result.Failure; } #if DO_TIMING diff = Processor.CycleCount - start; switch (res) { case Result.Fast: fastStats.Add(diff); nfast++; break; case Result.Regex: regexStats.Add(diff); nregex++; break; case Result.Expn: expnStats.Add(diff); nexpn++; break; case Result.Full: fullStats.Add(diff); nfull++; break; case Result.Failure: failureStats.Add(diff); nfail++; break; } #else switch (res) { case Result.Fast: nfast++; break; case Result.Regex: nregex++; break; case Result.Expn: nexpn++; break; case Result.Full: nfull++; break; case Result.Failure: nfail++; break; } #endif return (res != Result.Failure); } internal class CacheEntry : ICacheValue { private string! expansion; private Regex! regex; private Hashtable! evalCache; private const int MaxCachedEvals = 50; public CacheEntry (Cache! cache, string! _expansion, DateTime minExpiry) { this.regex = new Regex(_expansion); this.evalCache = new Hashtable(); this.expansion = _expansion; base(cache, minExpiry); } public string! Expansion { get { return this.expansion; } } public bool Eval(ulong prinPerm) { return (evalCache[prinPerm] != null); } public bool FullEval(string! principalStr, ulong prinPerm) { bool result = regex.IsMatch(principalStr); if (result) { // note that hash tables are multiple-reader safe lock (evalCache) { if (evalCache.Count >= MaxCachedEvals) { evalCache.Clear(); // just clear the HT if we've reached the limit } evalCache[prinPerm] = ""; } } return result; } } private string! PrincipalAndMode(string! principalName, AccessMode mode) { if (mode == null) return principalName; StringBuilder sb = new StringBuilder(); sb.Append(principalName); sb.Append("@"); sb.Append(mode.Val); return sb.ToString(); } #if DO_TIMING internal class Statistics { private ulong min; private ulong max; private ulong sum; private ulong count; public Statistics() { Clear(); } [Delayed] public void Clear() { min = 0; max = 0; sum = 0; count = 0; } public void Add(ulong sample) { count++; if (min == 0 || sample < min) { min = sample; } if (sample > max) { max = sample; } sum += sample; } public public void DumpStats(StringBuilder! sb) { ulong mean; if (count == 0) mean = 0; else mean = sum/(ulong)count; sb.AppendFormat("N:{1}, Min:{2}, Max:{3}, Mean:{4}\n", count, min, max, mean); } } #endif } }