in Software Engineering

Climbing the call stack and tumbling back down

While working on a node.js service for Eurostar we wanted to append the incoming requests’ correlation id to the logs. We were able to do this at the level of the incoming request / outgoing response by way of middleware. Logging the correlation id whenever logger.error was invoked is a different beast however. The project 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.