Hello list,
Lately I have been starting to dive deeper into Common Lisp and have started to
use ABCL more than SBCL or CCL lately. It is a very impressive project.
I want to post a patch for review/comments and hopefully have it be worthwhile
to eventually include it in ABCL. The patch attempts to fix a couple of stack
inconsistency bugs in the compiler. I came across the stack inconsistency issue
in one of my projects and started to try to find the root cause of the problem
based on a nice minimal reproduction of the bug found in
https://github.com/armedbear/abcl/issues/69.
However, my particular bug was slightly different. It had to do with using a
return-from in the cleanup form of an unwind-protect. The following two forms
also result in a stack inconsistency problem (and are a more minimal
reproduction of the bug my code introduced):
(defun two-arg-fn (one two)
(format t "Two args: ~S and ~S~%" one two))
(let ((fn (compile nil '(lambda ()
(two-arg-fn
(block test-block
(unwind-protect
30
(return-from test-block 8)))
-1)))))
(funcall fn))
My patch handles both the github issue and the stack inconsistency in the form
above. It also fixes jvm::print-code to print string representations of values
from the constant pool which I found useful in debugging the output.
Anyhow, let me attempt to quickly summarize the stack inconsistency problem in
general:
- Certain common lisp control flow forms
(tagbody/go/unwind-protect/block/return-from/throw/catch) require the use
of JVM exceptions to implement in bytecode
- When the JVM throws an exception, the operand stack is cleared and the
exception is pushed onto the operand stack (see jvms8, 6.5/athrow, p378)
- Therefore, any form which pushes values onto the operand stack for
further use is confounded when these control flow forms are child
forms
- To properly handle these alternate control flows we need to save the
result of the control flow form to a local variable in the stack frame
(distinct from the operand stack, and not destroyed by an exception
(well at least not until the exception passes /out/ of the method)
and then reload for use by the parent form to push on the operand stack
Take the case of the ash function (from the github issue). Bytecode for ash is
emitted from jvm::p2-ash. It compiles its arguments to the operand stack, and is
therefore vulnerable to the problem discussed above. Other low level functions
(like + for example in p2-plus) use the following forms to overcome this issue:
jvm::with-operand-accumulation and jvm::compile-operand. These forms save the
results of "unsafe" forms (opstack unsafe) to "registers" (local variables in
the stack frame). This technique allows for these complicated control flow forms
to be a child form of + with no issues, but not the ash function (which does not
do this). See my patch for how I added these already present
with-operand-accumulation and compile-operand forms to ash so it is no longer
vulnerable to stack inconsistency bugs.
Generally, function calls in ABCL are not vulnerable to these stack
inconsistency bugs. Function arguments are processed in jvm::process-args, and
in this function, the opstack safety of child forms is checked, and values are
saved to "registers" when a form is known to be unsafe. My case (the return-from
in the cleanup form of an unwind-protect) simply wasn't properly being marked as
opstack unsafe. I modified jvm::p1-unwind-protect to mark all direct children of
the unwind-protect as opstack unsafe, which eliminated my problem. I believe
there may have been confusion here in the code (speculation of course on my
part) of 'protected' form referring to the form actually protected by the
unwind-protect (which is totally different than being unsafe or needing opstack
/protection/) (or I'm misinterpreting this function and potentially causing
additional bugs!).
The only other item in my patch is fixing how bytecode is printed for debugging.
Basically, most items in a class constant pool are referenced with a 2 byte
index, but one (ldc) uses a one byte index). This has been accounted for in the
new function jvm::constant-pool-index.
Let me know what you think, and again please review, there aren't many lines
changed but I am new to the internals of the project. I think I ran all tests
(ant abcl.test) but while the ant task completed successfully, my output
complained of a missing dependency and I'm not sure what actually ran.
-Mark
The attached patch was produced against b3cfee6617e0c2c380d8675f3383d81e7758f358
from https://github.com/easye/abcl (latest master branch).
[jvms8] https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf