Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/collector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Copy link
Member Author

@cgewecke cgewecke Sep 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hope this ok - the VM reports that both PUSH1 and PUSH2 are called with the same hash at the top of the stack, resulting in duplicate hits. This makes everything contingent on a very specific implementation detail at Solidity though...

const idx = info.stack.length - 1;
let hash = web3Utils.toHex(info.stack[idx]).toString();
hash = self._normalizeHash(hash);
Expand Down
131 changes: 76 additions & 55 deletions lib/injector.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,65 @@
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) {
this.hashCounter++;
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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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;
10 changes: 10 additions & 0 deletions lib/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] &&
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions test/sources/solidity/contracts/statements/stack-too-deep.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions test/units/statements.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 0 additions & 18 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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=

[email protected]:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
Expand Down Expand Up @@ -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"
Expand Down