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

Reopen standard file descriptors when they are missing on Unix #75295

Merged
merged 1 commit into from
Sep 28, 2020

Conversation

tmiasko
Copy link
Contributor

@tmiasko tmiasko commented Aug 8, 2020

The syscalls returning a new file descriptors generally return lowest-numbered
file descriptor not currently opened, without any exceptions for those
corresponding to stdin, sdout, or stderr.

Previously when any of standard file descriptors has been closed before starting
the application, operations on std::io::{stderr,stdin,stdout} were likely to
either succeed while being performed on unrelated file descriptor, or fail with
EBADF which is silently ignored.

Avoid the issue by using /dev/null as a replacement when the standard file
descriptors are missing.

The implementation is based on the one found in musl. It was selected among a
few others on the basis of the lowest overhead in the case when all descriptors
are already present (measured on GNU/Linux).

Closes #57728.
Closes #46981.
Closes #60447.

Benefits:

  • Makes applications robust in the absence of standard file descriptors.
  • Upholds IntoRawFd / FromRawFd safety contract (which was broken previously).

Drawbacks:

  • Additional syscall during startup.
  • The standard descriptors might have been closed intentionally.
  • Requires /dev/null.

Alternatives:

  • Check if stdin, stdout, stderr are opened and provide no-op substitutes in std::io::{stdin,stdout,stderr} without reopening them directly.
  • Leave the status quo, expect robust applications to reopen them manually.

@rust-highfive
Copy link
Collaborator

r? @shepmaster

