12

The commonly endorsed, and considered the most reliable, way of evaluating the security of a program is through examining its source code. That is, this method is based on the fundamental assumption: "what you see is what is run".

But if the program's memory has both 'writeable' and 'executable' attributes and the program can compile at run-time and execute arbitrary code, including the code that is not present in the sources - does this commonly used method of evaluating the program's security still hold?

Don't we have a tradeoff here - between security and performance? If we could achieve without compilation to machine code, say, 75% of performance that is achieved with JIT - would JIT still be considered a good option?

3
  • 35
    Secure against what? – user3840170 Jun 7 at 10:58
  • 3
    I voted to close this question as "unclear". There is no such thing as "secure". You have to provide a precise, unambiguous, complete, objective specification of what, exactly your threat model is. What are you securing? How much is it worth to you? How much is it worth to your attacker? What are the threats? What are the attack vectors? Who is your attacker? What resources do they have? What resources do you have? E.g. I can tell you that not using a JIT compiler does not secure you against someone kidnapping your loved ones and forcing you to reveal your secrets. – Jörg W Mittag Jun 8 at 5:20
  • If you haven't already, you should read Ken Thompson's Turing Award lecture, Reflections on Trusting Trust. It's very relevant to this question. – Ray Jun 9 at 13:28
29

JIT compilation is risky because of the W^X violation: at runtime, it is possible to generate new code, similar to an eval() in dynamic languages. But being able to dynamically generate executable machine code is not only essential to many high-performance runtimes including OpenJDK, .NET Runtime, and V8 – it's also super useful for malware.

However, such a risk doesn't mean a JIT compiler is inherently insecure. The important insight is that just because a JIT compiler can produce native code doesn't mean that this code can now do anything. The JIT compiler can introduce restrictions of its own (e.g. ensuring memory safety), and the code is still limited by the security models of the CPU and the operating system. Thus, JIT compilation is at the heart of many highly secure sandboxes such as V8 or BPF.

Also, not every W^X violation is equal. In a security-conscious program, all memory pages are either writeable or executable at any given time, but not both at the same time. A user-space JIT compiler will need to issue syscalls such as mprotect() to change the flags on a page, and these syscalls can be audited and possibly denied. A malware would either need to exploit a bug that introduces a page that is both writeable and executable, or would have to inject code into a writeable page that will later become executable. If the JIT compiler is written carefully – and the mentioned runtimes are incredibly robust and well-tested – such exploitable vulnerabilities will be quite rare.

There is definitely a tradeoff between security and performance. However, a security-conscious JIT compiler will not lead to a large loss of security. JIT can however lead to a large gain of performance. In my experience, interpreters are often 10× to 100× slower than native code, but this is highly dependent on the use case and on the granularity of the interpreter.

It is correct that JIT compilation makes static analysis on the level of machine code less useful. This might be unacceptable in some settings, for example in a certain app store that wants to review all the code. However, static analysis is inherently limited and often not suitable to provide strong security guarantees. Runtime checks that allowlist permissible operations and deny anything else are much more suitable to limit the behaviour of real-world programs. For example, a browser might sandbox untrusted code in a separate process in which JIT is allowed, but no interaction with the outside world except by sending messages to a supervisor process (e.g. enforcible by seccomp on Linux). Even if the sandbox runs malware, it will not be able to do anything that ordinary non-JIT code wasn't already able to do.

13
  • 4
    Dynamically generated code does not require a JIT, this is also possible in an interpreter environment, or in an environment where an AOT compiler is part of the environment. – Doc Brown Jun 7 at 8:54
  • 4
    It would be worth mentioning that the VM (JIT or interpreter) is relatively small and well-used, compared to the whole body of code executed with it. This means much more effort is concentrated on it, making it more efficient and secure. Especially as the executed code is restricted by design for safety and security, especially to ensure the integrity of the VM. – Deduplicator Jun 7 at 9:57
  • 11
    I don't see how a JIT makes static analysis any less useful than if you were to run the source code through a classic compiler. I'm sure everybody here has read the classic Dennis Ritchie paper on that topic. You have to trust your JIT to not introduce security errors, sure, but the same is true for your compiler. There have been bugs in both that have caused security critical problems. – Voo Jun 7 at 11:07
  • 1
    @Voo I've tried to clarify that section a bit. With a low-magic, non-JITting program, static analysis can provide interesting insights such as “this binary cannot issue the open() syscall”. Of course, Turing completeness limits what properties can be statically analyzed. Now without W^X, all such guarantees go out of the window because the program could load new executable code at a later time. Then, runtime enforcement is the only option. You are completely right though that JIT compilation is no less secure than AOT compilation, and that statical analysis of source code is unaffected. – amon Jun 7 at 13:36
  • 2
    @DocBrown Indeed back in the day people used to do dynamically generated code in assembly by modifying the machine instruction itself. The "trick" was known as self-modifying code (in the worst case people modified instructions in-place) and was used to reduce program size especially for various kinds of indexing operations. It was really hard to debug – slebetman Jun 8 at 6:53
