Thoughts about Laurence Tratt’s Article “Can We Retain the Benefits of Transitive Dependencies Without Undermining Security?”

This is a comment on Laurence Tratt’s blog article “Can We Retain the Benefits of Transitive Dependencies Without Undermining Security?” discussed in the context of the Flow Design approach.

Indeed, it becomes more and more terrible to see how the transitive dependencies are becoming overwhelming (just think about the NPM transitive dependencies trees for non-trivial applications).

This is definitely a challenge for the future of programming and software development which needs to be tackled. And, I agree with your conclusion that some combination of privilege separation and compartmentalization (like actors) are needed at the end.

I’m part of a community which evaluates how software development can be thought completely different, seeing how often it results in a mess. Maybe, these ideas could also help in solving the transitive dependency hell (in the sense of security hell). And so, I’m throwing these ideas in the ring here.

If you are looking deeper for the reason where dependencies comes from, you will end up at subroutine calls. Yes, of course, finally subroutine calls are introducing any kind of dependency because this is what they are about: providing functionality by abstraction and introducing a relation between the caller the called one. How else could functionality be provided then through subroutines. Subroutines are such an old concept for abstraction that we would never question it. It was invented long before most of us were ever born. And, nobody is questioning this concept.
However, to find new approaches we, maybe, should do it!

It seems silly to question it. How to implement software, how to write programs without subroutines? How could programming look like without subroutines? Hard to imagine.

However, if you look closer to the actor model there is already a proposal to program functionality without subroutines – message passing. The actor model is following the message passing model and had a significant success with it regarding stability and resilience of applications written using this model.

But the actor model still introduces dependencies: An actor which wants to send a message is typically doing this by knowing the recipient of the message. That way, it has a direct dependency to that recipient actor. Finally, this is very similar to a subroutine call from dependency point of view.

It is worth to look back in the history (or just around ;-)): The Unix approach of connecting programs by pipes building pipelines of functionality is a model where no static dependencies exist between the programs in the pipeline. They do no call subroutines, we do not call each other! Moreover, this is the main principle and constraint of their implementation – each of them does not know, in context (pipeline) of which other programs it will be executed.

But from security point of view, each program is running in its own process which is well protected by the operating system. One process cannot leak into the other processes running before or behind them in the pipeline. The only way to influence on such an program is to send a messages to him over the pipe.

Why not incorporating this approach in general into programming?

The main idea here is to separate operational logic from integration logic. Building pipelines, connecting units of logic by pipes – that’s integration. Writing code with loops, conditions, and expressions – that’s operation. That could solve immediately two problems of modern software architectures:

First, it strictly implements the principle of separation of concern also for the aspects of integration and operational logic. Concerns that are almost not considered being different and having to be separated, because most of programming languages today are only providing subroutine calls as the only language concept to express both of them. So, you end up with code that do both, implementing logic with loops and expression mixed up with code integrating abstracted logic by calling subroutines. It is hard to reason about such code as it continuously changes the abstraction level from abstracted subroutines (AKA ubiquitous business logic terms) to low level language expression logic.

Second, it makes real reusing possible because this reusing scales. Functional units can be integrated at each level of abstraction. It does not matter what the real nature of the integrated functional unit is – integration unit or operational unit. It just gets integrated at the right level of abstraction because from integration point of view, there is not difference – everything is a functional unit.

Think of a Unix shell script which form a pipeline by calling other programs and other shell scripts connected by the pipe operator. And, which forward the input it receives from stdin into the pipeline it defines and forwards the output it gets from the pipeline to its stdout. This Unix shell script can be used itself in other pipelines inside other integrating shell scripts building a more abstract functionality. The Unix distributions are full of such examples, I guess.

Think that further as programming paradigm in a general purpose language: First, there are integration units as a language concept which are only for integration – connecting other units; no loops, conditions or other expressions allowed. Second, there are operation units as a language concept which must not have integration logic inside, means connecting other units. Only loops, conditions and expressions are allowed (Maybe, local private subroutines too. We won’t miss that abstraction concept completely. But these are local subroutines, only usable inside the same operational unit).

Both types of units define input and output pipes (Maybe, its better to call these input and output pipes “pins”. So, we could associate the units with integrated circuits which get integrated into electronic boards, which itself may get integrated into even bigger boards – a well known and working model in electronics and a good analogy). The units are running conceptually concurrently if needed, like actors do. An empowered runtime system can ensure that it scales and runs efficiently (e.g., as the virtual thread runtime system in Java 21 does it).

I tried to implement such an approach of programming concepts in Scala, as an internal DSL. Of course this is limited as no static code analysis is possible. Therefore it must be a language concept at the end!
The implementation can be found on GitHub with an detailed description in the readme: https://github.com/kuniss/ScalaFlow.

I even wrote an article about the idea in general: Go Beyond Object Oriented Design — Let it flow! .

But the original idea came from Ralf Westphal. It’s really worth to read his article series about it: https://radicalobjectorientation.substack.com/.

But back to the danger of transitive dependencies. Reading your article I think this approach could also help to defuse this danger.

Imagine, we have an operating system which provides us with threads which are protected against each other as OS processes are and which have an allocatable number of input and output pipes as the only way to get data into and out of these threads. Further, imagine we have a runtime system which maps the functional units with there input and outputs pins from above to these threads and pipes.

If a third party functionality is needed, in this approach it would be a functional unit which runs as such a protected thread as described above. The integration unit, integrating this third party unit, is defining and restricting what permission it may have (network access, file access, other access constraints). Dynamic link libraries as used today are not needed anymore in that scenario. So, their vulnerability cannot leak anymore into the program’s process.

That way the problem of dangerous transitive dependencies could really be tackled, I guess. This kind of operating system threads should probably be named differently — actors, maybe.

From my point of view, the actor system when deployed as its own operating system (I guess Erlang has this with its Erlang/OTP, see Erlang at Wikipedia) comes very close to that.

And, also the integration unit concept is already foreseen there, called the supervisor concept. But what is lacking in the Erlang’s runtime and programming paradigm (and of course in all actor frameworks) is the strict separation of integration and operation as a programming language concept and paradigm! But only that can provided the paradigm change which allows the security measures discussed above and requested by your article to get in place.

So finally, we need both: An operating system with actors as improved, secured and protected threads with pipes for input and output. And, a paradigm change with new programming language concepts implementing an appropriate runtime system. Both could improve the conceptual and the runtime architecture of software systems and how we look on them, significantly!

Leave a Reply

Your email address will not be published. Required fields are marked *

*