Beispiel #1
0
        /// <exception cref="InvalidOperationException">snapshot already taken, <see cref="MemoryBlockBase.Active"/> is <see langword="false"/>, or failed to make memory read-only</exception>
        public override void SaveXorSnapshot()
        {
            if (_snapshot != null)
            {
                throw new InvalidOperationException("Snapshot already taken");
            }
            if (!Active)
            {
                throw new InvalidOperationException("Not active");
            }

            // temporarily switch the entire block to `R`: in case some areas are unreadable, we don't want
            // that to complicate things
            Kernel32.MemoryProtection old;
            if (!Kernel32.VirtualProtect(Z.UU(AddressRange.Start), Z.UU(Size), Kernel32.MemoryProtection.READONLY, out old))
            {
                throw new InvalidOperationException($"{nameof(Kernel32.VirtualProtect)}() returned FALSE!");
            }

            _snapshot = new byte[Size];
            var ds = new MemoryStream(_snapshot, true);
            var ss = GetStream(AddressRange.Start, Size, false);

            ss.CopyTo(ds);
            XorHash = WaterboxUtils.Hash(_snapshot);

            ProtectAll();
        }
Beispiel #2
0
        /// <exception cref="InvalidOperationException">failed to protect memory</exception>
        public override void Protect(ulong start, ulong length, Protection prot)
        {
            if (length == 0)
            {
                return;
            }

            var pstart = GetPage(start);
            var pend   = GetPage(start + length - 1);

            for (var i = pstart; i <= pend; i++)
            {
                _pageData[i] = prot;                                              // also store the value for later use
            }
            if (!Active)
            {
                return;                      // it's legal to call this method if we're not active; the information is just saved for the next activation
            }
            var computedStart = WaterboxUtils.AlignDown(start);
            var protEnum      = prot.ToMemoryProtection();
            var exitCode      = mprotect(
                Z.US(computedStart),
                Z.UU(WaterboxUtils.AlignUp(start + length) - computedStart),
                protEnum
                );

            if (exitCode != 0)
            {
                throw new InvalidOperationException($"{nameof(mprotect)}() returned {exitCode}!");
            }
        }
Beispiel #3
0
        /// <exception cref="InvalidOperationException">failed to protect memory</exception>
        public override void Protect(ulong start, ulong length, Protection prot)
        {
            if (length == 0)
            {
                return;
            }
            int pstart = GetPage(start);
            int pend   = GetPage(start + length - 1);

            var p = GetKernelMemoryProtectionValue(prot);

            for (int i = pstart; i <= pend; i++)
            {
                _pageData[i] = prot; // also store the value for later use
            }
            if (Active)              // it's legal to Protect() if we're not active; the information is just saved for the next activation
            {
                var computedStart  = WaterboxUtils.AlignDown(start);
                var computedEnd    = WaterboxUtils.AlignUp(start + length);
                var computedLength = computedEnd - computedStart;

                Kernel32.MemoryProtection old;
                if (!Kernel32.VirtualProtect(Z.UU(computedStart),
                                             Z.UU(computedLength), p, out old))
                {
                    throw new InvalidOperationException($"{nameof(Kernel32.VirtualProtect)}() returned FALSE!");
                }
            }
        }
Beispiel #4
0
        /// <summary>set r/w/x protection on a portion of memory. rounded to encompassing pages</summary>
        /// <exception cref="InvalidOperationException">failed to protect memory</exception>
        public void Protect(ulong start, ulong length, Protection prot)
        {
            EnsureActive();
            if (length == 0)
            {
                return;
            }

            // Note: asking for prot.none on memory that was not previously committed, commits it

            var computedStart  = WaterboxUtils.AlignDown(start);
            var computedEnd    = WaterboxUtils.AlignUp(start + length);
            var computedLength = computedEnd - computedStart;

            // potentially commit more memory
            var minNewCommittedSize = computedEnd - Start;

            if (minNewCommittedSize > CommittedSize)
            {
                CommittedSize = minNewCommittedSize;
                // Since Commit() was called, we have to do a full ProtectAll -- remember that when refactoring
                _pal.Commit(CommittedSize);
            }

            int pstart = GetPage(start);
            int pend   = GetPage(start + length - 1);

            for (int i = pstart; i <= pend; i++)
            {
                _pageData[i] = prot;
            }

            // TODO: restore the previous behavior where we would only reprotect a partial range
            ProtectAll();
        }