8

For programs written in some high-level language, there is always an execution environment necessary. This can be a Just-in-Time compiler, an Ahead-of-Time compiler, an interpreter, or a combination of the former. CPUs can only execute machine code instructions, hence this kind of execution infrastructure is obligatory.

There is no such thing like running a program "without compilation/interpretation to machine code" - pretending this has something to do with performance, or using a JIT (as opposed to a different execution environment) is a fallacy.

For security audits, this means indeed

  • not only the program's code has to be evaluated, but also

  • the compiler's or interpreter's code requires a security evaluation, too.

For the latter, most organizations or people rely on the security inspections done by the vendor of the specific environment and/or the open source community. Hence, to lower the risk of security breaches, the best option is IMHO to use a widespread, popular, and mature environment, with no known severe security bugs and a vendor who has the reputation of fixing security breaches immediately when they become public. Which of the big commercial players or Open Source communities one trusts most is surely opinionated, but it is pretty independent of the type of environment they provide, JIT compiler, AOT compiler or interpreter.

4
  • Yes, the element of trust is necessary in the sphere of software. I was referring to a hypothetical situation when there are AOT, interpreter and JIT from highly trusted vendors - the JIT technology seems more prone to exploits. – Al Berger Jun 7 at 9:42
  • @AlBerger: in your question you wrote "If we could achieve without compilation to machine code ..." - that does definitely not read like you had AOT compilers in mind, more like some magic to run programs without any kind of translation to machine code. – Doc Brown Jun 7 at 9:50
  • 7
    There is a subtlety that this glosses over, which is that in an ahead-of-time compiled application, the compilation likely happens in a purely defender-controlled environment, whereas just-in-time compilation may happen in a partly attacker-controlled environment. This adds surface area, and means that security issues with the compiler that wouldn't matter ahead-of-time now matter. Maturity still counts, and depending on the threat model, the surface area may not matter, but it's an apples to oranges comparison if you simply compare the security of the generated code. – James_pic Jun 7 at 11:48
  • I think the OP referred specifically to the fact that an already compiled program can be and stay in read-only memory while a JIT by necessity must make the actually executing machine code writable, thus lowering the guards. amon addressed this by pointing out that this can be mitigated by making memory pages writable on demand and with some kind of permission. – Peter - Reinstate Monica Jun 8 at 12:31
2

One, we don’t achieve 75% of performance without JIT. Nowhere near. More like 10% or less.

On MacOS or iOS, JIT compilation means some pages are changed to writable / non executable, compiled code is written to these pages, then they are changed to read only / executable and THEN the compiled code can be examined before running it. I suppose that any decent OS will work that way. Executable pages are never writable at the same time.

4
  • "Nowhere near" - With existing technologies - yes. But there is for sure a plenty room for improvement. E.g., a prototype of a new language called Transd uses a new approach in interpretation technics: the decentralized computation based on a network of finite automata, instead of using the execution engine with the main loop. And its preliminary performance results seem better than 10%: github.com/transd-lang/graph. But, thanks for the answer. – Al Berger Jun 7 at 7:51
  • 3
    @AlBerger And I have seen interpreted Perl programs outperform C code, but that is because some problems might harmonize well with the interpreter's opcodes so that interpretation overhead becomes negligible. A single benchmark is unconvincing and is not evidence of a general improvement. In any case, I can't find a description of the Transd execution model (obfuscated source doesn't count) and can't seem to find reproducible benchmark results. No idea how “network of finite automata” are supposed to work. – amon Jun 7 at 8:36
  • @amon The code is not obfuscated, it's "minimized" in order to keep the implementation in a single source file (compactness of embeddable library is one of the features of the language). Also there is a test suite on github, which at the moment contains 17 files, some of which can be used for evaluating the performance (the parameters in files can be tweaked). It's still a prototype, some things may not be working, but it's intensively being readied for the beta-version, which is expected this summer. – Al Berger Jun 7 at 9:25
  • 3
    Amon, in “interpreted” Perl programs the interpreter just decides which library functions to call, which are mostly optimised C or C++ code. – gnasher729 Jun 7 at 10:05
0

It is an extra line of defense that helps against weakly prepared attacks. These attacks make the majority.

However it has been really many security breaches in these JIT compilers over the history. Never run JIT as root.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.