From 9aa52f8ae666876ef1ce6a91b07f7c102b19d447 Mon Sep 17 00:00:00 2001 From: cgewecke Date: Thu, 5 Sep 2019 21:00:56 -0700 Subject: [PATCH] Instrument with injected function calls (#381) --- lib/collector.js | 2 +- lib/injector.js | 131 ++++++++++-------- lib/parse.js | 10 ++ package.json | 1 - .../contracts/statements/stack-too-deep.sol | 26 ++++ test/units/statements.js | 5 + yarn.lock | 18 --- 7 files changed, 118 insertions(+), 75 deletions(-) create mode 100644 test/sources/solidity/contracts/statements/stack-too-deep.sol diff --git a/lib/collector.js b/lib/collector.js index f052c50..ea0ccd7 100644 --- a/lib/collector.js +++ b/lib/collector.js @@ -9,7 +9,7 @@ class DataCollector { const self = this; if (typeof info !== 'object' || !info.opcode ) return; - if (info.opcode.name.includes("PUSH") && info.stack.length > 0){ + if (info.opcode.name.includes("PUSH1") && info.stack.length > 0){ const idx = info.stack.length - 1; let hash = web3Utils.toHex(info.stack[idx]).toString(); hash = self._normalizeHash(hash); diff --git a/lib/injector.js b/lib/injector.js index 434a399..b722ae5 100644 --- a/lib/injector.js +++ b/lib/injector.js @@ -1,20 +1,19 @@ -const sha1 = require("sha1"); const web3Utils = require("web3-utils"); class Injector { constructor(){ this.hashCounter = 0; - this.definitionCounter = 0; } - /** - * Generates solidity statement to inject for line, stmt, branch, fn 'events' - * @param {String} memoryVariable - * @param {String} hash hash key to an instrumentationData entry (see _getHash) - * @param {String} type instrumentation type, e.g. line, statement - // @return {String} ex: _sc_82e0891[0] = bytes32(0xdc08...08ed1); /* function */ - _getInjectable(memoryVariable, hash, type){ - return `${memoryVariable}[0] = bytes32(${hash}); /* ${type} */ \n`; + _split(contract, injectionPoint){ + return { + start: contract.instrumented.slice(0, injectionPoint), + end: contract.instrumented.slice(injectionPoint) + } + } + + _getInjectable(fileName, hash, type){ + return `${this._getMethodIdentifier(fileName)}(${hash}); /* ${type} */ \n`; } _getHash(fileName) { @@ -22,34 +21,45 @@ class Injector { return web3Utils.keccak256(`${fileName}:${this.hashCounter}`); } + _getMethodIdentifier(fileName){ + return `coverage_${web3Utils.keccak256(fileName).slice(0,10)}` + } + + _getInjectionComponents(contract, injectionPoint, fileName, type){ + const { start, end } = this._split(contract, injectionPoint); + const hash = this._getHash(fileName) + const injectable = this._getInjectable(fileName, hash, type); + + return { + start: start, + end: end, + hash: hash, + injectable: injectable + } + } + /** * Generates a solidity statement injection. Declared once per fn. * Definition is the same for every fn in file. * @param {String} fileName * @return {String} ex: bytes32[1] memory _sc_82e0891 */ - _getMemoryVariableDefinition(fileName){ - this.definitionCounter++; - return `\nbytes32[1] memory _sc_${sha1(fileName).slice(0,7)};\n`; - } - - _getMemoryVariableAssignment(fileName){ - return `\n_sc_${sha1(fileName).slice(0,7)}`; + _getHashMethodDefinition(fileName){ + const hash = web3Utils.keccak256(fileName).slice(0,10); + const method = this._getMethodIdentifier(fileName); + return `\nfunction ${method}(bytes32 c__${hash}) public pure {}\n`; } - injectLine(contract, fileName, injectionPoint, injection, instrumentation){ const type = 'line'; - const start = contract.instrumented.slice(0, injectionPoint); - const end = contract.instrumented.slice(injectionPoint); + const { start, end } = this._split(contract, injectionPoint); const newLines = start.match(/\n/g); const linecount = ( newLines || []).length + 1; contract.runnableLines.push(linecount); - const hash = this._getHash(fileName); - const memoryVariable = this._getMemoryVariableAssignment(fileName); - const injectable = this._getInjectable(memoryVariable, hash , type) + const hash = this._getHash(fileName) + const injectable = this._getInjectable(fileName, hash, type); instrumentation[hash] = { id: linecount, @@ -63,12 +73,13 @@ class Injector { injectStatement(contract, fileName, injectionPoint, injection, instrumentation) { const type = 'statement'; - const start = contract.instrumented.slice(0, injectionPoint); - const end = contract.instrumented.slice(injectionPoint); - const hash = this._getHash(fileName); - const memoryVariable = this._getMemoryVariableAssignment(fileName); - const injectable = this._getInjectable(memoryVariable, hash, type) + const { + start, + end, + hash, + injectable + } = this._getInjectionComponents(contract, injectionPoint, fileName, type); instrumentation[hash] = { id: injection.statementId, @@ -82,13 +93,13 @@ class Injector { injectFunction(contract, fileName, injectionPoint, injection, instrumentation){ const type = 'function'; - const start = contract.instrumented.slice(0, injectionPoint); - const end = contract.instrumented.slice(injectionPoint); - const hash = this._getHash(fileName); - const memoryVariableDefinition = this._getMemoryVariableDefinition(fileName); - const memoryVariable = this._getMemoryVariableAssignment(fileName); - const injectable = this._getInjectable(memoryVariable, hash, type); + const { + start, + end, + hash, + injectable + } = this._getInjectionComponents(contract, injectionPoint, fileName, type); instrumentation[hash] = { id: injection.fnId, @@ -97,17 +108,18 @@ class Injector { hits: 0 } - contract.instrumented = `${start}${memoryVariableDefinition}${injectable}${end}`; + contract.instrumented = `${start}${injectable}${end}`; } injectBranch(contract, fileName, injectionPoint, injection, instrumentation){ const type = 'branch'; - const start = contract.instrumented.slice(0, injectionPoint); - const end = contract.instrumented.slice(injectionPoint); - const hash = this._getHash(fileName); - const memoryVariable = this._getMemoryVariableAssignment(fileName); - const injectable = this._getInjectable(memoryVariable, hash, type); + const { + start, + end, + hash, + injectable + } = this._getInjectionComponents(contract, injectionPoint, fileName, type); instrumentation[hash] = { id: injection.branchId, @@ -122,12 +134,13 @@ class Injector { injectEmptyBranch(contract, fileName, injectionPoint, injection, instrumentation) { const type = 'branch'; - const start = contract.instrumented.slice(0, injectionPoint); - const end = contract.instrumented.slice(injectionPoint); - const hash = this._getHash(fileName); - const memoryVariable = this._getMemoryVariableAssignment(fileName); - const injectable = this._getInjectable(memoryVariable, hash, type); + const { + start, + end, + hash, + injectable + } = this._getInjectionComponents(contract, injectionPoint, fileName, type); instrumentation[hash] = { id: injection.branchId, @@ -142,12 +155,13 @@ class Injector { injectAssertPre(contract, fileName, injectionPoint, injection, instrumentation) { const type = 'assertPre'; - const start = contract.instrumented.slice(0, injectionPoint); - const end = contract.instrumented.slice(injectionPoint); - const hash = this._getHash(fileName); - const memoryVariable = this._getMemoryVariableAssignment(fileName); - const injectable = this._getInjectable(memoryVariable, hash, type); + const { + start, + end, + hash, + injectable + } = this._getInjectionComponents(contract, injectionPoint, fileName, type); instrumentation[hash] = { id: injection.branchId, @@ -161,12 +175,13 @@ class Injector { injectAssertPost(contract, fileName, injectionPoint, injection, instrumentation) { const type = 'assertPost'; - const start = contract.instrumented.slice(0, injectionPoint); - const end = contract.instrumented.slice(injectionPoint); - const hash = this._getHash(fileName); - const memoryVariable = this._getMemoryVariableAssignment(fileName); - const injectable = this._getInjectable(memoryVariable, hash, type); + const { + start, + end, + hash, + injectable + } = this._getInjectionComponents(contract, injectionPoint, fileName, type); instrumentation[hash] = { id: injection.branchId, @@ -177,6 +192,12 @@ class Injector { contract.instrumented = `${start}${injectable}${end}`; } + + injectHashMethod(contract, fileName, injectionPoint, injection, instrumentation){ + const start = contract.instrumented.slice(0, injectionPoint); + const end = contract.instrumented.slice(injectionPoint); + contract.instrumented = `${start}${this._getHashMethodDefinition(fileName)}${end}`; + } }; module.exports = Injector; diff --git a/lib/parse.js b/lib/parse.js index 5923f07..d2e71d2 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -51,6 +51,16 @@ parse.ContractDefinition = function(contract, expression) { }; parse.ContractOrLibraryStatement = function(contract, expression) { + // We need to define a method to pass coverage hashes into at top of each contract. + // This lets us get a fresh stack for the hash and avoid stack-too-deep errors. + const start = expression.range[0]; + const end = contract.instrumented.slice(expression.range[0]).indexOf('{') + 1; + const loc = start + end;; + + (contract.injectionPoints[loc]) + ? contract.injectionPoints[loc].push({ type: 'injectHashMethod'}) + : contract.injectionPoints[loc] = [{ type: 'injectHashMethod'}]; + if (expression.subNodes) { expression.subNodes.forEach(construct => { parse[construct.type] && diff --git a/package.json b/package.json index 512bed4..fd4a64c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "istanbul": "^0.4.5", "node-dir": "^0.1.17", "req-cwd": "^1.0.1", - "sha1": "^1.1.1", "shelljs": "^0.8.3", "solidity-parser-antlr": "^0.4.7", "web3": "1.2.1", diff --git a/test/sources/solidity/contracts/statements/stack-too-deep.sol b/test/sources/solidity/contracts/statements/stack-too-deep.sol new file mode 100644 index 0000000..d41b4d0 --- /dev/null +++ b/test/sources/solidity/contracts/statements/stack-too-deep.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.5.0; + +contract Test { + // 15 fn args + 1 local variable assignment + // will normally compile w/out stack too deep + // error. + function a( + uint _a, + uint _b, + uint _c, + uint _d, + uint _e, + uint _f, + uint _g, + uint _h, + uint _i, + uint _j, + uint _k, + uint _l, + uint _m, + uint _n, + uint _o + ) public { + uint x = _a; + } +} diff --git a/test/units/statements.js b/test/units/statements.js index 3f67909..c5e9e47 100644 --- a/test/units/statements.js +++ b/test/units/statements.js @@ -48,6 +48,11 @@ describe('generic statements', () => { util.report(info.solcOutput.errors); }); + it('should instrument without triggering stack-too-deep', () => { + const info = util.instrumentAndCompile('statements/stack-too-deep'); + util.report(info.solcOutput.errors); + }); + it('should NOT pass tests if the contract has a compilation error', () => { const info = util.instrumentAndCompile('app/SimpleError'); try { diff --git a/yarn.lock b/yarn.lock index 1a5b48d..fe2866d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1614,11 +1614,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -"charenc@>= 0.0.1": - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" @@ -2028,11 +2023,6 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -"crypt@>= 0.0.1": - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - crypto-browserify@3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -7379,14 +7369,6 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -sha1@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/sha1/-/sha1-1.1.1.tgz#addaa7a93168f393f19eb2b15091618e2700f848" - integrity sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg= - dependencies: - charenc ">= 0.0.1" - crypt ">= 0.0.1" - sha3@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/sha3/-/sha3-1.2.3.tgz#ed5958fa8331df1b1b8529ca9fdf225a340c5418"