| OLD | NEW |
| 1 /* Copyright (c) 2007, Google Inc. | 1 /* Copyright (c) 2007, Google Inc. |
| 2 * All rights reserved. | 2 * All rights reserved. |
| 3 * | 3 * |
| 4 * Redistribution and use in source and binary forms, with or without | 4 * Redistribution and use in source and binary forms, with or without |
| 5 * modification, are permitted provided that the following conditions are | 5 * modification, are permitted provided that the following conditions are |
| 6 * met: | 6 * met: |
| 7 * | 7 * |
| 8 * * Redistributions of source code must retain the above copyright | 8 * * Redistributions of source code must retain the above copyright |
| 9 * notice, this list of conditions and the following disclaimer. | 9 * notice, this list of conditions and the following disclaimer. |
| 10 * * Redistributions in binary form must reproduce the above | 10 * * Redistributions in binary form must reproduce the above |
| (...skipping 11 matching lines...) Expand all Loading... |
| 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 * | 29 * |
| 30 * --- | 30 * --- |
| 31 * Author: Joi Sigurdsson | 31 * Author: Joi Sigurdsson |
| 32 * Author: Scott Francis |
| 32 * | 33 * |
| 33 * Implementation of PreamblePatcher | 34 * Implementation of PreamblePatcher |
| 34 */ | 35 */ |
| 35 | 36 |
| 36 #include "preamble_patcher.h" | 37 #include "preamble_patcher.h" |
| 37 | 38 |
| 38 #include "mini_disassembler.h" | 39 #include "mini_disassembler.h" |
| 39 | 40 |
| 40 // Definitions of assembly statements we need | 41 // Definitions of assembly statements we need |
| 41 #define ASM_JMP32REL 0xE9 | 42 #define ASM_JMP32REL 0xE9 |
| 42 #define ASM_INT3 0xCC | 43 #define ASM_INT3 0xCC |
| 44 #define ASM_NOP 0x90 |
| 45 // X64 opcodes |
| 46 #define ASM_MOVRAX_IMM 0xB8 |
| 47 #define ASM_REXW 0x48 |
| 48 #define ASM_JMP 0xFF |
| 49 #define ASM_JMP_RAX 0xE0 |
| 50 #define ASM_PUSH 0x68 |
| 51 #define ASM_RET 0xC3 |
| 43 | 52 |
| 44 namespace sidestep { | 53 namespace sidestep { |
| 45 | 54 |
| 46 SideStepError PreamblePatcher::RawPatchWithStub( | 55 SideStepError PreamblePatcher::RawPatchWithStub( |
| 47 void* target_function, | 56 void* target_function, |
| 48 void *replacement_function, | 57 void* replacement_function, |
| 49 unsigned char* preamble_stub, | 58 unsigned char* preamble_stub, |
| 50 unsigned long stub_size, | 59 unsigned long stub_size, |
| 51 unsigned long* bytes_needed) { | 60 unsigned long* bytes_needed) { |
| 52 if ((NULL == target_function) || | 61 if ((NULL == target_function) || |
| 53 (NULL == replacement_function) || | 62 (NULL == replacement_function) || |
| 54 (NULL == preamble_stub)) { | 63 (NULL == preamble_stub)) { |
| 55 SIDESTEP_ASSERT(false && | 64 SIDESTEP_ASSERT(false && |
| 56 "Invalid parameters - either pTargetFunction or " | 65 "Invalid parameters - either pTargetFunction or " |
| 57 "pReplacementFunction or pPreambleStub were NULL."); | 66 "pReplacementFunction or pPreambleStub were NULL."); |
| 58 return SIDESTEP_INVALID_PARAMETER; | 67 return SIDESTEP_INVALID_PARAMETER; |
| 59 } | 68 } |
| 60 | 69 |
| 61 // TODO(V7:joi) Siggi and I just had a discussion and decided that both | 70 // TODO(V7:joi) Siggi and I just had a discussion and decided that both |
| 62 // patching and unpatching are actually unsafe. We also discussed a | 71 // patching and unpatching are actually unsafe. We also discussed a |
| 63 // method of making it safe, which is to freeze all other threads in the | 72 // method of making it safe, which is to freeze all other threads in the |
| 64 // process, check their thread context to see if their eip is currently | 73 // process, check their thread context to see if their eip is currently |
| 65 // inside the block of instructions we need to copy to the stub, and if so | 74 // inside the block of instructions we need to copy to the stub, and if so |
| 66 // wait a bit and try again, then unfreeze all threads once we've patched. | 75 // wait a bit and try again, then unfreeze all threads once we've patched. |
| 67 // Not implementing this for now since we're only using SideStep for unit | 76 // Not implementing this for now since we're only using SideStep for unit |
| 68 // testing, but if we ever use it for production code this is what we | 77 // testing, but if we ever use it for production code this is what we |
| 69 // should do. | 78 // should do. |
| 70 // | 79 // |
| 71 // NOTE: Stoyan suggests we can write 8 or even 10 bytes atomically using | 80 // NOTE: Stoyan suggests we can write 8 or even 10 bytes atomically using |
| 72 // FPU instructions, and on newer processors we could use cmpxchg8b or | 81 // FPU instructions, and on newer processors we could use cmpxchg8b or |
| 73 // cmpxchg16b. So it might be possible to do the patching/unpatching | 82 // cmpxchg16b. So it might be possible to do the patching/unpatching |
| 74 // atomically and avoid having to freeze other threads. Note though, that | 83 // atomically and avoid having to freeze other threads. Note though, that |
| 75 // doing it atomically does not help if one of the other threads happens | 84 // doing it atomically does not help if one of the other threads happens |
| 76 // to have its eip in the middle of the bytes you change while you change | 85 // to have its eip in the middle of the bytes you change while you change |
| 77 // them. | 86 // them. |
| 87 unsigned char* target = reinterpret_cast<unsigned char*>(target_function); |
| 88 unsigned int required_trampoline_bytes = 0; |
| 89 const unsigned int kRequiredStubJumpBytes = 5; |
| 90 const unsigned int kRequiredTargetPatchBytes = 5; |
| 78 | 91 |
| 79 // First, deal with a special case that we see with functions that | 92 // Initialize the stub with INT3's just in case. |
| 80 // point into an IAT table (including functions linked statically | 93 if (stub_size) { |
| 81 // into the application): these function already starts with | 94 memset(preamble_stub, 0xcc, stub_size); |
| 82 // ASM_JMP32REL. For instance, malloc() might be implemented as a | |
| 83 // JMP to __malloc(). In that case, we replace the destination of | |
| 84 // the JMP (__malloc), rather than the JMP itself (malloc). This | |
| 85 // way we get the correct behavior no matter how malloc gets called. | |
| 86 void *new_target = ResolveTarget(target_function); | |
| 87 if (new_target != target_function) { // we're in the IAT case | |
| 88 // I'd like to just say "target = new_target", but I can't, | |
| 89 // because the new target will need to have its protections set. | |
| 90 return RawPatchWithStubAndProtections(new_target, replacement_function, | |
| 91 preamble_stub, stub_size, | |
| 92 bytes_needed); | |
| 93 } | 95 } |
| 94 unsigned char* target = reinterpret_cast<unsigned char*>(new_target); | 96 if (kIs64BitBinary) { |
| 97 // In 64-bit mode JMP instructions are always relative to RIP. If the |
| 98 // replacement - target offset is > 2GB, we can't JMP to the replacement |
| 99 // function. In this case, we're going to use a trampoline - that is, |
| 100 // we're going to do a relative jump to a small chunk of code in the stub |
| 101 // that will then do the absolute jump to the replacement function. By |
| 102 // doing this, we only need to patch 5 bytes in the target function, as |
| 103 // opposed to patching 12 bytes if we were to do an absolute jump. |
| 104 // |
| 105 // Note that the first byte of the trampoline is a NOP instruction. This |
| 106 // is used as a trampoline signature that will be detected when unpatching |
| 107 // the function. |
| 108 // |
| 109 // jmp <trampoline> |
| 110 // |
| 111 // trampoline: |
| 112 // nop |
| 113 // mov rax, <replacement_function> |
| 114 // jmp rax |
| 115 // |
| 116 __int64 replacement_target_offset = reinterpret_cast<__int64>( |
| 117 replacement_function) - reinterpret_cast<__int64>(target) - 5; |
| 118 if (replacement_target_offset > INT_MAX |
| 119 || replacement_target_offset < INT_MIN) { |
| 120 // The stub needs to be within 2GB of the target for the trampoline to |
| 121 // work! |
| 122 __int64 trampoline_offset = reinterpret_cast<__int64>(preamble_stub) |
| 123 - reinterpret_cast<__int64>(target) - 5; |
| 124 if (trampoline_offset > INT_MAX || trampoline_offset < INT_MIN) { |
| 125 // We're screwed. |
| 126 SIDESTEP_ASSERT(false |
| 127 && "Preamble stub is too far from target to patch."); |
| 128 return SIDESTEP_UNEXPECTED; |
| 129 } |
| 130 required_trampoline_bytes = 13; |
| 131 } |
| 132 } |
| 95 | 133 |
| 96 // Let's disassemble the preamble of the target function to see if we can | 134 // Let's disassemble the preamble of the target function to see if we can |
| 97 // patch, and to see how much of the preamble we need to take. We need 5 | 135 // patch, and to see how much of the preamble we need to take. We need 5 |
| 98 // bytes for our jmp instruction, so let's find the minimum number of | 136 // bytes for our jmp instruction, so let's find the minimum number of |
| 99 // instructions to get 5 bytes. | 137 // instructions to get 5 bytes. |
| 100 MiniDisassembler disassembler; | 138 MiniDisassembler disassembler; |
| 101 unsigned int preamble_bytes = 0; | 139 unsigned int preamble_bytes = 0; |
| 102 while (preamble_bytes < 5) { | 140 unsigned int stub_bytes = 0; |
| 141 while (preamble_bytes < kRequiredTargetPatchBytes) { |
| 142 unsigned int cur_bytes = 0; |
| 103 InstructionType instruction_type = | 143 InstructionType instruction_type = |
| 104 disassembler.Disassemble(target + preamble_bytes, preamble_bytes); | 144 disassembler.Disassemble(target + preamble_bytes, cur_bytes); |
| 105 if (IT_JUMP == instruction_type) { | 145 if (IT_JUMP == instruction_type) { |
| 106 SIDESTEP_ASSERT(false && | 146 unsigned int jump_bytes = 0; |
| 107 "Unable to patch because there is a jump instruction " | 147 SideStepError jump_ret = SIDESTEP_JUMP_INSTRUCTION; |
| 108 "in the first 5 bytes."); | 148 if (IsShortConditionalJump(target + preamble_bytes, cur_bytes)) { |
| 109 return SIDESTEP_JUMP_INSTRUCTION; | 149 jump_ret = PatchShortConditionalJump(target + preamble_bytes, cur_bytes, |
| 150 preamble_stub + stub_bytes, |
| 151 &jump_bytes, |
| 152 stub_size - stub_bytes); |
| 153 } else if (IsNearConditionalJump(target + preamble_bytes, cur_bytes) || |
| 154 IsNearRelativeJump(target + preamble_bytes, cur_bytes) || |
| 155 IsNearAbsoluteCall(target + preamble_bytes, cur_bytes) || |
| 156 IsNearRelativeCall(target + preamble_bytes, cur_bytes)) { |
| 157 jump_ret = PatchNearJumpOrCall(target + preamble_bytes, cur_bytes, |
| 158 preamble_stub + stub_bytes, &jump_bytes, |
| 159 stub_size - stub_bytes); |
| 160 } |
| 161 if (jump_ret != SIDESTEP_SUCCESS) { |
| 162 SIDESTEP_ASSERT(false && |
| 163 "Unable to patch because there is an unhandled branch " |
| 164 "instruction in the initial preamble bytes."); |
| 165 return SIDESTEP_JUMP_INSTRUCTION; |
| 166 } |
| 167 stub_bytes += jump_bytes; |
| 110 } else if (IT_RETURN == instruction_type) { | 168 } else if (IT_RETURN == instruction_type) { |
| 111 SIDESTEP_ASSERT(false && | 169 SIDESTEP_ASSERT(false && |
| 112 "Unable to patch because function is too short"); | 170 "Unable to patch because function is too short"); |
| 113 return SIDESTEP_FUNCTION_TOO_SMALL; | 171 return SIDESTEP_FUNCTION_TOO_SMALL; |
| 114 } else if (IT_GENERIC != instruction_type) { | 172 } else if (IT_GENERIC == instruction_type) { |
| 173 if (IsMovWithDisplacement(target + preamble_bytes, cur_bytes)) { |
| 174 unsigned int mov_bytes = 0; |
| 175 if (PatchMovWithDisplacement(target + preamble_bytes, cur_bytes, |
| 176 preamble_stub + stub_bytes, &mov_bytes, |
| 177 stub_size - stub_bytes) |
| 178 != SIDESTEP_SUCCESS) { |
| 179 return SIDESTEP_UNSUPPORTED_INSTRUCTION; |
| 180 } |
| 181 stub_bytes += mov_bytes; |
| 182 } else { |
| 183 memcpy(reinterpret_cast<void*>(preamble_stub + stub_bytes), |
| 184 reinterpret_cast<void*>(target + preamble_bytes), cur_bytes); |
| 185 stub_bytes += cur_bytes; |
| 186 } |
| 187 } else { |
| 115 SIDESTEP_ASSERT(false && | 188 SIDESTEP_ASSERT(false && |
| 116 "Disassembler encountered unsupported instruction " | 189 "Disassembler encountered unsupported instruction " |
| 117 "(either unused or unknown"); | 190 "(either unused or unknown"); |
| 118 return SIDESTEP_UNSUPPORTED_INSTRUCTION; | 191 return SIDESTEP_UNSUPPORTED_INSTRUCTION; |
| 119 } | 192 } |
| 193 preamble_bytes += cur_bytes; |
| 120 } | 194 } |
| 121 | 195 |
| 122 if (NULL != bytes_needed) | 196 if (NULL != bytes_needed) |
| 123 *bytes_needed = preamble_bytes + 5; | 197 *bytes_needed = stub_bytes + kRequiredStubJumpBytes |
| 198 + required_trampoline_bytes; |
| 124 | 199 |
| 125 // Inv: cbPreamble is the number of bytes (at least 5) that we need to take | 200 // Inv: cbPreamble is the number of bytes (at least 5) that we need to take |
| 126 // from the preamble to have whole instructions that are 5 bytes or more | 201 // from the preamble to have whole instructions that are 5 bytes or more |
| 127 // in size total. The size of the stub required is cbPreamble + size of | 202 // in size total. The size of the stub required is cbPreamble + |
| 128 // jmp (5) | 203 // kRequiredStubJumpBytes (5) + required_trampoline_bytes (0 or 13) |
| 129 if (preamble_bytes + 5 > stub_size) { | 204 if (stub_bytes + kRequiredStubJumpBytes + required_trampoline_bytes |
| 205 > stub_size) { |
| 130 SIDESTEP_ASSERT(false); | 206 SIDESTEP_ASSERT(false); |
| 131 return SIDESTEP_INSUFFICIENT_BUFFER; | 207 return SIDESTEP_INSUFFICIENT_BUFFER; |
| 132 } | 208 } |
| 133 | 209 |
| 134 // First, copy the preamble that we will overwrite. | |
| 135 memcpy(reinterpret_cast<void*>(preamble_stub), | |
| 136 reinterpret_cast<void*>(target), preamble_bytes); | |
| 137 | |
| 138 // Now, make a jmp instruction to the rest of the target function (minus the | 210 // Now, make a jmp instruction to the rest of the target function (minus the |
| 139 // preamble bytes we moved into the stub) and copy it into our preamble-stub. | 211 // preamble bytes we moved into the stub) and copy it into our preamble-stub. |
| 140 // find address to jump to, relative to next address after jmp instruction | 212 // find address to jump to, relative to next address after jmp instruction |
| 141 #ifdef _MSC_VER | 213 #ifdef _MSC_VER |
| 142 #pragma warning(push) | 214 #pragma warning(push) |
| 143 #pragma warning(disable:4244) | 215 #pragma warning(disable:4244) |
| 144 #endif | 216 #endif |
| 145 int relative_offset_to_target_rest | 217 int relative_offset_to_target_rest |
| 146 = ((reinterpret_cast<unsigned char*>(target) + preamble_bytes) - | 218 = ((reinterpret_cast<unsigned char*>(target) + preamble_bytes) - |
| 147 (preamble_stub + preamble_bytes + 5)); | 219 (preamble_stub + stub_bytes + kRequiredStubJumpBytes)); |
| 148 #ifdef _MSC_VER | 220 #ifdef _MSC_VER |
| 149 #pragma warning(pop) | 221 #pragma warning(pop) |
| 150 #endif | 222 #endif |
| 151 // jmp (Jump near, relative, displacement relative to next instruction) | 223 // jmp (Jump near, relative, displacement relative to next instruction) |
| 152 preamble_stub[preamble_bytes] = ASM_JMP32REL; | 224 preamble_stub[stub_bytes] = ASM_JMP32REL; |
| 153 // copy the address | 225 // copy the address |
| 154 memcpy(reinterpret_cast<void*>(preamble_stub + preamble_bytes + 1), | 226 memcpy(reinterpret_cast<void*>(preamble_stub + stub_bytes + 1), |
| 155 reinterpret_cast<void*>(&relative_offset_to_target_rest), 4); | 227 reinterpret_cast<void*>(&relative_offset_to_target_rest), 4); |
| 156 | 228 |
| 229 if (kIs64BitBinary && required_trampoline_bytes != 0) { |
| 230 // Construct the trampoline |
| 231 unsigned int trampoline_pos = stub_bytes + kRequiredStubJumpBytes; |
| 232 preamble_stub[trampoline_pos] = ASM_NOP; |
| 233 preamble_stub[trampoline_pos + 1] = ASM_REXW; |
| 234 preamble_stub[trampoline_pos + 2] = ASM_MOVRAX_IMM; |
| 235 memcpy(reinterpret_cast<void*>(preamble_stub + trampoline_pos + 3), |
| 236 reinterpret_cast<void*>(&replacement_function), |
| 237 sizeof(void *)); |
| 238 preamble_stub[trampoline_pos + 11] = ASM_JMP; |
| 239 preamble_stub[trampoline_pos + 12] = ASM_JMP_RAX; |
| 240 |
| 241 // Now update replacement_function to point to the trampoline |
| 242 replacement_function = preamble_stub + trampoline_pos; |
| 243 } |
| 244 |
| 157 // Inv: preamble_stub points to assembly code that will execute the | 245 // Inv: preamble_stub points to assembly code that will execute the |
| 158 // original function by first executing the first cbPreamble bytes of the | 246 // original function by first executing the first cbPreamble bytes of the |
| 159 // preamble, then jumping to the rest of the function. | 247 // preamble, then jumping to the rest of the function. |
| 160 | 248 |
| 161 // Overwrite the first 5 bytes of the target function with a jump to our | 249 // Overwrite the first 5 bytes of the target function with a jump to our |
| 162 // replacement function. | 250 // replacement function. |
| 163 // (Jump near, relative, displacement relative to next instruction) | 251 // (Jump near, relative, displacement relative to next instruction) |
| 164 target[0] = ASM_JMP32REL; | 252 target[0] = ASM_JMP32REL; |
| 165 | 253 |
| 166 // Find offset from instruction after jmp, to the replacement function. | 254 // Find offset from instruction after jmp, to the replacement function. |
| 167 #ifdef _MSC_VER | 255 #ifdef _MSC_VER |
| 168 #pragma warning(push) | 256 #pragma warning(push) |
| 169 #pragma warning(disable:4244) | 257 #pragma warning(disable:4244) |
| 170 #endif | 258 #endif |
| 171 int offset_to_replacement_function = | 259 int offset_to_replacement_function = |
| 172 reinterpret_cast<unsigned char*>(replacement_function) - | 260 reinterpret_cast<unsigned char*>(replacement_function) - |
| 173 reinterpret_cast<unsigned char*>(target) - 5; | 261 reinterpret_cast<unsigned char*>(target) - 5; |
| 174 #ifdef _MSC_VER | 262 #ifdef _MSC_VER |
| 175 #pragma warning(pop) | 263 #pragma warning(pop) |
| 176 #endif | 264 #endif |
| 177 // complete the jmp instruction | 265 // complete the jmp instruction |
| 178 memcpy(reinterpret_cast<void*>(target + 1), | 266 memcpy(reinterpret_cast<void*>(target + 1), |
| 179 reinterpret_cast<void*>(&offset_to_replacement_function), 4); | 267 reinterpret_cast<void*>(&offset_to_replacement_function), 4); |
| 268 |
| 180 // Set any remaining bytes that were moved to the preamble-stub to INT3 so | 269 // Set any remaining bytes that were moved to the preamble-stub to INT3 so |
| 181 // as not to cause confusion (otherwise you might see some strange | 270 // as not to cause confusion (otherwise you might see some strange |
| 182 // instructions if you look at the disassembly, or even invalid | 271 // instructions if you look at the disassembly, or even invalid |
| 183 // instructions). Also, by doing this, we will break into the debugger if | 272 // instructions). Also, by doing this, we will break into the debugger if |
| 184 // some code calls into this portion of the code. If this happens, it | 273 // some code calls into this portion of the code. If this happens, it |
| 185 // means that this function cannot be patched using this patcher without | 274 // means that this function cannot be patched using this patcher without |
| 186 // further thought. | 275 // further thought. |
| 187 if (preamble_bytes > 5) { | 276 if (preamble_bytes > kRequiredTargetPatchBytes) { |
| 188 memset(reinterpret_cast<void*>(target + 5), ASM_INT3, preamble_bytes - 5); | 277 memset(reinterpret_cast<void*>(target + kRequiredTargetPatchBytes), |
| 278 ASM_INT3, preamble_bytes - kRequiredTargetPatchBytes); |
| 189 } | 279 } |
| 190 | 280 |
| 191 // Inv: The memory pointed to by target_function now points to a relative | 281 // Inv: The memory pointed to by target_function now points to a relative |
| 192 // jump instruction that jumps over to the preamble_stub. The preamble | 282 // jump instruction that jumps over to the preamble_stub. The preamble |
| 193 // stub contains the first stub_size bytes of the original target | 283 // stub contains the first stub_size bytes of the original target |
| 194 // function's preamble code, followed by a relative jump back to the next | 284 // function's preamble code, followed by a relative jump back to the next |
| 195 // instruction after the first cbPreamble bytes. | 285 // instruction after the first cbPreamble bytes. |
| 196 | 286 // |
| 287 // In 64-bit mode the memory pointed to by target_function *may* point to a |
| 288 // relative jump instruction that jumps to a trampoline which will then |
| 289 // perform an absolute jump to the replacement function. The preamble stub |
| 290 // still contains the original target function's preamble code, followed by a |
| 291 // jump back to the instructions after the first preamble bytes. |
| 292 // |
| 197 return SIDESTEP_SUCCESS; | 293 return SIDESTEP_SUCCESS; |
| 198 } | 294 } |
| 199 | 295 |
| 200 }; // namespace sidestep | 296 }; // namespace sidestep |
| OLD | NEW |