/// <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(); }
/// <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}!"); } }
/// <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!"); } } }
/// <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(); }
/// <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(); }
/// <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); } }
/// <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); }
/// <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); }
/// <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); }
/// <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; }
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); }
/// <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); }
/// <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); }
/// <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); } } }
/// <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); }
/// <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); }
/// <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(); }
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); }