Beispiel #5
0
        /// <summary>take a snapshot of the entire memory block's contents, for use in <see cref="GetXorStream"/></summary>
        /// <exception cref="InvalidOperationException">
        /// snapshot already taken, <see cref="MemoryBlock.Active"/> is <see langword="false"/>, or failed to make memory read-only
        /// </exception>
        public void SaveXorSnapshot()
        {
            // note: The snapshot only holds up to the current committed size.  We compensate for that in xorstream
            if (_snapshot != null)
            {
                throw new InvalidOperationException("Snapshot already taken");
            }
            if (!Active)
            {
                throw new InvalidOperationException("Not active");
            }

            // temporarily switch the entire committed area to `R`: in case some areas are unreadable, we don't want
            // that to complicate things
            if (CommittedSize > 0)
            {
                _pal.Protect(Start, CommittedSize, Protection.R);
            }

            _snapshot = new byte[CommittedSize];
            var ds = new MemoryStream(_snapshot, true);
            var ss = GetStream(Start, CommittedSize, false);

            ss.CopyTo(ds);
            XorHash = WaterboxUtils.Hash(_snapshot);

            ProtectAll();
        }
Beispiel #6
0
        /// <summary>set r/w/x protection on a portion of memory. rounded to encompassing pages</summary
        /// <exception cref="InvalidOperationException">failed to protect memory</exception>
        public void Protect(ulong start, ulong length, Protection prot)
        {
            EnsureActive();
            if (length == 0)
            {
                return;
            }
            if (_sealed)
            {
                _pal.GetWriteStatus(_dirtydata, _pageData);
            }

            // Note: asking for prot.none on memory that was not previously committed, commits it

            var computedStart  = WaterboxUtils.AlignDown(start);
            var computedEnd    = WaterboxUtils.AlignUp(start + length);
            var computedLength = computedEnd - computedStart;

            // potentially commit more memory
            var minNewCommittedSize = computedEnd - Start;

            if (minNewCommittedSize > CommittedSize)
            {
                CommittedSize = minNewCommittedSize;
                // Since Commit() was called, we have to do a full ProtectAll -- remember that when refactoring
                _pal.Commit(CommittedSize);
            }

            int pstart = GetPage(start);
            int pend   = GetPage(start + length - 1);

            for (int i = pstart; i <= pend; i++)
            {
                _pageData[i] = prot;
                // inform the low level code what addresses might fault on it
                if (prot == Protection.RW || prot == Protection.RW_Stack)
                {
                    _dirtydata[i] |= WriteDetectionStatus.CanChange;
                }
                else
                {
                    _dirtydata[i] &= ~WriteDetectionStatus.CanChange;
                }
            }

            // TODO: restore the previous behavior where we would only reprotect a partial range
            ProtectAll();
            if (_sealed)
            {
                _pal.SetWriteStatus(_dirtydata);
            }
        }
Beispiel #7
0
 /// <summary>allocate <paramref name="size"/> bytes starting at a particular address <paramref name="start"/></summary>
 /// <exception cref="ArgumentOutOfRangeException"><paramref name="start"/> is not aligned or <paramref name="size"/> is <c>0</c></exception>
 protected MemoryBlockBase(ulong start, ulong size)
 {
     if (!WaterboxUtils.Aligned(start))
     {
         throw new ArgumentOutOfRangeException(nameof(start), start, "start address must be aligned");
     }
     if (size == 0)
     {
         throw new ArgumentOutOfRangeException(nameof(size), size, "cannot create 0-length block");
     }
     Size         = WaterboxUtils.AlignUp(size);
     AddressRange = start.RangeToExclusive(start + Size);
     _pageData    = new Protection[1 + GetPage(AddressRange.EndInclusive)];
 }
        /// <summary>set r/w/x protection on a portion of memory. rounded to encompassing pages</summary>
        /// <exception cref="InvalidOperationException">failed to protect memory</exception>
        public void Protect(ulong start, ulong length, Protection prot)
        {
            if (length == 0)
            {
                return;
            }

            // Note: asking for prot.none on memory that was not previously committed, commits it

            var computedStart  = WaterboxUtils.AlignDown(start);
            var computedEnd    = WaterboxUtils.AlignUp(start + length);
            var computedLength = computedEnd - computedStart;

            _pal.Protect(computedStart, computedLength, prot);
        }
Beispiel #9
0
        /// <summary>
        /// take a hash of the current full contents of the block, including unreadable areas
        /// but not uncommitted areas
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// <see cref="MemoryBlock.Active"/> is <see langword="false"/> or failed to make memory read-only
        /// </exception>
        public byte[] FullHash()
        {
            if (!Active)
            {
                throw new InvalidOperationException("Not active");
            }
            // temporarily switch the committed parts to `R` so we can read them
            if (CommittedSize > 0)
            {
                _pal.Protect(Start, CommittedSize, Protection.R);
            }
            var ret = WaterboxUtils.Hash(GetStream(Start, CommittedSize, false));

            ProtectAll();
            return(ret);
        }
Beispiel #10
0
        /// <exception cref="InvalidOperationException"><see cref="MemoryBlockBase.Active"/> is <see langword="false"/> or failed to make memory read-only</exception>
        public override byte[] FullHash()
        {
            if (!Active)
            {
                throw new InvalidOperationException("Not active");
            }
            // temporarily switch the entire block to `R`
            Kernel32.MemoryProtection old;
            if (!Kernel32.VirtualProtect(Z.UU(AddressRange.Start), Z.UU(Size), Kernel32.MemoryProtection.READONLY, out old))
            {
                throw new InvalidOperationException($"{nameof(Kernel32.VirtualProtect)}() returned FALSE!");
            }
            var ret = WaterboxUtils.Hash(GetStream(AddressRange.Start, Size, false));

            ProtectAll();
            return(ret);
        }
Beispiel #11
0
        /// <summary>allocate <paramref name="size"/> bytes</summary>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="size"/> is not aligned or is <c>0</c></exception>
        public MemoryBlock(ulong size)
        {
            if (!WaterboxUtils.Aligned(size))
            {
                throw new ArgumentOutOfRangeException(nameof(size), size, "size must be aligned");
            }
            if (size == 0)
            {
                throw new ArgumentOutOfRangeException(nameof(size), size, "cannot create 0-length block");
            }
            Size = WaterboxUtils.AlignUp(size);

            _pal = OSTailoredCode.IsUnixHost
                                ? (IMemoryBlockPal) new MemoryBlockLinuxPal(Size)
                                : new MemoryBlockWindowsPal(Size);
            Start        = _pal !.Start;
            EndExclusive = Start + Size;
        }
Beispiel #12
0
 public void Seal()
 {
     EnsureActive();
     if (_sealed)
     {
         throw new InvalidOperationException("Already sealed");
     }
     _snapshot = new byte[CommittedSize];
     if (CommittedSize > 0)
     {
         // temporarily switch the committed parts to `R` so we can read them
         _pal.Protect(Start, CommittedSize, Protection.R);
         Marshal.Copy(Z.US(Start), _snapshot, 0, (int)CommittedSize);
     }
     _hash   = WaterboxUtils.Hash(_snapshot);
     _sealed = true;
     ProtectAll();
     _pal.SetWriteStatus(_dirtydata);
 }
Beispiel #13
0
        /// <summary>set r/w/x protection on a portion of memory. rounded to encompassing pages</summary>
        /// <exception cref="InvalidOperationException">failed to protect memory</exception>
        /// <exception cref="ObjectDisposedException">disposed</exception>
        public void Protect(ulong start, ulong length, Protection prot)
        {
            if (_pal == null)
            {
                throw new ObjectDisposedException(nameof(MemoryBlock));
            }
            if (length == 0)
            {
                return;
            }

            // Note: asking for prot.none on memory that was not previously committed, commits it

            var computedStart  = WaterboxUtils.AlignDown(start);
            var computedEnd    = WaterboxUtils.AlignUp(start + length);
            var computedLength = computedEnd - computedStart;

            _pal.Protect(computedStart, computedLength, prot);
        }
Beispiel #14
0
        /// <summary>allocate <paramref name="size"/> bytes starting at a particular address <paramref name="start"/></summary>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="start"/> is not aligned or <paramref name="size"/> is <c>0</c></exception>
        public MemoryBlock(ulong start, ulong size)
        {
            if (!WaterboxUtils.Aligned(start))
            {
                throw new ArgumentOutOfRangeException(nameof(start), start, "start address must be aligned");
            }
            if (size == 0)
            {
                throw new ArgumentOutOfRangeException(nameof(size), size, "cannot create 0-length block");
            }
            Start        = start;
            Size         = WaterboxUtils.AlignUp(size);
            EndExclusive = Start + Size;
            _pageData    = new Protection[GetPage(EndExclusive - 1) + 1];

            _pal = OSTailoredCode.IsUnixHost
                                ? (IMemoryBlockPal) new MemoryBlockUnixPal(Start, Size)
                                : new MemoryBlockWindowsPal(Start, Size);
        }
Beispiel #15
0
        /// <summary>set r/w/x protection on a portion of memory. rounded to encompassing pages</summary
        /// <exception cref="InvalidOperationException">failed to protect memory</exception>
        public void Protect(ulong start, ulong length, Protection prot)
        {
            if (length == 0)
            {
                return;
            }

            // Note: asking for prot.none on memory that was not previously committed, commits it

            var computedStart  = WaterboxUtils.AlignDown(start);
            var computedEnd    = WaterboxUtils.AlignUp(start + length);
            var computedLength = computedEnd - computedStart;

            int pstart = GetPage(start);
            int pend   = GetPage(start + length - 1);

            for (int i = pstart; i <= pend; i++)
            {
                _pageData[i] = prot;                 // also store the value for later use
            }
            // potentially commit more memory
            var minNewCommittedSize = computedEnd - Start;

            if (minNewCommittedSize > CommittedSize)
            {
                CommittedSize = minNewCommittedSize;
            }

            if (Active)             // it's legal to Protect() if we're not active; the information is just saved for the next activation
            {
                if (CommittedSize > LastActiveCommittedSize)
                {
                    _pal.Commit(CommittedSize);
                    LastActiveCommittedSize = CommittedSize;
                    ProtectAll();
                }
                else
                {
                    _pal.Protect(computedStart, computedLength, prot);
                }
            }
        }
Beispiel #16
0
        /// <exception cref="InvalidOperationException"><see cref="MemoryBlockBase.Active"/> is <see langword="false"/> or failed to make memory read-only</exception>
        public override byte[] FullHash()
        {
            if (!Active)
            {
                throw new InvalidOperationException("Not active");
            }

            // temporarily switch the entire block to `R`
            var exitCode = mprotect(Z.US(AddressRange.Start), Z.UU(Size), MemoryProtection.Read);

            if (exitCode != 0)
            {
                throw new InvalidOperationException($"{nameof(mprotect)}() returned {exitCode}!");
            }

            var ret = WaterboxUtils.Hash(GetStream(AddressRange.Start, Size, false));

            ProtectAll();
            return(ret);
        }
Beispiel #17
0
        /// <summary>allocate <paramref name="size"/> bytes starting at a particular address <paramref name="start"/></summary>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="start"/> is not aligned or <paramref name="size"/> is <c>0</c></exception>
        public MemoryBlock(ulong start, ulong size)
        {
            if (!WaterboxUtils.Aligned(start))
            {
                throw new ArgumentOutOfRangeException(nameof(start), start, "start address must be aligned");
            }
            if (size == 0)
            {
                throw new ArgumentOutOfRangeException(nameof(size), size, "cannot create 0-length block");
            }
            if (start == 0)
            {
                throw new NotImplementedException("Start == 0 doesn't work right now, not really");
            }
            Start        = start;
            Size         = WaterboxUtils.AlignUp(size);
            EndExclusive = Start + Size;
            _pageData    = (Protection[])(object)new byte[GetPage(EndExclusive - 1) + 1];

            _pal = OSTailoredCode.IsUnixHost
                                ? (IMemoryBlockPal) new MemoryBlockLinuxPal(Start, Size)
                                : new MemoryBlockWindowsPal(Start, Size);
        }
Beispiel #18
0
        /// <exception cref="InvalidOperationException">snapshot already taken, <see cref="MemoryBlockBase.Active"/> is <see langword="false"/>, or failed to make memory read-only</exception>
        public override void SaveXorSnapshot()
        {
            if (_snapshot != null)
            {
                throw new InvalidOperationException("Snapshot already taken");
            }
            if (!Active)
            {
                throw new InvalidOperationException("Not active");
            }

            // temporarily switch the entire block to `R`: in case some areas are unreadable, we don't want that to complicate things
            var exitCode = mprotect(Z.US(AddressRange.Start), Z.UU(Size), MemoryProtection.Read);

            if (exitCode != 0)
            {
                throw new InvalidOperationException($"{nameof(mprotect)}() returned {exitCode}!");
            }

            _snapshot = new byte[Size];
            GetStream(AddressRange.Start, Size, false).CopyTo(new MemoryStream(_snapshot, true));
            XorHash = WaterboxUtils.Hash(_snapshot);
            ProtectAll();
        }
Beispiel #19
0
        public void LoadState(BinaryReader r)
        {
            EnsureActive();
            EnsureSealed();
            _pal.GetWriteStatus(_dirtydata, _pageData);

            if (r.ReadUInt64() != MAGIC || r.ReadUInt64() != Start || r.ReadUInt64() != Size)
            {
                throw new InvalidOperationException("Savestate internal mismatch");
            }
            if (!r.ReadBytes(_hash.Length).SequenceEqual(_hash))
            {
                // TODO: We'll probably have to allow this for romhackurz
                throw new InvalidOperationException("Waterbox consistency guarantee failed");
            }
            var newCommittedSize = r.ReadUInt64();

            if (newCommittedSize > CommittedSize)
            {
                _pal.Commit(newCommittedSize);
            }
            else if (newCommittedSize < CommittedSize)
            {
                // PAL layer won't let us shrink commits, but that's kind of OK
                var start = Start + newCommittedSize;
                var size  = CommittedSize - newCommittedSize;
                _pal.Protect(start, size, Protection.RW);
                WaterboxUtils.ZeroMemory(Z.US(start), (long)size);
                _pal.Protect(start, size, Protection.None);
            }
            CommittedSize = newCommittedSize;
            var newPageData  = (Protection[])(object)r.ReadBytes(_pageData.Length);
            var newDirtyData = (WriteDetectionStatus[])(object)r.ReadBytes(_dirtydata.Length);

            var   buff = new byte[4096];
            int   p;
            ulong addr;
            ulong endAddr = Start + CommittedSize;

            for (p = 0, addr = Start; addr < endAddr; p++, addr += 4096)
            {
                var dirty    = (_dirtydata[p] & WriteDetectionStatus.DidChange) != 0;
                var newDirty = (newDirtyData[p] & WriteDetectionStatus.DidChange) != 0;
                var inState  = newPageData[p] != Protection.RW_Invisible && newDirty;

                if (dirty || inState)
                {
                    // must write out changed data

                    // TODO: It's slow to toggle individual pages like this
                    if (_pageData[p] != Protection.RW)
                    {
                        _pal.Protect(addr, 4096, Protection.RW);
                    }

                    // NB: There are some weird behaviors possible if a block transitions to or from RW_Invisible,
                    // but nothing really "broken" (as far as I know); just reflecting the fact that you cannot track it.

                    if (inState)
                    {
                        // changed data comes from the savestate
                        r.Read(buff, 0, 4096);
                        Marshal.Copy(buff, 0, Z.US(addr), 4096);
                    }
                    else
                    {
                        // data comes from the snapshot
                        var offs = (int)(addr - Start);
                        if (offs < _snapshot.Length)
                        {
                            Marshal.Copy(_snapshot, offs, Z.US(addr), 4096);
                        }
                        else
                        {
                            // or was not in the snapshot at all, so had never been changed by seal
                            WaterboxUtils.ZeroMemory(Z.US(addr), 4096);
                        }
                    }
                }
            }
            _pageData  = newPageData;
            _dirtydata = newDirtyData;
            ProtectAll();
            _pal.SetWriteStatus(_dirtydata);
        }