Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Process.chroot #5577

Merged
merged 3 commits into from Apr 14, 2018
Merged

Adds Process.chroot #5577

merged 3 commits into from Apr 14, 2018

Conversation

chris-huxtable
Copy link
Contributor

No description provided.

src/process.cr Outdated
path.check_no_null_byte
return if (LibC.chroot(path) == 0 && LibC.chdir("/") == 0)

case (Errno.value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case isn't required, Errno.new calls strerror and checks Errno.value for you:

if LibC.chroot(path) != 0
  raise Errno.new("Failed to chroot")
end

if LibC.chdir("/") != 0
  raise Errno.new("Failed to chdir after chroot")
end

would be the idiomatic way to write this method.

However, it does raise the issue of error handling, and that Process.chroot is now not atomic. If chroot succeeds but chdir fails, we're in some undefined state that the application is going to be very confused about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected

src/process.cr Outdated
when Errno::EIO then raise Errno.new("An I/O error occurred while reading from or writing to the file system.")
when Errno::EPERM then raise Errno.new("The caller is not the superuser.")
else raise Errno.new("...")
if (LibC.chroot(path) != 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parentheses are obsolete here

src/process.cr Outdated
raise Errno.new("Failed to chroot")
end

if (LibC.chdir("/") != 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

@RX14
Copy link
Contributor

RX14 commented Jan 12, 2018

I'm fine on the code for this, but i'm a little concerned about chdir failing (yes, i know, extremely exceptional circumstances) and being left in an inconsistent state.

@jkthorne
Copy link
Contributor

If chdir fails should it rollback to the previous directory it was in?

@bew
Copy link
Contributor

bew commented Mar 16, 2018

Maybe we should only keep the chroot in the method, and let the user chdir by himself?

@jkthorne
Copy link
Contributor

jkthorne commented Mar 16, 2018 via email

@chris-huxtable
Copy link
Contributor Author

chroot should always be followed by a chdir. If it fails it should raise which would kill the proc if it isn't caught. If it is caught the proc should still be killed.

The importance of keeping it in the chroot method is that we have to assume the person using it doesn't know how to use it properly. All the documentation in the world isn't going to prevent that. This forces a response from the developer without producing a silent inconsistent state.

@bew
Copy link
Contributor

bew commented Mar 16, 2018

ok @chris-huxtable didn't know that it was needed, the exception on error looks good, but there's no way to ensure the proc will exit, the user can always catch the exception..

@chris-huxtable
Copy link
Contributor Author

@bew - The only option I can think of that avoids that is to call exit(1) or something similar instead of raising. I am fine with this, but figured a catch could be useful if you were trying to provide better feedback.

@RX14
Copy link
Contributor

RX14 commented Mar 16, 2018

I suspect that we'll want to do a "hard abort" in case chdir() after chroot() fails. If the chroot fails we can raise normally, but if the chdir fails, we have to do something like

errno = Errno.new("chdir after chroot failed")
errno.callstack = Callstack.new
errno.inspect_with_backtrace(STDERR)
STDERR.puts "Unresolvable state, exiting..."
Process.exit(1)

src/process.cr Outdated

if LibC.chdir("/") != 0
errno = Errno.new("chdir after chroot failed")
errno.callstack = Callstack.new
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't compile, should be CallStack. My bad.

It'd be great if you could add some specs for this PR. You'll have to start a new process for testing chroot, but you can copy this: https://github.com/crystal-lang/crystal/blob/master/spec/std/kernel_spec.cr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I will have a look at this when I have a bit of spare time.

@@ -2,6 +2,28 @@ require "spec"
require "process"
require "tempfile"

private def build_and_run(code)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the same implementation as in kernel_spec.cr, it would make sense to extract to a spec helper instead of duplicating.

@@ -81,6 +103,21 @@ describe Process do
$?.exit_code.should eq(0)
end

it "can chroot" do
status, output = build_and_run <<-CODE
Process.chroot("/var/empty")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to ensure this actually exists. I would prefer to have it either in the project folder or /tmp (we need to resolve #5811), but not /var.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/var/empty is a standard location on UNIX filesystems. It is used almost exclusively for chrooting processes which don't need file system access.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need to ensure it exists.

@straight-shoota
Copy link
Member

Chroot requires a privileged user. How would we deal with the failing spec when crystal spec is usually run unprivileged?

@chris-huxtable
Copy link
Contributor Author

BTW It passes if run as root (not that we should be running as root, we shouldn't).

@@ -414,6 +414,27 @@ class Process
private def close_io(io)
io.close if io
end

# Changes the root directory and the current working directory for the current
# process.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should probably be obvious to everyone using it, but it wouldn't hurt to add a note that chroot should not be used as a security mechanism (especially if the process does not drop privilege).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have addressed this in 7361ad6.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't need to be so detailed ;) A simple "Chroot should not be used as security measure." would have been fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add that as a first sentence to make it Crystal clear? 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think "chroot should not be used as security measure." is correct. It can be used effectively as a mitigation, though its not perfect.

@straight-shoota
Copy link
Member

How about changing the spec code to succeed if Process.chroot raises Errno with EPERM in normal spec run (=unprivileged).
To test that it actually works with appropriate privileges, we could add a special spec file containing the current spec, which would not be included in a normal spec run. It must not be placed in spec/std, but could be like spec/privileged_spec.cr.

This way it can at least be tested manually running bin/crystal spec spec/privileged_spec.cr as root. Adding this to CI should be a task for a subsequent PR.

it "can chroot" do
status, output = build_and_run <<-CODE
Process.chroot("/var/empty")
working_dir = Dir.current()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant ()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passes 'format'. I prefer the inclusion as it makes it visibly a function. As this is a sense of style I would consider this "bike-shedding".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the style used throughout the stdlib, empty () are considered redundant if not needed.


puts "Wrong Directory" if working_dir != "/"
puts "Not a Directory" if !File.directory?(working_dir)
puts "Directoyr isn't empty" if !Dir.empty?(working_dir)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: Directoyr

@@ -8,6 +8,7 @@ lib LibC
X_OK = 0x01
SC_CLK_TCK = 3

fun chroot(dirname : Char*) : Int
Copy link
Contributor

@Sija Sija Apr 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, use consistent naming: path vs dirname. Former would be preferred.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made it path when the source/man pages says path and dirname when the source/man pages says dirname. Generally BSD uses dirname, linux uses `path'.

@RX14
Copy link
Contributor

RX14 commented Apr 6, 2018

If we need root to spec it, we can't spec it. I'm sorry for leading you on a wild goose chase but it's probably best to remove the spec :(

Still, a typeof "does it compile" spec is required.

@straight-shoota
Copy link
Member

straight-shoota commented Apr 6, 2018

@RX14 What speaks about a separate spec file that is usually not run, but can (with root) to verify?

src/process.cr Outdated
errno.callstack = CallStack.new
errno.inspect_with_backtrace(STDERR)
STDERR.puts "Unresolvable state, exiting..."
Process.exit(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this use abort("Unresolvable state, exiting...") instead of STDERR.puts + Process.exit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since #5413 is merged, it should also set AtExitHandlers.exception = errno. With this in place errno.inspect_with_backtrace(STDERR) is needless.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtExitHandlers.exception prints Unhandled exception but it is not - and you just can't handle this exception.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But abort would be great.

Process.chroot("/var/empty")
puts "FAIL"
rescue ex : Errno
puts ( ex.errno == Errno::EPERM ) ? "Success" : "FAIL: " + (ex.message||"")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pardon the comment on style, but the string concatenation looks really silly and should not be encouraged. Just use "FAIL: #{ex.message}". Ditto below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe simply ex.message

@@ -414,6 +414,27 @@ class Process
private def close_io(io)
io.close if io
end

# Changes the root directory and the current working directory for the current
# process.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't need to be so detailed ;) A simple "Chroot should not be used as security measure." would have been fine.

@@ -414,6 +414,27 @@ class Process
private def close_io(io)
io.close if io
end

# Changes the root directory and the current working directory for the current
# process.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add that as a first sentence to make it Crystal clear? 😄

src/process.cr Outdated
errno.callstack = CallStack.new
errno.inspect_with_backtrace(STDERR)
STDERR.puts "Unresolvable state, exiting..."
Process.exit(1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But abort would be great.

@straight-shoota
Copy link
Member

About the failing spec: I told you so 😛 #5577 (comment)

@chris-huxtable
Copy link
Contributor Author

@straight-shoota - I am perplexed. I took another dive into Linux last week. When I couldn't find ifconfig I threw up my hands and went back to OpenBSD. Now I find out it doesn't have /var/empty/. BSD Forever!

@chris-huxtable
Copy link
Contributor Author

So the one failure appears to have occurred due to a failure to allocate memory. Is there a way to re-run it?

Process.chroot("/usr")
puts "FAIL"
rescue ex : Errno
puts ( ex.errno == Errno::EPERM ) ? "Success" : "FAIL: " + (ex.message||"")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

puts (ex.errno == Errno::EPERM) ? "Success" : "FAIL: #{ex.message}"

ditto below

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There appears to be a bug or something in the language (or I am missing something). It wont allow that. Says "undefined local variable or method 'ex'". Also tried:

puts ( ex.errno == Errno::EPERM ) ? "Success" : "FAIL: #{ex.message||"No Message"}"

to no avail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#{ex.message} will be interpolated in the heredoc. You need to either escape the hash or use a non-interpolating heredoc (<<-'CODE' header) to use interpolation in the source string.

@RX14
Copy link
Contributor

RX14 commented Apr 7, 2018

@chris-huxtable I have /var/empty/ on my distro... Also the reason why we can't remove redundant () empty brackets on some calls in the formatter is just a technical reason. They should be removed, and this is a perfect candidate for someone starting a style guide saying this.

@chris-huxtable
Copy link
Contributor Author

chris-huxtable commented Apr 7, 2018

Is there a problem with the Travis CI? It's failed several times on random things.

@chris-huxtable
Copy link
Contributor Author

@RX14 - It now has a spec and is ready for review.

Copy link
Contributor

@RX14 RX14 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good enough

@chris-huxtable chris-huxtable changed the title Adds chroot to Process Adds chroot to Process Apr 7, 2018
@chris-huxtable chris-huxtable changed the title Adds chroot to Process Adds Process.chroot Apr 7, 2018
@chris-huxtable
Copy link
Contributor Author

Rebased and combined commits.

Copy link
Contributor

@RX14 RX14 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just what @straight-shoota said about not duplicating the spec helper than we're good to go.

Process.chroot("/usr")
puts "FAIL"
rescue ex : Errno
puts ( ex.errno == Errno::EPERM ) ? "Success" : "FAIL: \#{ex.message||"No Message"}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Please, remove whitespaces after ( & before )
  • ||"No Message" part here is redundant, #{ex.message} is enough. ditto below
  • Would be a tad cleaner to use non-interpolated heredoc (<<-'CODE') as suggested by @straight-shoota, instead of escaping interpolation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Sija - We very much need a style guide. You seem interested in compelling a specific style. Perhaps you can draft a style guide that can be discussed and adopted. I am not throwing shade; I also have strong style opinions.

The space before and after brackets formats. So based on previously discussed standards of 'correctness' it should be fine unless otherwise declared not to be. I am going to change it this time but I think this is an unnecessarily micro management of style.

Copy link
Contributor

@RX14 RX14 Apr 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chris-huxtable we have a style guide right now - crystal tool format. This code doesn't format. Feel free to ignore the rest of what @Sija recommended.

I don't care that it doesn't format because it's inside a string, inside a the spec suite, and it doesn't break the build, and honestly who cares, but it seems some people do.

Copy link
Member

@straight-shoota straight-shoota left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks GTG!

@straight-shoota
Copy link
Member

I just remembered there is already an (apparently undocumented) folder spec/manual. Could place the privileged spec there so it can at least be verified manually.

Copy link
Contributor

@RX14 RX14 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally!

@sdogruyol sdogruyol merged commit 913cd8d into crystal-lang:master Apr 14, 2018
@sdogruyol sdogruyol added this to the Next milestone Apr 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants