Breaking Bash++: Undefined Behavior
I think you didn’t get a reply because you used the terms “correct” and “proper”, neither of which has much meaning in Perl culture. :-)
— Larry Wall
I will never claim that the approaches we’ve chosen for Bash++ are the “correct” way to do things – they’re just the approaches we’ve chosen. Personally, I don’t believe in “correctness” in programming – but take that with a grain of salt.
A few days ago, I put up a page on the Bash++ website about interoperability with Bash. In particular, about certain Bash features that we consider to have “undefined behavior” in Bash++. You might wonder why these features aren’t simply banned in Bash++ as a safety precaution. Well, maybe they should be – again I’m not willing to say that the route we’ve chosen is the correct route – but my philosophy has been to allow as much as possible.
This post isn’t meant to defend these decisions, just to talk about them and maybe to explain them.
My approach in designing Bash++ has been to try to create a small number of basic principles, and then to discover what the consequences of those principles are when put together. In cases where the consequences are very far from what we might want, we can then discuss changing the principles or adding new ones. But so far, speaking on my own behalf, I’ve been happy to meet Bash++ on its own terms.
Eval: An example of undefined behavior
In Bash++, using
eval
can lead to unexpected behavior, especially when dealing with objects and methods, because it bypasses the Bash++ compiler altogether. It may interfere with scope tracking, scope safety, object lifetimes, and method resolution.
— bpp-interoperability(3)
What might be the consequences of using eval
in Bash++? The most obvious one to me is that we can use it to completely bypass scope accessibility checks, as long as we can anticipate what the compiler will generate.
For example:
@class TestClass
@TestClass test
private_string_ptr="&@test" # Take the address of the object
private_string_ptr+="__privateString" # Append the deterministic address suffix of the private data member
eval "${private_string_ptr}=\"Modified Private String\"";
@test.printPrivateString # Prints "Modified Private String"
Here, we’re able to get full, unadulterated, read-write access to the private data member of the TestClass
object, even though it is marked as @private
, by anticipating the memory layout of the object and using eval
to modify it directly.
Of course, you can do exactly the same thing in C++:
#include <string>
#include <iostream>
class TestClass {
private:
std::string privateString = "Private String";
public:
void printPrivateString() {
std::cout << privateString << std::endl;
}
};
int main() {
TestClass test;
std::string* privateStringPtr = reinterpret_cast<std::string*>(&test);
// Modify the private string
*privateStringPtr = "Modified Private String";
test.printPrivateString(); // Outputs: Modified Private String
return 0;
}
Again, by anticipating what the compiler will generate, we can bypass the scope rules by exploiting our direct access to the program’s memory layout. This gets us full, unadulterated, read-write access to the private data member of the TestClass
object.
In the C++ case, we can anticipate that, because the privateString
member is the first member of the TestClass
object, it will be located at the start of the memory allocated for that object. Therefore we can take the address of the object and just cast it to a std::string*
to get a pointer to the privateString
member. If it wasn’t the first member, we could’ve incremented the address in the pointer by the size of all the previous members (and any padding that might have been added by the compiler) to get to the privateString
member.
The only real difference here is the architecture underlying the compiled program. C++ compiles to machine code which has direct access to memory, while Bash++ compiles to a shell script which is interpreted by the Bash shell. In the case of Bash++, a lot of things such as pointer arithmetic aren’t available to us, but there are alternatives to achieve the same effects, as long as we’re willing to accept the terms of the architecture we’re working with. This point is interesting enough to me to merit its own article, but for now, we’ll just mention it in passing.
In both cases, I would argue that these results are fundamental consequences of the language design, and not bugs. But again, maybe I’m wrong. Regardless, both languages allow you to do this, and both languages consider this undefined behavior.
It’s my feeling that allowing whatever consequences arise from the language design is a good thing, because it allows us to explore the full range of what the language can do. It also allows us to discover new ways of using the language that we might not have thought of otherwise.
A good example of this kind of thing, and something I didn’t anticipate initially, is a consequence of the fact that pointers are primitives:
@class Object
@class Container
@Container container
# Imagine we've populated the container with objects
@Object* obj=@(@container.getObject 0)
Here, a couple of things are made possible by the fact that pointers are primitives in Bash++:
- We can store pointers in a primitive array
- We can pass pointers as arguments to methods
- We can “return” pointers from methods by writing them to stdout
Bash doesn’t have “return types” – we can return exit codes, and we can write to some or other output stream, but we can’t return a value in the same way that C++ does. But because we allow pointers to be assigned any primitive value, we can assign the pointer to take the output of a method, and thereby “return” an object from a method.
This is a consequence of the language design that I didn’t anticipate initially, but which could easily inform a distinct and productive “style” of programming in Bash++. My feeling generally is that we should discover the language as it comes, and not try to force it into a mold that we think it should fit into.
But, again, I could easily be very wrong.