├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── build.sh ├── build_image.sh ├── builder └── Dockerfile ├── crashcart.png ├── packages ├── src ├── errors.rs ├── logger.rs ├── loopback.rs └── main.rs ├── vol ├── .crashcartrc └── build_crashcart.sh └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | crashcart 4 | .wercker 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Crashcart # 2 | 3 | *Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.* 4 | 5 | Pull requests can be made under 6 | [The Oracle Contributor Agreement](https://www.oracle.com/technetwork/community/oca-486395.html) (OCA). 7 | 8 | For pull requests to be accepted, the bottom of 9 | your commit message must have the following line using your name and 10 | e-mail address as it appears in the OCA Signatories list. 11 | 12 | ``` 13 | Signed-off-by: Your Name 14 | ``` 15 | 16 | This can be automatically added to pull requests by committing with: 17 | 18 | ``` 19 | git commit --signoff 20 | ``` 21 | 22 | Only pull requests from committers that can be verified as having 23 | signed the OCA can be accepted. 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crashcart" 3 | version = "0.1.0" 4 | authors = ["Vishvananda Ishaya Abrams "] 5 | 6 | [dependencies] 7 | caps = "0.0.1" 8 | error-chain = "0.10.0" 9 | getopts = "0.2.14" 10 | nix = "0.8.0" 11 | libc = "0.2.21" 12 | log = {version = "0.3.6", features = ["release_max_level_info"] } 13 | scopeguard = "0.3.2" 14 | glob = "0.2.11" 15 | 16 | [profile.release] 17 | lto = true 18 | panic = 'abort' 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 2 | 3 | This software is dual-licensed to you under the Universal Permissive License (UPL) or the Apache License 2.0 or both. See below for license terms. 4 | ____________________________ 5 | The Universal Permissive License (UPL), Version 1.0 6 | Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 7 | 8 | Subject to the condition set forth below, permission is hereby granted to any person obtaining a copy of this software, associated documentation and/or data (collectively the "Software"), free of charge and under any and all copyright rights in the Software, and any and all patent rights owned or freely licensable by each licensor hereunder covering either (i) the unmodified Software as contributed to or provided by such licensor, or (ii) the Larger Works (as defined below), to deal in both 9 | 10 | (a) the Software, and 11 | (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if one is included with the Software (each a "Larger Work" to which the Software is contributed by such licensors), 12 | 13 | without restriction, including without limitation the rights to copy, create derivative works of, display, perform, and distribute the Software and make, use, sell, offer for sale, import, export, have made, and have sold the Software and the Larger Work(s), and to sublicense the foregoing rights on either these or other terms. 14 | 15 | This license is subject to the following condition: 16 | 17 | The above copyright notice and either this complete permission notice or at a minimum a reference to the UPL must be included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | The Apache Software License, Version 2.0 22 | Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved. 23 | 24 | Licensed under the Apache License, Version 2.0 (the "License"); You may not use this product except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. A copy of the license is also reproduced below. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 25 | 26 | Apache License 27 | 28 | Version 2.0, January 2004 29 | 30 | http://www.apache.org/licenses/ 31 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 32 | 1. Definitions. 33 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 34 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 35 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 36 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 37 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 38 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 39 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 40 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 41 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 42 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 43 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 44 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 45 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 46 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 47 | You must cause any modified files to carry prominent notices stating that You changed the files; and 48 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 49 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 50 | 51 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 52 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 53 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 54 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 55 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 56 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 57 | END OF TERMS AND CONDITIONS 58 | 59 | APPENDIX: How to apply the Apache License to your work. 60 | 61 | To apply the Apache License to your work, attach the following 62 | boilerplate notice, with the fields enclosed by brackets "[]" 63 | replaced with your own identifying information. (Don't include 64 | the brackets!) The text should be enclosed in the appropriate 65 | comment syntax for the file format. We also recommend that a 66 | file or class name and description of purpose be included on the 67 | same "printed page" as the copyright notice for easier 68 | identification within third-party archives. 69 | 70 | Copyright [yyyy] [name of copyright owner] 71 | 72 | Licensed under the Apache License, Version 2.0 (the "License"); 73 | you may not use this file except in compliance with the License. 74 | You may obtain a copy of the License at 75 | 76 | http://www.apache.org/licenses/LICENSE-2.0 77 | 78 | Unless required by applicable law or agreed to in writing, software 79 | distributed under the License is distributed on an "AS IS" BASIS, 80 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 81 | See the License for the specific language governing permissions and 82 | limitations under the License. 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `crashcart` - microcontainer debugging tool # 2 | 3 | ![crashcart](https://github.com/oracle/crashcart/raw/master/crashcart.png 4 | "crashcart") 5 | 6 | ## What is `crashcart`? ## 7 | 8 | `crashcart` is a simple command line utility that lets you sideload an image 9 | with linux binaries into an existing container. 10 | 11 | ## Building `crashcart` ## 12 | 13 | [![wercker status](https://app.wercker.com/status/3b1da922588f5550faca49a356013e52/s/master "wercker status")](https://app.wercker.com/project/byKey/3b1da922588f5550faca49a356013e52) 14 | 15 | Install rust: 16 | 17 | curl https://sh.rustup.rs -sSf | sh 18 | rustup toolchain install stable-x86_64-unknown-linux-gnu 19 | rustup default stable-x86_64-unknown-linux-gnu # for stable 20 | rustup target install x86_64-unknown-linux-musl # for stable 21 | rustup toolchain install nightly-x86_64-unknown-linux-gnu 22 | rustup default nightly-x86_64-unknown-linux-gnu # for nightly 23 | rustup target install x86_64-unknown-linux-musl # for nightly 24 | 25 | Building can be done via build.sh: 26 | 27 | build.sh 28 | 29 | By default, build.sh builds a dynamic binary using gnu. To build a static 30 | binary, set `TARGET` to `x86_64-unknown-linux-musl`: 31 | 32 | TARGET=x86_64-unknown-linux-musl ./build.sh 33 | 34 | ## Building `crashcart.img` ## 35 | 36 | Image build dependencies: 37 | 38 | sudo 39 | docker 40 | 41 | `crashcart` will load binaries from an image file into a running container. To 42 | build the image, you just need docker installed and then you can use 43 | build_image.sh: 44 | 45 | build_image.sh 46 | 47 | The build image script will build a `crashcart_builder` image using the 48 | dockerfile in the builder directory. It will then run this builder as a 49 | privileged container. It needs to be privileged because the image is created by 50 | loopback mounting an ext3 filesystem and copying files in. It may be possible 51 | to do this without root privileges using something like e2tools, but these have 52 | not been packaged for alpine. 53 | 54 | The `crashcart_builder` will take a very long time the first time it is run. 55 | The relocated binaries are built from source via the nix package manager, and 56 | the toolchain needs to be built from scratch. Later builds should go much more 57 | quickly because the nix store is cached in a in the vol directory and bind 58 | mounted into the builder. 59 | 60 | To add to the list of packages in the resulting image, simply add the package 61 | names to the packages file before building. Packages are installed via the 62 | nix-env tool. An up-to-date list of nix packages can be searched 63 | [here](https://nixos.org/nixos/packages.html). 64 | 65 | ## Using `crashcart` ## 66 | 67 | To enter a container and run `crashcart`'s bash just pass the container id: 68 | 69 | sudo ./crashcart $ID 70 | 71 | $ID can be the container id of a `docker` or `rkt` container, or the pid of any 72 | process running inside a container. 73 | 74 | To run another command from the `crashcart` image, pass the full path: 75 | 76 | sudo ./crashcart $ID /dev/crashcart/bin/tcpdump 77 | 78 | To use docker-exec instead of entering the namespaces via `crashcart`'s 79 | internal namespace handling, use the -e flag (NOTE: that this requires $ID to be 80 | a docker container id): 81 | 82 | sudo ./crashcart -e $ID 83 | 84 | ## Manually Running Binaries from the `crashcart` Image ## 85 | 86 | To manually mount the `crashcart` image into a container, use the -m flag. 87 | 88 | sudo ./crashcart -m $ID 89 | 90 | To manually unmount the `crashcart` image from a container, use the -u flag. 91 | 92 | sudo ./crashcart -u $ID 93 | 94 | Once you have manually mounted the image, you can use `docker exec` or 95 | `nsenter` to run things inside the container. `crashcart` locates its binaries 96 | in `/dev/crashcart/bin` or `/dev/crashcart/sbin`. To execute 97 | `tcpdump` for example, you can use: 98 | 99 | docker exec -it $CONTAINER_ID /dev/crashcart/bin/tcpdump 100 | 101 | To run a shell with the all of `crashcart`'s utilities available in the path, you 102 | can use: 103 | 104 | docker exec -it $CONTAINER_ID -- \ 105 | /dev/crashcart/profile/bin/bash --rcfile /dev/crashcart/.crashcartrc -i 106 | 107 | You can also do an equivalent command using `nsenter`: 108 | 109 | sudo nsenter -m -u -i -n -p -t $PID -- \ 110 | /dev/crashcart/profile/bin/bash --rcfile /dev/crashcart/.crashcartrc -i 111 | 112 | Note that if you are using user namespaces you might have to specify -U. You 113 | also can use -S and -G to use a different user or group id in the container. 114 | 115 | `crashcart` leaves the image mounted as a loopback device. If there are no 116 | containers still using the `crashcart` image, you can remove the device as 117 | follows: 118 | 119 | sudo losetup -d `readlink crashcart.img.lnk`; sudo rm crashcart.img.lnk 120 | 121 | ## Known Issues ## 122 | 123 | `crashcart` doesn't work with user namespaces prior to kernel 4.8. In earlier 124 | versions of the kernel, when you attempt to mount a device inside a mount 125 | namespace that is a child of a user namespace, the kernel returns EPERM. The 126 | logic was changed in 4.8 so that it is possible as long as the caller of mount 127 | is in the init userns. 128 | 129 | ## TODO ## 130 | 131 | * add functionality to run image with crashcart mount using docker run -v 132 | * temporarily remount /dev in the container rw if it is ro 133 | * allow user to set uid and gid in the container 134 | 135 | ## Contributing ## 136 | 137 | `crashcart` is an open source project. See [CONTRIBUTING](CONTRIBUTING.md) for 138 | details. 139 | 140 | Oracle gratefully acknowledges the contributions to `crashcart` that have been made 141 | by the community. 142 | 143 | ## Getting in touch ## 144 | 145 | The best way to get in touch is Slack. 146 | 147 | Click [here](https://join.slack.com/t/oraclecontainertools/shared_invite/enQtMzIwNzg3NDIzMzE5LTIwMjZlODllMWRmNjMwZGM1NGNjMThlZjg3ZmU3NDY1ZWU5ZGJmZWFkOTBjNzk0ODIxNzQ2ODUyNThiNmE0MmI) to join the the [Oracle Container Tools workspace](https://oraclecontainertools.slack.com). 148 | 149 | Then join the [Crashcart channel](https://oraclecontainertools.slack.com/messages/C8CJ5M9ML). 150 | 151 | ## License ## 152 | 153 | Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved. 154 | 155 | `crashcart` is dual licensed under the Universal Permissive License 1.0 and the 156 | Apache License 2.0. 157 | 158 | See [LICENSE](LICENSE.txt) for more details. 159 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | TARGET=${TARGET-x86_64-unknown-linux-gnu} 3 | if [ "$TARGET" != "" ]; then 4 | TGT="--target $TARGET" 5 | fi 6 | VERSION=debug 7 | if [[ "$1" == "--release" ]]; then 8 | VERSION=release 9 | fi 10 | cargo build --verbose $TGT $1 && \ 11 | cp target/$TARGET/$VERSION/crashcart . 12 | -------------------------------------------------------------------------------- /build_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | http_proxy=${http_proxy:-} 5 | https_proxy=${https_proxy:-} 6 | ftp_proxy=${ftp_proxy:-} 7 | 8 | PACKAGES=$(sed "s/\n/ /g" packages) 9 | 10 | docker build -t crashcart-builder \ 11 | --build-arg http_proxy="${http_proxy}" \ 12 | --build-arg https_proxy="${https_proxy}" \ 13 | --build-arg ftp_proxy="${ftp_proxy}" \ 14 | builder 15 | 16 | docker run --privileged --rm -i \ 17 | -e "PACKAGES=${PACKAGES}" \ 18 | -e http_proxy="${http_proxy}" \ 19 | -e https_proxy="${https_proxy}" \ 20 | -e ftp_proxy="${ftp_proxy}" \ 21 | -v "${PWD}"/vol:/dev/crashcart crashcart-builder /dev/crashcart/build_crashcart.sh 22 | 23 | mv -f vol/crashcart.img . 24 | -------------------------------------------------------------------------------- /builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | ENV USER=root 4 | ARG nversion=1.11.15 5 | ARG nsha=57bebb9718c3e12dfed6ae5ac0aa6960d8cc73efb01aecd0e6d2854c48c39444 6 | RUN apt-get update && apt-get -y install curl build-essential pkg-config autotools-dev dh-autoreconf libssl-dev libbz2-dev libsqlite3-dev libcurl4-openssl-dev liblzma-dev libgc-dev libdbi-perl libdbd-sqlite3-perl libwww-curl-perl libxml2 libxslt-dev libseccomp-dev \ 7 | && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 8 | RUN echo 'nixbld:x:998:nobody' >> /etc/group && \ 9 | curl -OL https://nixos.org/releases/nix/nix-${nversion}/nix-${nversion}.tar.bz2 && \ 10 | echo "${nsha} nix-${nversion}.tar.bz2" | sha256sum -c && \ 11 | tar -xjf nix-${nversion}.tar.bz2 && \ 12 | cd nix-${nversion} && \ 13 | ./configure --localstatedir=/dev/crashcart/var --with-store-dir=/dev/crashcart/store && \ 14 | make && \ 15 | make install && \ 16 | cd - && \ 17 | rm -rf nix-${nversion}* 18 | -------------------------------------------------------------------------------- /crashcart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oracle/crashcart/3d0349fd383f09d9686a9d2634d3292ad2ac396b/crashcart.png -------------------------------------------------------------------------------- /packages: -------------------------------------------------------------------------------- 1 | bash-interactive 2 | binutils 3 | bzip2 4 | coreutils 5 | curl 6 | gdb 7 | gnutar 8 | gzip 9 | lsof 10 | ltrace 11 | man 12 | netcat-gnu 13 | openssl 14 | pigz 15 | strace 16 | tcpdump 17 | wget 18 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | error_chain! { 2 | types { 3 | Error, ErrorKind, ResultExt, Result; 4 | } 5 | foreign_links { 6 | Nix(::nix::Error); 7 | Io(::std::io::Error); 8 | Caps(::caps::Error); 9 | } 10 | // errors { 11 | // InvalidSpec(t: String) { 12 | // description("invalid spec") 13 | // display("invalid spec: '{}'", t) 14 | // } 15 | // SeccompError(t: String) { 16 | // description("seccomp spec") 17 | // display("seccomp error: '{}'", t) 18 | // } 19 | // } 20 | } 21 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use log::{Log, LogRecord, LogLevel, LogMetadata}; 2 | 3 | pub struct SimpleLogger; 4 | 5 | impl Log for SimpleLogger { 6 | fn enabled(&self, metadata: &LogMetadata) -> bool { 7 | metadata.level() <= LogLevel::Debug 8 | } 9 | 10 | fn log(&self, record: &LogRecord) { 11 | if self.enabled(record.metadata()) { 12 | println!("{} - {}", record.level(), record.args()); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/loopback.rs: -------------------------------------------------------------------------------- 1 | use libc; 2 | use nix::{Errno, Result}; 3 | use std::os::unix::io::RawFd; 4 | 5 | const LOOP_MAJOR: u64 = 7; 6 | 7 | #[cfg(target_env = "musl")] 8 | const LOOP_SET_FD: libc::c_int = 0x4C00; 9 | #[cfg(target_env = "musl")] 10 | const LOOP_CTL_GET_FREE: libc::c_int = 0x4C82; 11 | #[cfg(not(target_env = "musl"))] 12 | const LOOP_SET_FD: libc::c_ulong = 0x4C00; 13 | #[cfg(not(target_env = "musl"))] 14 | const LOOP_CTL_GET_FREE: libc::c_ulong = 0x4C82; 15 | 16 | pub fn loop_ctl_get_free(fd: RawFd) -> Result { 17 | let devnr = unsafe { libc::ioctl(fd, LOOP_CTL_GET_FREE) }; 18 | Errno::result(devnr) 19 | } 20 | 21 | pub fn loop_set_fd(fd: RawFd, source: RawFd) -> Result<()> { 22 | let res = unsafe { libc::ioctl(fd, LOOP_SET_FD, source) }; 23 | Errno::result(res).map(drop) 24 | } 25 | 26 | pub fn makedev(major: u64, minor: u64) -> u64 { 27 | (minor & 0xff) | ((major & 0xfff) << 8) | ((minor & !0xff) << 12) | ((major & !0xfff) << 32) 28 | } 29 | 30 | pub fn loopdev(devnr: i32) -> u64 { 31 | makedev(LOOP_MAJOR, devnr as u64) 32 | } 33 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate caps; 2 | #[macro_use] 3 | extern crate error_chain; 4 | extern crate getopts; 5 | extern crate glob; 6 | extern crate libc; 7 | #[macro_use] 8 | extern crate log; 9 | extern crate nix; 10 | #[macro_use] 11 | extern crate scopeguard; 12 | 13 | mod errors; 14 | mod logger; 15 | mod loopback; 16 | 17 | use errors::*; 18 | use getopts::Options; 19 | use glob::glob; 20 | use nix::c_int; 21 | use nix::fcntl::{open, OFlag, O_RDWR, O_CREAT, flock, FlockArg}; 22 | use nix::mount::{mount, umount, MS_RDONLY, MsFlags}; 23 | use nix::mount::{MS_REMOUNT, MS_NOSUID, MS_STRICTATIME}; 24 | use nix::sched::{CloneFlags, CLONE_NEWUSER, CLONE_NEWNET, CLONE_NEWCGROUP}; 25 | use nix::sched::{setns, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWIPC, CLONE_NEWUTS}; 26 | use nix::sys::signal::{sigaction, kill}; 27 | use nix::sys::signal::{SigAction, SigHandler, SaFlags, SigSet, Signal}; 28 | use nix::sys::stat::{mknod, S_IFBLK, Mode, fstat}; 29 | use nix::sys::wait::{waitpid, WaitStatus}; 30 | use nix::unistd::{close, fork, ForkResult, execvp, setresgid, setresuid}; 31 | use nix::Errno; 32 | use std::env; 33 | use std::fs::{read_link, create_dir_all, remove_file, remove_dir}; 34 | use std::fs::{File, canonicalize, metadata}; 35 | use std::io::{BufRead, BufReader}; 36 | use std::io::prelude::*; 37 | use std::os::unix::fs::symlink; 38 | use std::path::Path; 39 | use std::ffi::CString; 40 | 41 | 42 | const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); 43 | 44 | fn print_usage(program: &str, opts: &Options) { 45 | let brief = format!("Usage: {} [options] ID [--] [CMD]", program); 46 | print!("{}", opts.usage(&brief)); 47 | } 48 | 49 | 50 | fn mount_image(image: &str, link: &str) -> Result { 51 | // get free loop device 52 | let cfd = open("/dev/loop-control", OFlag::empty(), Mode::empty()) 53 | .chain_err(|| "failed to open /dev/loop-control")?; 54 | let devnr = loopback::loop_ctl_get_free(cfd).chain_err( 55 | || "failed to get free device", 56 | )?; 57 | defer!(close(cfd).unwrap()); 58 | 59 | // set backing file for loop device to image 60 | let lp = format!("/dev/loop{}", devnr); 61 | let lfd = open(&*lp, OFlag::empty(), Mode::empty()).chain_err(|| { 62 | format!("failed to open {}", lp) 63 | })?; 64 | defer!(close(lfd).unwrap()); 65 | 66 | let ifd = open(image, OFlag::empty(), Mode::empty()).chain_err(|| { 67 | format!("failed to open {}", image) 68 | })?; 69 | defer!(close(ifd).unwrap()); 70 | 71 | loopback::loop_set_fd(lfd, ifd).chain_err(|| { 72 | format!("failed to set backing file to {}", image) 73 | })?; 74 | 75 | symlink(&lp, &link).chain_err(|| { 76 | format!("failed to symlink from {} to {}", link, lp) 77 | })?; 78 | 79 | info!("backed /dev/loop{} to {}", devnr, image); 80 | Ok(devnr) 81 | } 82 | 83 | macro_rules! maybe { 84 | ($e:expr) => (match $e { 85 | Ok(val) => val, 86 | Err(_) => { 87 | return false; 88 | }, 89 | }); 90 | } 91 | 92 | fn is_backing(devnr: i32, image: &str) -> bool { 93 | let path = format!("/sys/block/loop{}/loop/backing_file", devnr); 94 | let mut f = maybe!(File::open(path)); 95 | let mut backing = String::new(); 96 | maybe!(f.read_to_string(&mut backing)); 97 | let image_path = maybe!(canonicalize(&image)); 98 | let backing_path = maybe!(canonicalize(&backing.trim())); 99 | image_path == backing_path 100 | } 101 | 102 | fn is_readonly_dev(pid: u64) -> bool { 103 | let path = format!("/proc/{}/mounts", pid); 104 | let f = maybe!(File::open(path)); 105 | for line in BufReader::new(f).lines() { 106 | let l = match line { 107 | Ok(l) => l, 108 | Err(e) => { 109 | warn!("failed to read mount info: {}", e); 110 | return false; 111 | } 112 | }; 113 | let fields: Vec<&str> = l.split(' ').collect(); 114 | if fields.len() < 4 { 115 | warn!("mount data is corrupted"); 116 | continue; 117 | } 118 | if fields[1] != "/dev" { 119 | continue; 120 | } 121 | return fields[3].starts_with("ro"); 122 | } 123 | false 124 | } 125 | 126 | fn make_device(image: &str) -> Result { 127 | // create lock file 128 | let lockp = format!("{}.lock", image); 129 | let lockfd = open(&*lockp, O_RDWR | O_CREAT, Mode::from_bits_truncate(0o644)) 130 | .chain_err(|| format!("failed to open {}", lockp))?; 131 | defer!(close(lockfd).unwrap()); 132 | 133 | flock(lockfd, FlockArg::LockExclusive).chain_err(|| { 134 | format!("could not get lock on {}", lockp) 135 | })?; 136 | defer!(flock(lockfd, FlockArg::Unlock).unwrap()); 137 | defer!(remove_file(&lockp).unwrap()); 138 | 139 | let link = format!("{}.link", image); 140 | match read_link(&link) { 141 | Ok(m) => { 142 | let devnr = m.to_str().unwrap()["/dev/loop".len()..] 143 | .parse::() 144 | .unwrap(); 145 | if !is_backing(devnr, image) { 146 | remove_file(&link).chain_err( 147 | || format!("could not delete {}", link), 148 | )?; 149 | return mount_image(image, &link); 150 | }; 151 | info!("{} is backed to /dev/loop{}", image, devnr); 152 | Ok(devnr) 153 | } 154 | Err(e) => { 155 | if e.kind() != std::io::ErrorKind::NotFound { 156 | let msg = format!("could not read {}", image); 157 | Err(e).chain_err(|| msg) 158 | } else { 159 | mount_image(image, &link) 160 | } 161 | } 162 | } 163 | } 164 | 165 | const PID_GLOBS: &'static [&'static str] = &[ 166 | "/var/run/docker/libcontainerd/containerd/{}*/init/pid", 167 | "/var/lib/rkt/pods/run/{}*/pid", 168 | ]; 169 | 170 | fn get_pid(id: &str) -> Result { 171 | // NOTE: An alternative option for finding docker pids is to find the 172 | // docker cgroup hierarchy and read the first pid in the tasks file 173 | // tasks file associated with he container id, for example: 174 | // /sys/fs/cgroup/memory/docker/*/tasks 175 | let mut out = id.to_owned(); 176 | let mut pid_file = String::new(); 177 | for entry in PID_GLOBS { 178 | let results = glob(&entry.replace("{}", id)) 179 | .chain_err(|| format!("invalid glob for id {}", id))? 180 | .map(|s| s.unwrap().to_str().unwrap().to_string()) 181 | .collect::>(); 182 | match results.len() { 183 | 0 => (), 184 | 1 => pid_file = results[0].to_owned(), 185 | _ => bail!("id {} is ambiguous", id), 186 | } 187 | } 188 | if !pid_file.is_empty() { 189 | info!("Found pid_file at {}", pid_file); 190 | let mut f = File::open(&pid_file).chain_err(|| { 191 | format!("could not open {}", pid_file) 192 | })?; 193 | out = String::new(); 194 | f.read_to_string(&mut out).chain_err(|| { 195 | format!("could not read {}", pid_file) 196 | })?; 197 | } 198 | out.parse::().chain_err( 199 | || format!("{} is not a valid pid", out), 200 | ) 201 | } 202 | 203 | const NAMESPACES: &[(CloneFlags, &'static str)] = &[ 204 | (CLONE_NEWIPC, "ipc"), 205 | (CLONE_NEWUTS, "uts"), 206 | (CLONE_NEWNET, "net"), 207 | (CLONE_NEWPID, "pid"), 208 | (CLONE_NEWNS, "mnt"), 209 | (CLONE_NEWCGROUP, "cgroup"), 210 | (CLONE_NEWUSER, "user"), 211 | ]; 212 | 213 | fn enter_namespaces(pid: u64, namespaces: CloneFlags) -> Result<()> { 214 | let mut to_enter = Vec::new(); 215 | for &(space, name) in NAMESPACES { 216 | if namespaces.contains(space) { 217 | debug!("entering {} namespace of {}", name, pid); 218 | let oldpath = format!("/proc/self/ns/{}", name); 219 | let oldfd = match open(&*oldpath, OFlag::empty(), Mode::empty()) { 220 | Err(e) => { 221 | if e.errno() == Errno::ENOENT { 222 | continue; 223 | } 224 | let msg = format!("failed to open {}", oldpath); 225 | return Err(e).chain_err(|| msg)?; 226 | } 227 | Ok(fd) => fd, 228 | }; 229 | let stat = fstat(oldfd).chain_err(|| "failed to stat")?; 230 | close(oldfd).unwrap(); 231 | let newpath = format!("/proc/{}/ns/{}", pid, name); 232 | let fd = match open(&*newpath, OFlag::empty(), Mode::empty()) { 233 | Err(e) => { 234 | if e.errno() == Errno::ENOENT { 235 | continue; 236 | } 237 | let msg = format!("failed to open {}", oldpath); 238 | return Err(e).chain_err(|| msg); 239 | } 240 | Ok(fd) => fd, 241 | }; 242 | let nstat = fstat(fd).chain_err(|| "failed to stat")?; 243 | if stat.st_dev == nstat.st_dev && stat.st_ino == nstat.st_ino { 244 | close(fd).unwrap(); 245 | } else { 246 | to_enter.push((space, fd)); 247 | } 248 | } 249 | } 250 | for &(space, fd) in &to_enter { 251 | setns(fd, space).chain_err(|| "failed to enter")?; 252 | close(fd).unwrap(); 253 | if space == CLONE_NEWUSER { 254 | setresgid(0, 0, 0).chain_err(|| "failed to setgid")?; 255 | setresuid(0, 0, 0).chain_err(|| "failed to setuid")?; 256 | } 257 | } 258 | Ok(()) 259 | } 260 | 261 | fn enter_mount_ns(pid: u64) -> Result Result<()>)>> { 262 | let origpath = "/proc/self/ns/mnt"; 263 | let ofd = open(origpath, OFlag::empty(), Mode::empty()).chain_err( 264 | || { 265 | format!("failed to open {}", origpath) 266 | }, 267 | )?; 268 | 269 | // enter ns and return closure to reset 270 | let cwd = env::current_dir().chain_err(|| "failed to get cwd")?; 271 | enter_namespaces(pid, CLONE_NEWNS)?; 272 | Ok(Box::new(move || { 273 | setns(ofd, CLONE_NEWNS).chain_err(|| "failed to setns")?; 274 | close(ofd).chain_err( 275 | || format!("failed to close {}", origpath), 276 | )?; 277 | env::set_current_dir(&cwd).chain_err(|| "failed to set cwd")?; 278 | Ok(()) 279 | })) 280 | } 281 | 282 | fn enter_pid_ns(pid: u64) -> Result Result<()>)>> { 283 | let origpath = "/proc/self/ns/pid"; 284 | let ofd = open(origpath, OFlag::empty(), Mode::empty()).chain_err( 285 | || { 286 | format!("failed to open {}", origpath) 287 | }, 288 | )?; 289 | 290 | // enter ns and return closure to reset 291 | enter_namespaces(pid, CLONE_NEWPID)?; 292 | Ok(Box::new(move || { 293 | setns(ofd, CLONE_NEWPID).chain_err(|| "failed to setns")?; 294 | close(ofd).chain_err( 295 | || format!("failed to close {}", origpath), 296 | )?; 297 | Ok(()) 298 | })) 299 | } 300 | 301 | fn find_root(path: &str) -> Result { 302 | let mut file = match File::open(path) { 303 | Err(e) => { 304 | if e.kind() == std::io::ErrorKind::NotFound { 305 | return Ok(0); 306 | } 307 | let msg = format!("failed to open {}", path); 308 | return Err(e).chain_err(|| msg); 309 | } 310 | Ok(f) => f, 311 | }; 312 | let mut contents = String::new(); 313 | file.read_to_string(&mut contents).unwrap(); 314 | for line in contents.split('\n') { 315 | let words: Vec<&str> = line.trim().split_whitespace().collect(); 316 | if words.len() < 2 { 317 | continue; 318 | } 319 | if words[0] == "0" { 320 | return Ok( 321 | words[1].parse::().chain_err(|| "failed to parse root")?, 322 | ); 323 | } 324 | } 325 | Ok(0) 326 | } 327 | 328 | fn set_fsids(pid: u64) -> Result ())>> { 329 | let uid = find_root(&format!("/proc/{}/uid_map", pid))?; 330 | let gid = find_root(&format!("/proc/{}/gid_map", pid))?; 331 | if uid == 0 && gid == 0 { 332 | return Ok(Box::new(|| {})); 333 | } 334 | // set the filesystem ids 335 | unsafe { 336 | libc::setfsgid(gid); 337 | libc::setfsuid(uid); 338 | } 339 | // reset capabilities (to get CAP_MKNOD back) 340 | let mut all = caps::CapsHashSet::new(); 341 | for c in caps::Capability::iter_variants() { 342 | all.insert(c); 343 | } 344 | caps::set(None, caps::CapSet::Effective, all).chain_err( 345 | || "failed to set capabilities", 346 | )?; 347 | Ok(Box::new(|| unsafe { 348 | libc::setfsgid(0); 349 | libc::setfsuid(0); 350 | })) 351 | } 352 | 353 | fn is_mounted(path: &str) -> Result { 354 | let fd = match open(path, OFlag::empty(), Mode::empty()) { 355 | Err(e) => { 356 | if e.errno() == Errno::ENOENT { 357 | return Ok(false); 358 | } 359 | let msg = format!("failed to open {}", path); 360 | return Err(e).chain_err(|| msg); 361 | } 362 | Ok(fd) => fd, 363 | }; 364 | let stat = fstat(fd).chain_err(|| "failed to stat")?; 365 | close(fd).unwrap(); 366 | let ppath = match Path::new(path).parent() { 367 | None => { 368 | return Ok(false); 369 | } 370 | Some(p) => p, 371 | }; 372 | let pfd = match open(ppath, OFlag::empty(), Mode::empty()) { 373 | Err(e) => { 374 | if e.errno() == Errno::ENOENT { 375 | return Ok(false); 376 | } 377 | let msg = format!("failed to open {:?}", ppath); 378 | return Err(e).chain_err(|| msg); 379 | } 380 | Ok(fd) => fd, 381 | }; 382 | let pstat = fstat(pfd).chain_err(|| "failed to stat")?; 383 | close(fd).unwrap(); 384 | Ok(stat.st_dev != pstat.st_dev) 385 | } 386 | 387 | const CC_LOOP_TMP: &'static str = "/dev/cc-loop"; 388 | const CC_MOUNT_PATH: &'static str = "/dev/crashcart"; 389 | 390 | fn do_mount(pid: u64, image: &str) -> Result<()> { 391 | let devnr = make_device(image)?; 392 | // if we are in a userns, make sure that we have the right fsids 393 | let reset_fsids = set_fsids(pid)?; 394 | defer!(reset_fsids()); 395 | let readonly = is_readonly_dev(pid); 396 | let exit_mount_ns = enter_mount_ns(pid)?; 397 | defer!(exit_mount_ns().unwrap()); 398 | 399 | if readonly { 400 | // TODO: The userns should be entered first so don't mess up the 401 | // permissions on remount. 402 | if let Err(e) = mount( 403 | Some("/dev"), 404 | "/dev", 405 | None::<&str>, 406 | MS_REMOUNT | MS_NOSUID | MS_STRICTATIME, 407 | None::<&str>, 408 | ) 409 | { 410 | if e.errno() != Errno::EBUSY { 411 | warn!("could not remount dev read/write"); 412 | } 413 | } 414 | let sentinel = "/dev/readonly"; 415 | let fd = open(sentinel, O_RDWR | O_CREAT, Mode::from_bits_truncate(0o644)) 416 | .chain_err(|| format!("failed to open {}", sentinel))?; 417 | close(fd).unwrap(); 418 | 419 | } 420 | 421 | // NOTE: the default dev device inside a user namespace can not hold 422 | // loopback devices, so we create a new tmpfs mount from the 423 | // init_user_ns to hold the device 424 | if !is_mounted(CC_LOOP_TMP)? { 425 | create_dir_all(CC_LOOP_TMP).chain_err(|| { 426 | format!("failed to create {}", CC_LOOP_TMP) 427 | })?; 428 | if let Err(e) = mount( 429 | Some("tmpfs"), 430 | CC_LOOP_TMP, 431 | Some("tmpfs"), 432 | MsFlags::empty(), 433 | None::<&str>, 434 | ) 435 | { 436 | if e.errno() != Errno::EBUSY { 437 | let msg = format!("could not mount tmpfs to {}", CC_LOOP_TMP); 438 | Err(e).chain_err(|| msg)?; 439 | } 440 | } 441 | } 442 | let ccimage = format!("{}/loop{}", CC_LOOP_TMP, devnr); 443 | if let Err(e) = mknod( 444 | &*ccimage, 445 | S_IFBLK, 446 | Mode::from_bits_truncate(0o660), 447 | loopback::loopdev(devnr), 448 | ) 449 | { 450 | if e.errno() != Errno::EEXIST { 451 | let msg = format!("could not mknod {}", ccimage); 452 | Err(e).chain_err(|| msg)?; 453 | } 454 | } 455 | if !is_mounted(CC_MOUNT_PATH)? { 456 | create_dir_all(CC_MOUNT_PATH).chain_err(|| { 457 | format!("failed to create {}", CC_MOUNT_PATH) 458 | })?; 459 | if let Err(e) = mount( 460 | Some(&*ccimage), 461 | CC_MOUNT_PATH, 462 | Some("ext3"), 463 | MS_RDONLY, 464 | None::<&str>, 465 | ) 466 | { 467 | if e.errno() != Errno::EBUSY { 468 | let msg = format!("could not mount {} to {}", ccimage, CC_MOUNT_PATH); 469 | Err(e).chain_err(|| msg)?; 470 | } 471 | } 472 | } 473 | info!("{} is loaded into namespace of pid {}", image, pid); 474 | Ok(()) 475 | } 476 | 477 | static mut CHILD_PID: i32 = 0; 478 | 479 | extern "C" fn signal_handler(signo: c_int) { 480 | // the unsafe is due to usage of CHILD_PID, although it is safe to use it 481 | // as mentioned below. 482 | unsafe { 483 | // ignoring errors since it isn't safe to do anything with them in the 484 | // signal handler. 485 | let _ = kill(CHILD_PID, Signal::from_c_int(signo).unwrap()); 486 | } 487 | } 488 | 489 | const DEFAULT_ARGS: &'static [&'static str] = &[ 490 | "/dev/crashcart/bin/bash", 491 | "--rcfile", 492 | "/dev/crashcart/.crashcartrc", 493 | "-i", 494 | ]; 495 | 496 | fn do_exec(pid: u64, docker_id: &str, args: &[&str]) -> Result { 497 | let a = if args.is_empty() { 498 | &DEFAULT_ARGS[..] 499 | } else { 500 | args 501 | }; 502 | if !docker_id.is_empty() { 503 | let mut all = Vec::new(); 504 | all.push(CString::new("docker").unwrap()); 505 | all.push(CString::new("exec").unwrap()); 506 | all.push(CString::new("-it").unwrap()); 507 | all.push(CString::new(docker_id.to_string()).chain_err( 508 | || "invalid docker id", 509 | )?); 510 | let mut other: Vec = a.iter() 511 | .map(|s| CString::new(s.to_string()).unwrap()) 512 | .collect(); 513 | all.append(&mut other); 514 | execvp(&all[0], &all).chain_err(|| "failed to exec")?; 515 | } 516 | 517 | // enter pid namespace before fork 518 | let exit_pid_ns = enter_pid_ns(pid)?; 519 | 520 | match fork().chain_err(|| "failed to fork")? { 521 | ForkResult::Child => { 522 | // enter remaining namespaces 523 | enter_namespaces( 524 | pid, 525 | CLONE_NEWUSER | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWCGROUP | 526 | CLONE_NEWNET, 527 | )?; 528 | // child execs parameters or execs docker_exec 529 | let all: Vec = a.iter() 530 | .map(|s| CString::new(s.to_string()).unwrap()) 531 | .collect(); 532 | execvp(&all[0], &all).chain_err(|| "failed to exec")?; 533 | Ok(-1) 534 | } 535 | ForkResult::Parent { child } => { 536 | // parent waits for child to exit, passing along signals 537 | unsafe { 538 | // NOTE: the child pid is only set once prior to setting up the 539 | // signal handler, so it should be safe to access it from the 540 | // signal handler. 541 | CHILD_PID = child; 542 | let a = SigAction::new( 543 | SigHandler::Handler(signal_handler), 544 | SaFlags::empty(), 545 | SigSet::all(), 546 | ); 547 | sigaction(Signal::SIGTERM, &a).chain_err( 548 | || "failed to sigaction", 549 | )?; 550 | sigaction(Signal::SIGQUIT, &a).chain_err( 551 | || "failed to sigaction", 552 | )?; 553 | sigaction(Signal::SIGINT, &a).chain_err( 554 | || "failed to sigaction", 555 | )?; 556 | sigaction(Signal::SIGHUP, &a).chain_err( 557 | || "failed to sigaction", 558 | )?; 559 | sigaction(Signal::SIGUSR1, &a).chain_err( 560 | || "failed to sigaction", 561 | )?; 562 | sigaction(Signal::SIGUSR2, &a).chain_err( 563 | || "failed to sigaction", 564 | )?; 565 | } 566 | let mut exit_code = -1; 567 | while exit_code == -1 { 568 | let result = match waitpid(child, None) { 569 | Err(e) => { 570 | // ignore EINTR as it gets sent when we get a SIGCHLD 571 | if e.errno() != Errno::EINTR { 572 | let msg = format!("could not waitpid on {}", child); 573 | Err(e).chain_err(|| msg)?; 574 | } 575 | WaitStatus::StillAlive 576 | } 577 | Ok(result) => result, 578 | 579 | }; 580 | match result { 581 | WaitStatus::Exited(_, code) => exit_code = code as i32, 582 | WaitStatus::Signaled(_, signal, _) => exit_code = signal as i32 + 128, 583 | _ => (), 584 | }; 585 | } 586 | // reset pid namespace 587 | exit_pid_ns().chain_err(|| "failed to exit pid ns")?; 588 | Ok(exit_code) 589 | } 590 | } 591 | } 592 | 593 | fn do_unmount_ns(pid: u64, devnr: i32) -> Result<()> { 594 | // if we are in a userns, make sure that we have the right fsids 595 | let reset_fsids = set_fsids(pid)?; 596 | defer!(reset_fsids()); 597 | let exit_mount_ns = enter_mount_ns(pid)?; 598 | defer!(exit_mount_ns().unwrap()); 599 | 600 | let ccimage = format!("{}/loop{}", CC_LOOP_TMP, devnr); 601 | if let Err(e) = umount(CC_MOUNT_PATH) { 602 | if e.errno() != Errno::ENOENT { 603 | let msg = format!("could not unmount {} from {}", ccimage, CC_MOUNT_PATH); 604 | Err(e).chain_err(|| msg)?; 605 | } 606 | } 607 | if let Err(e) = remove_dir(CC_MOUNT_PATH) { 608 | if e.kind() != std::io::ErrorKind::NotFound { 609 | let msg = format!("could not delete {}", CC_MOUNT_PATH); 610 | Err(e).chain_err(|| msg)?; 611 | } 612 | } 613 | if let Err(e) = remove_file(&ccimage) { 614 | if e.kind() != std::io::ErrorKind::NotFound { 615 | let msg = format!("could not delete {}", &ccimage); 616 | Err(e).chain_err(|| msg)?; 617 | } 618 | } 619 | if let Err(e) = umount(CC_LOOP_TMP) { 620 | if e.errno() != Errno::ENOENT { 621 | let msg = format!("could not unmount tmpfs from {}", CC_LOOP_TMP); 622 | Err(e).chain_err(|| msg)?; 623 | } 624 | } 625 | if let Err(e) = remove_dir(CC_LOOP_TMP) { 626 | if e.kind() != std::io::ErrorKind::NotFound { 627 | let msg = format!("could not delete {}", CC_LOOP_TMP); 628 | Err(e).chain_err(|| msg)?; 629 | } 630 | } 631 | let sentinel = "/dev/readonly"; 632 | if metadata(sentinel).is_ok() { 633 | // TODO: The userns should be entered first so don't mess up the 634 | // permissions on remount. 635 | remove_file(sentinel).unwrap(); 636 | if let Err(e) = mount( 637 | Some("/dev"), 638 | "/dev", 639 | None::<&str>, 640 | MS_REMOUNT | MS_RDONLY | MS_NOSUID | MS_STRICTATIME, 641 | None::<&str>, 642 | ) 643 | { 644 | if e.errno() != Errno::EBUSY { 645 | warn!("could not remount dev readonly"); 646 | } 647 | } 648 | } 649 | Ok(()) 650 | } 651 | 652 | fn do_unmount(pid: u64, image: &str) -> Result<()> { 653 | let link = format!("{}.link", image); 654 | match read_link(&link) { 655 | Ok(m) => { 656 | let devnr = m.to_str().unwrap()["/dev/loop".len()..] 657 | .parse::() 658 | .unwrap(); 659 | if is_backing(devnr, image) { 660 | do_unmount_ns(pid, devnr)?; 661 | }; 662 | } 663 | Err(e) => { 664 | if e.kind() != std::io::ErrorKind::NotFound { 665 | let msg = format!("could not read {}", image); 666 | Err(e).chain_err(|| msg)?; 667 | } 668 | } 669 | } 670 | info!("{} is unloaded from namespace of pid {}", image, pid); 671 | Ok(()) 672 | } 673 | 674 | // only show backtrace in debug mode 675 | #[cfg(not(debug_assertions))] 676 | fn print_backtrace(_: &Error) {} 677 | 678 | #[cfg(debug_assertions)] 679 | fn print_backtrace(e: &Error) { 680 | match e.backtrace() { 681 | Some(backtrace) => error!("{:?}", backtrace), 682 | None => error!("to view backtrace, use RUST_BACKTRACE=1"), 683 | } 684 | } 685 | 686 | fn main() { 687 | if let Err(ref e) = run() { 688 | error!("{}", e); 689 | 690 | for e in e.iter().skip(1) { 691 | error!("caused by: {}", e); 692 | } 693 | 694 | print_backtrace(e); 695 | unsafe { 696 | if CHILD_PID != 0 { 697 | kill(CHILD_PID, Signal::SIGTERM).unwrap(); 698 | } 699 | } 700 | ::std::process::exit(1); 701 | } 702 | } 703 | 704 | fn run() -> Result<()> { 705 | let args: Vec = env::args().collect(); 706 | let program = &args[0]; 707 | 708 | let mut opts = Options::new(); 709 | opts.optopt("i", "image", "image to mount ", "IMAGE"); 710 | opts.optflag("h", "help", "display this help and exit"); 711 | opts.optflag("m", "mount", "mount only (do not run command)"); 712 | opts.optflag("e", "exec", "use docker exec instead of setns"); 713 | opts.optflag("u", "unmount", "unmount only (do not run command)"); 714 | opts.optflag("V", "version", "output version information and exit"); 715 | opts.optflag("v", "verbose", "enable more verbose logging"); 716 | 717 | let matches = opts.parse(&args[1..]).chain_err( 718 | || "unable to parse options", 719 | )?; 720 | 721 | if matches.opt_present("h") { 722 | println!("crashcart - mount crashcart image in container"); 723 | println!(""); 724 | print_usage(program, &opts); 725 | return Ok(()); 726 | } 727 | 728 | if matches.opt_present("V") { 729 | println!("{} version: {}", program, VERSION.unwrap_or("unknown")); 730 | return Ok(()); 731 | } 732 | 733 | let level = if matches.opt_present("v") { 734 | log::LogLevelFilter::Debug 735 | } else { 736 | log::LogLevelFilter::Info 737 | }; 738 | 739 | let _ = log::set_logger(|max_log_level| { 740 | max_log_level.set(level); 741 | Box::new(logger::SimpleLogger) 742 | }); 743 | 744 | let image = matches.opt_str("i").unwrap_or_else( 745 | || "crashcart.img".to_string(), 746 | ); 747 | 748 | let id = if !matches.free.is_empty() { 749 | matches.free[0].clone() 750 | } else { 751 | print_usage(program, &opts); 752 | return Ok(()); 753 | }; 754 | 755 | let pid = get_pid(&id)?; 756 | if !matches.opt_present("u") { 757 | do_mount(pid, &image)?; 758 | } 759 | 760 | let exit_code = if !matches.opt_present("u") && !matches.opt_present("m") { 761 | let a: Vec<&str> = matches.free.iter().map(AsRef::as_ref).collect(); 762 | let docker_id = if matches.opt_present("e") { 763 | id 764 | } else { 765 | String::new() 766 | }; 767 | do_exec(pid, &docker_id, &a[1..])? 768 | } else { 769 | 0 770 | }; 771 | 772 | 773 | if !matches.opt_present("m") { 774 | do_unmount(pid, &image)?; 775 | } 776 | ::std::process::exit(exit_code); 777 | } 778 | -------------------------------------------------------------------------------- /vol/.crashcartrc: -------------------------------------------------------------------------------- 1 | CART=/dev/crashcart 2 | export PATH=$CART/sbin:$CART/bin:$PATH 3 | -------------------------------------------------------------------------------- /vol/build_crashcart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | PACKAGES=${PACKAGES:-} 5 | 6 | cd /dev/crashcart/ 7 | nix-channel --list | grep nixos-17.09 || nix-channel --add https://nixos.org/channels/nixos-17.09 8 | nix-channel --update 9 | 10 | # workaround for ncurses 11 | sed -i 's;20170902;20180106;g' \ 12 | ~/.nix-defexpr/channels/nixos-17.09/pkgs/development/libraries/ncurses/default.nix 13 | sed -i 's;1cks4gsz4148jw6wpqia4w5jx7cfxr29g2kmpvp0ssmvwczh8dr4;27a178398314b81c27d54672b42b6bb4475c77e72f126dbedde8f8bf220d081e;g' \ 14 | ~/.nix-defexpr/channels/nixos-17.09/pkgs/development/libraries/ncurses/default.nix 15 | 16 | rm -f profile 17 | nix-env -p profile -i ${PACKAGES} 18 | rm -f crashcart.img 19 | truncate -s 1G crashcart.img 20 | mkfs.ext3 crashcart.img 21 | mkdir -p out 22 | mount -t ext2 -o loop crashcart.img out 23 | ln -s "$(readlink -f profile)" out/profile 24 | ln -s profile/bin out/bin 25 | ln -s profile/sbin out/sbin 26 | cp .crashcartrc out/ 27 | mkdir -p out/store 28 | for deps in $(nix-store -qR profile); do 29 | cp -a "${deps#/dev/crashcart/*}" out/store/ 30 | done 31 | umount out 32 | # We expect this to return 1 33 | set +e 34 | e2fsck -f crashcart.img 35 | set -e 36 | resize2fs -M crashcart.img 37 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: scorpil/rust:stable 2 | build: 3 | steps: 4 | - install-packages: 5 | packages: build-essential 6 | - script: 7 | name: build 8 | code: ./build.sh 9 | 10 | build-musl: 11 | box: 12 | id: ekidd/rust-musl-builder:stable 13 | cmd: /usr/bin/sudo -E /bin/bash 14 | steps: 15 | - script: 16 | name: update path 17 | code: export PATH=$PATH:/home/rust/.cargo/bin 18 | - script: 19 | name: build 20 | code: TARGET=x86_64-unknown-linux-musl ./build.sh 21 | 22 | build-nightly: 23 | box: scorpil/rust:nightly 24 | steps: 25 | - install-packages: 26 | packages: build-essential 27 | - script: 28 | name: build 29 | code: ./build.sh 30 | --------------------------------------------------------------------------------