in Software Engineering

Climbing the call stack and tumbling back down

While consulting at Eurostar we wanted to understand how incoming requests flowed throughout our 20+ microservices and especially at what point throughout this chain of requests bugs arise. We had logging throughout our services but did not append a correlation id to the logs so we lacked visibility of the lifetime of a request to response. One way to solve the problem is to assign the user a correlation id on their first request and pass that to upstream services. Seems straight forward enough but passing down the correlation id to every call to `logger.error` wasn’t as clean as anticipated. Projects followed the repository pattern. Errors were being logged at the level of controller, service, and repo (that’s at least three levels deep) – we’d need to pass down the correlation id from the request in the controller, to the domain service, through to the repo if it were to be in scope when logging errors. There were up to a hundred functions which would need updating. Was there a way to do this without changing the signature of all these functions?

What’s unique throughout the lifecycle of a function invoked in JavaScript? The call stack.

If at whatever point in the codebase we could go up the call stack to the incoming request, then we could extract the correlation id from the request header.

JavaScript functions have access to an array-like object called arguments. Not only does arguments contain the arguments that the current function was called with but it also links to the function which invoked the current function, the caller function. We can inspect the arguments that this function which was invoked higher up the stack was called with. Consider the following chain of functions:


function first(req, res, next) {
second()
}

function second(){
third()
}

function third(){
// get access to the arguments when function first is invoked
}


first("A", "B", "C")

We can use the following function to go up the call stack and retrieve arguments from a function with a particular name:


function getArgsFromCallStack(functionName, maxStackSize){
let isSearching = true;
let ctx = arguments.callee // currently called function context
let args = null
let error = false
let stackSize = 0

while (isSearching) {
if (stackSize > maxStackSize) {
error = `maximum stack size (${maxStackSize}) exceeded`
isSearching = false
} else if (ctx.name === functionName) {
isSearching = false
args = […ctx.arguments] // shallow copy array arguments
} else if (typeof ctx.caller === “function”) {
ctx = ctx.caller // set the context to the function up the stack
stackSize += 1
} else {
isSearching = false
error = `function ${functionName} not found in call stack`
}
}


return {
error,
stackSize,
args
}
}

We can use this function to solve the earlier problem:


function first(req, res, next) {
second()
}

function second(){
third()
}

function third(){
console.log(getArgsFromCallStack(“first”, 50))
}

first(“A”, “B”, “C”)


// logs { error: false, stackSize: 3, args: [ 'A', 'B', 'C' ] }

Wonderful right? Almost. Running this code within our service resulted in the following error:

TypeError: ‘caller’, ‘callee’, and ‘arguments’ properties may not be accessed on strict mode functions or the arguments objects for calls to them

It turns out that strict mode prevents functions from accessing information about their caller. Most of the JavaScript files in our project were set to run in strict mode so this is solution is a no go. As noted on Mozilla caller is obsolete. Mulling it over, it became clear why.

The security hole

Suppose a JavaScript function has access to its caller’s arguments, the caller’s caller’s arguments, and so on until the top the stack is reached. What’s to prevent a devilish engineer creating a library which not only does what it claims to, but searches up the stack until a “login” function is called and post its arguments to a server?

In the end we simply passed the correlation id down from the controller to the domain service files. There’s likely a more elegant way of solving this problem but the side tangent which led to learning how arguments links functions, why it may be a security vulnerability, and how strict mode patches the potential hole was too good not to share in a post.