This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Imque's profile picture

Top Level

Started by Imque, 18 April 2013 - 01:06 AM
Imque #1
Posted 18 April 2013 - 03:06 AM
Hai.


I have seen some programs which allow you to kill the parent shell but I am not understanding the code used. What steps are taken and what needs to be done to kill the parent shell?
Spongy141 #2
Posted 18 April 2013 - 03:41 AM
When you say "shell" your referring to a program that will open another program right?
theoriginalbit #3
Posted 18 April 2013 - 03:50 AM
When you say "shell" your referring to a program that will open another program right?
Thats what I'm understanding it as, since I believe he is talking about this great hack by NeverCast
Imque #4
Posted 18 April 2013 - 05:21 PM
Yes like how NeverCast has completed the task.
Imque #5
Posted 21 April 2013 - 03:54 AM
Cough
GravityScore #6
Posted 21 April 2013 - 04:42 AM
To understand NeverCast's code, you have to understand how the stack works in Lua. The stack is like a pile of dishes. When a new program is run, or a function is called, the function/program is put on the top of the stack, just like a new dirty dish is put on top of the pile. When a function or program ends, it's like its dish has been cleaned, and taken off the pile. The function/program will be taken off the stack.

The first step to his code is looking through the pile of dirty dishes for the shell. The shell is the thing you see when you start the computer up (CraftOS basically). When he finds it, he tells it to exit, which basically means it tells the shell to clean all the dishes on top of it, then clean itself, so the shell or anything run inside of it is no longer on the stack.

You know when you type exit into CraftOS, and it says press any key to continue, then shuts down the computer? The next step is to override the key press and shutdown methods (os.pullEvent and os.shutdown) and basically simulate them, and make the computer believe they've behaved normally. Note, he doesn't actually shut the computer down, he just makes the shell think the computer's shut down, but it actually hasn't.

Then he resets everything back to normal, and voila! The program is now above CraftOS, above Rednet, etc…
theoriginalbit #7
Posted 21 April 2013 - 04:50 AM
-snip-
Nice explanation, much more low-level and easier to understand than the massive one I was working on :)/>
Imque #8
Posted 22 April 2013 - 02:27 AM
Okay, to become "top level" I need to trick the computer into shutting down then run my program and the shell again?
Espen #9
Posted 22 April 2013 - 02:51 AM
Okay, to become "top level" I need to trick the computer into shutting down then run my program and the shell again?
Almost, you need to trick the computer into thinking it shut down. ^_^/>
GravityScore #10
Posted 22 April 2013 - 03:14 PM
Okay, to become "top level" I need to trick the computer into shutting down then run my program and the shell again?
Almost, you need to trick the computer into thinking it shut down. ^_^/>

Yes, but you can't just override os.shutdown and call it, because it won't shut anything down. NeverCast's method is basically the only way I can think of, unless there's another method of getting the computer to shut down.
Imque #11
Posted 22 April 2013 - 08:11 PM
Eg.

os.shutdown = function()
  do
	sleep(0)
  end
end

Wont work?
Espen #12
Posted 22 April 2013 - 09:29 PM
Almost, you need to trick the computer into thinking it shut down. ^_^/>

Yes, but you can't just override os.shutdown and call it, because it won't shut anything down. NeverCast's method is basically the only way I can think of, unless there's another method of getting the computer to shut down.
Where did I say anything to the contrary?

Forcing the shell exit is making the bios.lua continue with its code after the parallel.waitForAny() that startet the top level shell.
Then the bios.lua presents the user with a message to "Press any key to continue" and wait for a key event, which we intercept and simply return just that event.
After that the bios.lua's last step would be to call os.shutdown(), which we intercept as well and instead recover everything to our heart's desire.
So after our successful injection bios.lua is waiting for its last call to return, namely that one from os.shutdown()

I didn't go into all the other necessary steps in my previous answer, because I was trying to give a direct answer to his question, concentrating only on the difference between actually shutting down the computer and making it only think we shut it down.
That's what I meant with "you need to trick the computer into thinking it shut down".
It was just a clarification of Imque's misunderstanding that we actually shut down the computer. Which we don't, we just make it think we did.
Imque #13
Posted 22 April 2013 - 10:22 PM
Does this work?

prev = {
shutdown = os.shutdown
}
os.shutdown = function()
term.clear()
term.setCursorPos(1, 1)
print("Booted before shell")
shell.run("rom/programs/shell")
os.shutdown = prev.shutdown
end
Espen #14
Posted 23 April 2013 - 01:06 AM
@Imque:
I'm afraid it wouldn't. The only thing you're essentially doing is to start another shell on top of your running program.
And when that shell ends, you'll be back in your program, which then would end and land you right back in the top level shell, from which your program started to begin with.

Let me try to explain how to do it …
Spoiler… by first starting with what normally happens:
When the computer is turned on, bios.lua (henceforth just "bios") is being started.
The bios sets up some stuff that is not important to know for our purposes and finally, near the end, starts the very shell we all use, as well as rednet (both via parallel.waitForAny()).
The bios won't run any code after that, until either the shell program ends (proper return or termination) or rednet ends.

So now we enter the shell, which also sets up some stuff and finally goes into an input-loop.
When you are hitting ENTER to confirm a command you typed into the shell, it processes it and then loops right back to the beginning of the loop, so that you can enter something again (that is, only after any program that you started via the shell has ended).
This will happen over and over again, until you type "exit" into the shell, which makes the shell run its own shell.exit() function.
shell.exit() simply sets bExit to true, which is a condition the shell checks for in its input-loop.
So once bExit == true the input-loop ends and thus the shell continues with the code that comes after the input-loop.
In that last code it basically checks if it is the top level shell and, if it is, tries to issue a shut down, which will (usually) make the computer shutdown.

If it wouldn't try to issue a shutdown at the end of its code, then what would happen, you ask?
Well, if you remember, the bios initiated the shell and is still waiting for it to return/end.
Since we ended the shell now and there is nothing else in the shell's code, we return back to the bios.
And because the bios ran the shell and rednet with parallel.waitForAny(), that function will return now, as one of the coroutines ended (the shell).

So now the bios continues with the code that comes after that.
If the shell ended with an error, then it will print that error.
After that it will print the message "Press any key to continue" and do a os.pullEvent("key").
I.e. when you come to see this message the computer will wait for you to press a key.
And, finally, the very last call of the bios will be os.shutdown().


To sum it up:
The computer usually shuts down in one of two ways:
  • You type "exit" at the top level shell, which exits the shell's loop and makes it either run the program "shutdown" or os.shutdown(), the former of which would normally do the same as the latter.
  • You terminate the shell (CTRL + T), which would prevent the shell from issuing any shutdown and instead would make the bios try to issue os.shutdown().
In both cases os.shutdown() is the thing we definitely do need to overwrite, but it is not the only thing, and it is not where we need to start.



First of all we need to kill the shell, because we don't want to run anything on top of that, we want to run something instead of it!

So what are the possibilites to end the shell again? We can…
  • … type "exit" which makes the shell call it's shell.exit() function, which sets its bExit variable to true.
  • … press CTRL + T in order to terminate the shell directly (if os.pullEvent() is untouched).
Since we are operating from within a program (our injection program) and not typing manually in the shell, we have to somehow make the shell run its shell.exit() function without us first having to end our program.
Remember: The shell waits for the return of a program until it continues with its loop.
So how did NeverCast do that?
He iterates through all levels of the function environments relative to the running program via getfenv().
When he finds the one who's key is "shell", then the value of that key will be the actual shell table.
So if we call exit() on that value, then we basically directly call the shell's exit() function, even though we haven't yet ended our program and then typed "exit"!

Of course the shell would now try to issue a shutdown…
That means we have to prevent that by overwriting os.shutdown().
Once that is prevented and the shell has finally ended, we return to the bios, which was patiently waiting all that time for either the shell or rednet to end.

Since the shell has ended, the bios now continues with its code.
Because the shell ended "proper", i.e. it didn't error, the bios won't show an error warning.
But now the bios prints "Press any key to continue" and issues an os.pullEvent("key").

We could be done now and be ok with having to press a key.
But NeverCast overwrote the os.pullEvent() for keys as well, so that it will always immediately return such events and thus we don't have to press anything for the bios (there's a bit more to overwriting os.pullEvent() though).
After that is done the bios finally wants to call os.shutdown() itself.
But since we've overwritten it, the computer will not shutdown.
Instead our own recover() function is being called which, as the name suggests, restores all the overwritten functions with their originals and then runs all the programs we'd like it to run. Like our own top level shell for example, along with some other programs, in a parallel call. :)/>

All the while though, the bios has not yet ended though! Remember: it called os.shutdown() as its very last action.
Normally the computer would've shutdown. But now that the computer remains on, it is still waiting for that function to return.
So at the moment the computer is in this state:
  • bios ran os.shutdown() ==> our fake os.shutdown() ==> our recover() ==> parallel.waitForAny( all of our desired programs )
As you can see we are within the parallel.waitForAny() function and are waiting for any of its coroutines to end.
If that should happen, then recover() will return to our fake os.shutdown(), which will return to where the bios called os.shutdown().
And because there is nothing left after that in the bios, the computer would remain on, I guess?

Haven't tried that yet, but maybe it will then shutdown anyway because of it not yielding anymore?
At least that's my guess, because the screen will clear nonetheless, which it wouldn't necessarily do if it were just halting the computer.

Reading this again after a few minutes I notice that I did forget something, namely the details for the overwritten pullEvent and setTimer functions.
But I hope it's become a bit clearer what the general steps are that one has to take.
Understanding how everything comes together in the first place is important before you can attempt to circumvent it. ^_^/>
Edited on 23 April 2013 - 12:03 AM
GravityScore #15
Posted 23 April 2013 - 01:30 AM
Almost, you need to trick the computer into thinking it shut down. ^_^/>

Yes, but you can't just override os.shutdown and call it, because it won't shut anything down. NeverCast's method is basically the only way I can think of, unless there's another method of getting the computer to shut down.

Forcing the shell exit is making the bios.lua continue with its code after the parallel.waitForAny() that startet the top level shell.
Then the bios.lua presents the user with a message to "Press any key to continue" and wait for a key event, which we intercept and simply return just that event.
After that the bios.lua's last step would be to call os.shutdown(), which we intercept as well and instead recover everything to our heart's desire.
So after our successful injection bios.lua is waiting for its last call to return, namely that one from os.shutdown()

I didn't go into all the other necessary steps in my previous answer, because I was trying to give a direct answer to his question, concentrating only on the difference between actually shutting down the computer and making it only think we shut it down.
That's what I meant with "you need to trick the computer into thinking it shut down".
It was just a clarification of Imque's misunderstanding that we actually shut down the computer. Which we don't, we just make it think we did.

I'm sorry, I quoted the wrong post :P/> I was ment to quote Imque's post.

I was directing it to Imque, just to make sure he/she understood that the way NeverCast did it is probably the only way.
Espen #16
Posted 23 April 2013 - 01:32 AM
I'm sorry, I quoted the wrong post :P/> I was ment to quote Imque's post.

I was directing it to Imque, just to make sure he/she understood that the way NeverCast did it is probably the only way.
Hey, honest mistake, no problem. The derp is strong in us humans, no shame in that. ^^