(rust_highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 8, 2020
@the8472
Copy link
Member

the8472 commented Aug 8, 2020

For windows there already is the Maybe::Fake implementation. Perhaps it would make sense to also apply that to unix systems when the file descriptors are not open? Then the process could keep working without needing access to /dev/null which might not exist in some chroot environment.

@tmiasko
Copy link
Contributor Author

tmiasko commented Aug 8, 2020

Using a no-op placeholder is definitely another option to consider. The presence of /dev/null would no longer required. The check if descriptors are valid at start-up would still be necessary. It is not immediately clear what should AsRawFd return in that case, although -1 might be an option. Furthermore if a placeholder is used it is no longer possible to customize the behaviour by changing the file descriptors directly.

AFAICS the Windows implementation no longer uses Maybe::Fake, but always obtains a fresh handle, avoiding the last issue in the process. But I don't see how this could be addressed on Unix while using placeholders, at least not without API additions.

I would like to avoid introducing a hard requirement on /dev/null, so I changed the PR to reopen the fds on the best effort basis.

if pfd.revents & libc::POLLNVAL == 0 {
continue;
}
if libc::open("/dev/null\0".as_ptr().cast(), libc::O_RDWR, 0) == -1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if libc::open("/dev/null\0".as_ptr().cast(), libc::O_RDWR, 0) == -1 {
if libc::open(b"/dev/null\0".as_ptr().cast(), libc::O_RDWR, 0) == -1 {

@tmiasko
Copy link
Contributor Author

tmiasko commented Aug 12, 2020

r? @joshtriplett

@joshtriplett
Copy link
Member

joshtriplett commented Aug 13, 2020

So, it's definitely sketchy to run a process with file descriptors 0, 1, or 2 closed, for exactly this reason: the next opened file will become one of those file descriptors, which will wreak all manner of havoc.

However, assuming the existence or accessibility of devices like /dev/null, as common as they are, also feels problematic.

As an alternative, which doesn't require any particular device file to exist on the filesystem, we could use pipes:

  • If file descriptor 0 doesn't exist, then open a pipe, close the write end, and set fd 0 to the read end. Any reads will then produce EOF.
  • If file descriptor 1 or 2 doesn't exist, then open a pipe, close the read end, and set fd 1 or 2 (whichever doesn't exist) to that. Any writes will produce an error.

I think that will work better. It requires a little bit of care (because some of the pipes will initially become the wrong file descriptors), but the following algorithm should work:

  • If fd 0 doesn't exist, then open a pipe, and the read end will always become fd 0. Just close the write end.
  • Then, if fd 1 doesn't exist, open a pipe, close the read end (which will be fd 1), dup2 the write end to fd 1, close the old write end.
  • Then, if fd 2 doesn't exist, open a pipe, close the read end (which will be fd 2), dup2 the write end to fd 2, close the old write end.

@the8472
Copy link
Member

the8472 commented Aug 13, 2020

The output pipes would begin to block once they're full, wouldn't they? The advantage of /dev/null is that it never blocks

@joshtriplett
Copy link
Member

No, if the other end is closed it'll error.

@tmiasko
Copy link
Contributor Author

tmiasko commented Aug 13, 2020

I don't think a closed pipe would be a good replacement due to SIGPIPE
generated when writing to it. While, the startup code does ignore SIGPIPE, the
signal disposition can be changed afterwards, and application do change it,
like rustc driver for example.

I would find receiving a SIGPIPE in this case to be highly undesirable.

@joshtriplett
Copy link
Member

It's also undesirable to run a program without those file descriptors. And it's undesirable to require the hardcoded path /dev/null.

@the8472
Copy link
Member

the8472 commented Aug 13, 2020

And it's undesirable to require the hardcoded path /dev/null.

The current version of this PR uses them on a best-effort basis, i.e. it's not required.

@tmiasko
Copy link
Contributor Author

tmiasko commented Aug 13, 2020

To add another argument for /dev/null. When started in secure mode (setuid,
etc.), the mainstream libc implementations already do require the presence of
it to reopen the standard file descriptors.

EDIT: Another disadvantage of closed pipe for stdout / stderr is that println!
/ eprintln! would panic when used.

@joshtriplett
Copy link
Member

Re-posting a review comment at the top level:
I wasn't aware of RFC 1014. Given that, I agree, the pipe idea won't work. Objection withdrawn; let's go with /dev/null.

@tmiasko
Copy link
Contributor Author

tmiasko commented Aug 14, 2020

In context of RFC 1014, I extended test to verify non-panicking behaviour.

@tmiasko tmiasko force-pushed the fds branch 3 times, most recently from dbdea19 to 7eb4381 Compare August 22, 2020 23:30
@tmiasko
Copy link
Contributor Author

tmiasko commented Aug 22, 2020

The poll on Darwin doesn't set POLLNVAL for closed fds, so I used an alternative implementation there.

@crlf0710 crlf0710 added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. O-linux Operating system: Linux T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 11, 2020
@rust-log-analyzer
Copy link
Collaborator

The job arm-android of your PR failed (pretty log, raw log). Through arcane magic we have determined that the following fragments from the build log may contain information about the problem.

Click to expand the log.
Initialized empty Git repository in /home/runner/work/rust/rust/.git/
##[command]git remote add origin https://github.com/rust-lang/rust
##[command]git config gc.auto 0
##[command]git config --get-all http.https://github.com/rust-lang/rust.extraheader
##[command]git -c http.extraheader="AUTHORIZATION: basic ***" fetch --tags --prune --progress --no-recurse-submodules --depth=2 origin +refs/heads/*:refs/remotes/origin/* +refs/pull/75295/merge:refs/remotes/pull/75295/merge
---
    Finished release [optimized] target(s) in 28.71s
Check compiletest suite=ui mode=ui (x86_64-unknown-linux-gnu -> arm-linux-androideabi)

running 10822 tests
ii.................i.ii............................................................................. 100/10822
.......................iiiiiiiiiiiiiii.............................................................. 200/10822
.................................................................................................... 400/10822
.................................................................................................... 500/10822
........................................i........................................................... 600/10822
........i.i......................................................................................... 700/10822
---
.................................................................................................... 8600/10822
.................................................................................................... 8700/10822
.................................................................................................... 8800/10822
..................................................i................................................. 8900/10822
............................iiiiii.................................................................. 9000/10822
.....iiiiii..iiiiii.i............................................................................... 9100/10822
.................................................................................................... 9300/10822
.................................................................................................... 9400/10822
....i............................................................................................... 9500/10822
.................................................................................................... 9600/10822
---
..........i......................................................................................... 10800/10822
......................
failures:

---- [ui] ui/closed-std-fds.rs stdout ----
error: test run failed!
status: exit code: 101
status: exit code: 101
command: "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0-tools-bin/remote-test-client" "run" "0" "/checkout/obj/build/x86_64-unknown-linux-gnu/test/ui/closed-std-fds/a"
------------------------------------------
------------------------------------------
uploaded "/checkout/obj/build/x86_64-unknown-linux-gnu/test/ui/closed-std-fds/a", waiting for result
------------------------------------------
stderr:
------------------------------------------
------------------------------------------
thread 'main' panicked at 'child failed with signal: 11', /checkout/src/test/ui/closed-std-fds.rs:34:9

------------------------------------------


---
thread 'main' panicked at 'Some tests failed', src/tools/compiletest/src/main.rs:354:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


command did not execute successfully: "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0-tools-bin/compiletest" "--compile-lib-path" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage2/lib" "--run-lib-path" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage2/lib/rustlib/arm-linux-androideabi/lib" "--rustc-path" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage2/bin/rustc" "--src-base" "/checkout/src/test/ui" "--build-base" "/checkout/obj/build/x86_64-unknown-linux-gnu/test/ui" "--stage-id" "stage2-arm-linux-androideabi" "--mode" "ui" "--target" "arm-linux-androideabi" "--host" "x86_64-unknown-linux-gnu" "--llvm-filecheck" "/checkout/obj/build/x86_64-unknown-linux-gnu/llvm/build/bin/FileCheck" "--linker" "/android/ndk/arm-14/bin/arm-linux-androideabi-clang" "--host-rustcflags" "-Crpath -O -Cdebuginfo=0 -Zunstable-options  -Lnative=/checkout/obj/build/x86_64-unknown-linux-gnu/native/rust-test-helpers" "--target-rustcflags" "-Crpath -O -Cdebuginfo=0 -Zunstable-options  -Lnative=/checkout/obj/build/arm-linux-androideabi/native/rust-test-helpers" "--docck-python" "/usr/bin/python3" "--lldb-python" "/usr/bin/python3" "--quiet" "--llvm-version" "11.0.0-rust-1.48.0-nightly" "--llvm-components" "aarch64 aarch64asmparser aarch64codegen aarch64desc aarch64disassembler aarch64info aarch64utils aggressiveinstcombine all all-targets analysis arm armasmparser armcodegen armdesc armdisassembler arminfo armutils asmparser asmprinter avr avrasmparser avrcodegen avrdesc avrdisassembler avrinfo binaryformat bitreader bitstreamreader bitwriter cfguard codegen core coroutines coverage debuginfocodeview debuginfodwarf debuginfogsym debuginfomsf debuginfopdb demangle dlltooldriver dwarflinker engine executionengine extensions frontendopenmp fuzzmutate globalisel gtest gtest_main hexagon hexagonasmparser hexagoncodegen hexagondesc hexagondisassembler hexagoninfo instcombine instrumentation interpreter ipo irreader jitlink libdriver lineeditor linker lto mc mca mcdisassembler mcjit mcparser mips mipsasmparser mipscodegen mipsdesc mipsdisassembler mipsinfo mirparser msp430 msp430asmparser msp430codegen msp430desc msp430disassembler msp430info native nativecodegen nvptx nvptxcodegen nvptxdesc nvptxinfo objcarcopts object objectyaml option orcerror orcjit passes powerpc powerpcasmparser powerpccodegen powerpcdesc powerpcdisassembler powerpcinfo profiledata remarks riscv riscvasmparser riscvcodegen riscvdesc riscvdisassembler riscvinfo riscvutils runtimedyld scalaropts selectiondag sparc sparcasmparser sparccodegen sparcdesc sparcdisassembler sparcinfo support symbolize systemz systemzasmparser systemzcodegen systemzdesc systemzdisassembler systemzinfo tablegen target testingsupport textapi transformutils vectorize webassembly webassemblyasmparser webassemblycodegen webassemblydesc webassemblydisassembler webassemblyinfo windowsmanifest x86 x86asmparser x86codegen x86desc x86disassembler x86info xray" "--cc" "" "--cxx" "" "--cflags" "" "--remote-test-client" "/checkout/obj/build/x86_64-unknown-linux-gnu/stage0-tools-bin/remote-test-client" "--adb-path" "adb" "--adb-test-dir" "/data/tmp/work" "--android-cross-path" "/android/ndk/arm-14" "--color" "always"


failed to run: /checkout/obj/build/bootstrap/debug/bootstrap --stage 2 test --target arm-linux-androideabi
Build completed unsuccessfully in 0:33:45
Build completed unsuccessfully in 0:33:45
== clock drift check ==
  local time: Sat Sep 26 12:52:02 UTC 2020
  network time: Sat, 26 Sep 2020 12:52:02 GMT
== end clock drift check ==
##[error]Process completed with exit code 1.
Terminate orphan process: pid (6113) (python)

I'm a bot! I can only do what humans tell me to, so if this was not helpful or you have suggestions for improvements, please ping or otherwise contact @rust-lang/infra. (Feature Requests)

@tmiasko
Copy link
Contributor Author

tmiasko commented Sep 26, 2020

The child process spawned in the test case segfaults on arm-android. I don't know why yet, but it is unrelated to the changes in std, since the same issue occurs without any changes.

The syscalls returning a new file descriptors generally use
lowest-numbered file descriptor not currently opened, without any
exceptions for those corresponding to the standard streams.

Previously when any of standard streams has been closed before starting
the application, operations on std::io::{stderr,stdin,stdout} objects
were likely to operate on other logically unrelated file resources
opened afterwards.

Avoid the issue by reopening the standard streams when they are closed.
@tmiasko
Copy link
Contributor Author

tmiasko commented Sep 27, 2020

I modified the PR to reuse an existing test case instead of adding a new one. This test case was already ignored on android for presumably the same reasons (closing any two standard streams before spawning a new process is sufficient to cause the segfault).

@Amanieu
Copy link
Member

Amanieu commented Sep 27, 2020

@bors r+

@bors
Copy link
Contributor

bors commented Sep 27, 2020

📌 Commit 7d98d22 has been approved by Amanieu

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 27, 2020
@bors
Copy link
Contributor

bors commented Sep 28, 2020

⌛ Testing commit 7d98d22 with merge db7ee7c...

@bors
Copy link
Contributor

bors commented Sep 28, 2020

☀️ Test successful - checks-actions, checks-azure
Approved by: Amanieu
Pushing db7ee7c to master...

@bors bors added the merged-by-bors This PR was explicitly merged by bors. label Sep 28, 2020
@bors bors merged commit db7ee7c into rust-lang:master Sep 28, 2020
@rustbot rustbot added this to the 1.48.0 milestone Sep 28, 2020
@RalfJung
Copy link
Member

Something went wrong for Miri on apple targets:

error[E0428]: the name `sanitize_standard_fds` is defined multiple times

   --> /home/travis/.rustup/toolchains/master/lib/rustlib/src/rust/library/std/src/sys/unix/mod.rs:149:5

    |

134 |     unsafe fn sanitize_standard_fds() {

    |     --------------------------------- previous definition of the value `sanitize_standard_fds` here

...

149 |     unsafe fn sanitize_standard_fds() {}

    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `sanitize_standard_fds` redefined here

    |

    = note: `sanitize_standard_fds` must be defined only once in the value namespace of this block

I'll submit a PR.

@RalfJung
Copy link
Member

Submitted #77288

RalfJung added a commit to RalfJung/rust that referenced this pull request Sep 28, 2020
fix building libstd for Miri on macOS

Fixes a Miri regression introduced by rust-lang#75295
Cc @tmiasko @Amanieu
RalfJung added a commit to RalfJung/rust that referenced this pull request Sep 28, 2020
fix building libstd for Miri on macOS

Fixes a Miri regression introduced by rust-lang#75295
Cc @tmiasko @Amanieu
@tmiasko tmiasko deleted the fds branch October 18, 2020 00:21
@Alphare
Copy link

Alphare commented Sep 10, 2021

I've found that this change introduced a regression in the "Rust first then fallback to Python" version of the Mercurial test suite.

I was testing if going from 1.41.1 (previous Debian stable version) to 1.48.0 broke anything, and it appears it did. This test checks that missing file descriptors raise an acceptable error, and this does not happen anymore. Here is the fallback code for rhg, running the Python version of hg as a subprocess.

Here is the (somewhat convoluted) reproduction, which requires python be a valid Python 3 interpreter that I used with cargo bisect-rust (thanks @SimonSapin for the pointer on that tool btw):

//! Run the executable (not with `cargo play`, it'll eat the redirect)
//!     `myexecutable -c 'import sys; sys.stdout.write("hello")' 1>&-`
//! If compiled with 1.41.1, it will output (with an error code of 1):
//!
//! Traceback (most recent call last):
//!  File "<string>", line 1, in <module>
//!  AttributeError: 'NoneType' object has no attribute 'write'
//!
//! But with 1.48.0, it doesn't output anything and silently eats the error code, exiting with 0.

use std::process::Command;

fn main() {
    let mut args_os = std::env::args_os();
    args_os.next().expect("expected argv[0] to exist");
    let mut c = Command::new("python");
    c.args(args_os);
    let status = c.status();
    match status {
        Err(_) => (),
        Ok(s) =>  {
            if s.success() {
                std::process::exit(1);
            }
        }
    }
}

I'm not 100% sure what to think of it yet, but my gut feeling is that eating errors is bad; I'm reporting this now since it's fresh in my mind.

@Mark-Simulacrum
Copy link
Member

Please file a new issue -- it's unlikely to receive much attention on a closed PR, but an issue will let us appropriately tag and track any potential fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merged-by-bors This PR was explicitly merged by bors. O-linux Operating system: Linux S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs Relevant to the library team, which will review and decide on the PR/issue. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet