├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bulk.yaml ├── examples ├── bind_mount.rs ├── move.rs ├── overlay_readonly.rs ├── overlay_writable.rs ├── remount.rs └── tmpfs.rs ├── src ├── bind.rs ├── error.rs ├── escape.rs ├── explain.rs ├── lib.rs ├── modify.rs ├── mountinfo.rs ├── overlay.rs ├── remount.rs ├── tmpfs.rs └── util.rs └── vagga.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | /.vagga 4 | /tmp 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: stable 3 | os: linux 4 | dist: trusty 5 | sudo: false 6 | addons: 7 | apt: 8 | packages: 9 | - fakeroot 10 | - musl-tools 11 | 12 | cache: 13 | - apt 14 | - cargo 15 | 16 | before_cache: 17 | - rm -r $TRAVIS_BUILD_DIR/target/debug 18 | 19 | script: 20 | - cargo build $CARGO_ARGS 21 | - cargo test $CARGO_ARGS 22 | 23 | jobs: 24 | include: 25 | - rust: stable 26 | - rust: beta 27 | - rust: nightly 28 | 29 | # deploy 30 | - stage: publish docs 31 | env: 32 | # GH_TOKEN 33 | - secure: "g8UoQdG0w09xquGMDmnvkCO3b4/jb00OrFvXgCrN9g8m7RrX+ZQ98Ztjjp9Dr4Sni1U3V9LrTFrEFv9+vrNJJX6TQ2rLOZOF+IvlYLFOsz6LcNHTqcnAYJoTy+VqsXTwkNs3TP9LwUnxX0X0N8GxyKAJJ0wSHZvNooDDi8x2jeGuT5AwyRd3S9zoBi4nlQdwU20NErH0i50ayEKE1SLQJcG3Xtl6OtHGqxHWgOPXNQwqY/042NOnMFiL40lvh3iS2xtM2sDwRdfWj+pmvji2FRMHlkC+g5JwC50KkeCL+VBrT8MZbUW8UWjDC9J1tV7zfA8HY0nURgYl6g2NWe0/7vTgx0oQM+9Kf0OcNWVBA2j7g8MVJA9/1JW6B11a7oFgRDmJLGGrqaIAndrviho/V1OQtB4l0h1ZF6iUVZwuRHdCd6wxY4ahCQHjnsGUxnzYAgU2e/7IL/gKXjYhUdly99Ah018XXZ2MqAXISoh3fpKJg52pKcfjnJr3bDs6mpBHVdHH/zfk3jWtmvcozPubRSXsjnLFeBnAGdLz8pWHq9tzRTIQoV1RDNo4a73ck9diti52HhKfWFGojmrAXABWwkJ7wor0F3/uxHhbTpuHf+AFO8tgPyEamLFWL7WCbxFLXI7SSuewdCgEEV0Kz2Xrj0/prywHmaREUgblwyk8wpc=" 34 | # CARGO_TOKEN 35 | - secure: "AatVkDxYdzu8K1Qa6xlwVbfXu1YfuxfKd+jZoCFzKq1tP3OLtAbWsoZsiyGcgfDk3gq3oXHFbgjXObM/N6wA4U94mvZMralHkWc7eoECrfGjPz0ei8EOXzgFlWtE3keKBMvrCzIOUNt6+y20NsFgxfv9tSLWN7gRRWl8T+RQ6aO5p5kuI7mmN+e5y7QdtIEODeqRnpBl4wbG/BltJG0YrBoxt/R42inIHw0gUYZzAk8Xypjczb9aGdSwZmXxMw78n9Km4e+Z5/enUxam+qr/Rf4vBoBDpq3l7GIgl6ZHBHWAcKb9r8Vj1OJdwN0Bf4q13LRJNYISLCya0XYUZl0A4/2TOGBkO+RpPON8OoYTZcGSJ8O/5z9tuViSKoogmJipm8KTPh/u3yi4qdkt2hSdkn8OEpvDV53CCm9lQVTglE14uQCnrUYefXtrp3JPSIYb+TTqJW13p5LV/vr/lHDXk2oqU1eSL3b+se1kOogsVQOS+WDo5lvhDN6PbkU7+iLeYkFe7wVcDfZV/nf4QV4ZfMpgtXHPdy+PbhITnOZKuGA52ZTqi+tDn3RxHY0sGZFXLRpVZB2o73jJAc6t/uWlrEUQw1T2CJA3ohxfFlrF8WTItCAYT/pIR7Rr4qxloLkvVcWB5L3WsudZyJpUNrl3ch67yydpzSzr4xiv2X6u0Bo=" 36 | install: true 37 | 38 | deploy: 39 | - provider: script 40 | script: 'cargo publish --verbose --token=$CARGO_TOKEN' 41 | on: 42 | tags: true 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libmount" 3 | description = """ 4 | The type-safe wrapper around mount system call 5 | """ 6 | license = "MIT/Apache-2.0" 7 | readme = "README.md" 8 | keywords = ["linux", "container", "mount", "volume", "filesystem"] 9 | homepage = "http://github.com/tailhook/libmount" 10 | documentation = "http://docs.rs/libmount" 11 | version = "0.1.15" 12 | authors = ["paul@colomiets.name"] 13 | 14 | [dependencies] 15 | libc = "0.2.28" 16 | nix = "0.14" 17 | quick-error = "1.2.0" 18 | 19 | [dev-dependencies] 20 | argparse = "0.2.1" 21 | env_logger = "0.5.10" 22 | log = "0.4.1" 23 | 24 | [lib] 25 | name = "libmount" 26 | path = "src/lib.rs" 27 | 28 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The libmount Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | libmount 2 | ======== 3 | 4 | [Documentation](https://docs.rs/libmount) | 5 | [Github](https://github.com/tailhook/libmount) | 6 | [Crate](https://crates.io/crates/libmount) 7 | 8 | This is a higher-level wrapper around ``mount()`` system call for linux. 9 | 10 | Goals: 11 | 12 | 1. Type-safe wrapper, including mount options 13 | 2. Support of new features such as overlayfs 14 | 3. Good support of unprivileges user namespaces 15 | 4. Very detailed error messages, which are helpful for end users 16 | 17 | Features: 18 | 19 | * [x] Bind Mounts 20 | * [x] OverlayFS 21 | * [x] Tmpfs 22 | * [ ] Pseudo file systems: `proc`, `sys` 23 | * [ ] `umount` and `umount2` 24 | * [x] Parser of `/proc/PID/mountinfo` 25 | * [x] Read-only mounts (remount) 26 | * [ ] Ext2/3/4 27 | * [ ] Btrfs 28 | * [ ] Support of mount flags throught trait 29 | * [ ] Fuse 30 | 31 | 32 | License 33 | ======= 34 | 35 | Licensed under either of 36 | 37 | * Apache License, Version 2.0, (./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 38 | * MIT license (./LICENSE-MIT or http://opensource.org/licenses/MIT) 39 | 40 | at your option. 41 | 42 | Contribution 43 | ------------ 44 | 45 | Unless you explicitly state otherwise, any contribution intentionally 46 | submitted for inclusion in the work by you, as defined in the Apache-2.0 47 | license, shall be dual licensed as above, without any additional terms or 48 | conditions. 49 | -------------------------------------------------------------------------------- /bulk.yaml: -------------------------------------------------------------------------------- 1 | minimum-bulk: v0.4.5 2 | 3 | versions: 4 | 5 | - file: Cargo.toml 6 | block-start: ^\[package\] 7 | block-end: ^\[.*\] 8 | regex: ^version\s*=\s*"(\S+)" 9 | -------------------------------------------------------------------------------- /examples/bind_mount.rs: -------------------------------------------------------------------------------- 1 | extern crate libmount; 2 | extern crate argparse; 3 | extern crate env_logger; 4 | #[macro_use] extern crate log; 5 | 6 | use std::path::PathBuf; 7 | use std::process::exit; 8 | 9 | use argparse::{ArgumentParser, Parse, StoreFalse, StoreTrue}; 10 | 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let mut source = PathBuf::new(); 15 | let mut target = PathBuf::new(); 16 | let mut recursive = true; 17 | let mut readonly = false; 18 | { 19 | let mut ap = ArgumentParser::new(); 20 | ap.set_description("Bind mounting utility. Similar to `mount --bind`"); 21 | ap.refer(&mut source).add_argument("source", Parse, 22 | "Source directory for bind mount").required(); 23 | ap.refer(&mut target).add_argument("target", Parse, 24 | "Target directory for bind mount").required(); 25 | ap.refer(&mut recursive).add_option(&["--non-recursive"], StoreFalse, 26 | "Disable recursive mount (only a real superuser can do this)"); 27 | ap.refer(&mut readonly).add_option(&["--readonly"], StoreTrue, 28 | "Readonly mount"); 29 | ap.parse_args_or_exit(); 30 | } 31 | match libmount::BindMount::new(source, target) 32 | .recursive(recursive) 33 | .readonly(readonly) 34 | .mount() 35 | { 36 | Ok(()) => {} 37 | Err(e) => { 38 | error!("{}", e); 39 | exit(1); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/move.rs: -------------------------------------------------------------------------------- 1 | extern crate libmount; 2 | extern crate argparse; 3 | extern crate env_logger; 4 | #[macro_use] extern crate log; 5 | 6 | use std::path::PathBuf; 7 | use std::process::exit; 8 | 9 | use argparse::{ArgumentParser, Parse}; 10 | 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let mut source = PathBuf::new(); 15 | let mut target = PathBuf::new(); 16 | { 17 | let mut ap = ArgumentParser::new(); 18 | ap.set_description("Move mountpoint utility. \ 19 | Similar to `mount --move`"); 20 | ap.refer(&mut source).add_argument("source", Parse, 21 | "Source directory for bind mount").required(); 22 | ap.refer(&mut target).add_argument("target", Parse, 23 | "Target directory for bind mount").required(); 24 | ap.parse_args_or_exit(); 25 | } 26 | match libmount::Move::new(source, target).move_mountpoint() { 27 | Ok(()) => {} 28 | Err(e) => { 29 | error!("{}", e); 30 | exit(1); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/overlay_readonly.rs: -------------------------------------------------------------------------------- 1 | extern crate libmount; 2 | extern crate argparse; 3 | extern crate env_logger; 4 | #[macro_use] extern crate log; 5 | 6 | use std::path::PathBuf; 7 | use std::process::exit; 8 | 9 | use argparse::{ArgumentParser, Parse, Collect}; 10 | 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let mut lowerdirs = Vec::::new(); 15 | let mut target = PathBuf::new(); 16 | { 17 | let mut ap = ArgumentParser::new(); 18 | ap.set_description("Overlayfs mount utility. 19 | Similar to `mount -t overlay`"); 20 | ap.refer(&mut target) 21 | .add_argument("target", Parse, 22 | "The destination directory for mount").required(); 23 | ap.refer(&mut lowerdirs).add_argument("lowerdir", Collect, 24 | "The source layers of the overlay").required(); 25 | ap.parse_args_or_exit(); 26 | } 27 | match libmount::Overlay::readonly(lowerdirs.iter().map(|x| x.as_ref()), 28 | target) 29 | .mount() 30 | { 31 | Ok(()) => {} 32 | Err(e) => { 33 | error!("{}", e); 34 | exit(1); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/overlay_writable.rs: -------------------------------------------------------------------------------- 1 | extern crate libmount; 2 | extern crate argparse; 3 | extern crate env_logger; 4 | #[macro_use] extern crate log; 5 | 6 | use std::path::PathBuf; 7 | use std::process::exit; 8 | 9 | use argparse::{ArgumentParser, Parse, ParseOption, Collect}; 10 | 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let mut lowerdirs = Vec::::new(); 15 | let mut target = PathBuf::new(); 16 | let mut upper = PathBuf::new(); 17 | let mut work = None::; 18 | { 19 | let mut ap = ArgumentParser::new(); 20 | ap.set_description("Overlayfs mount utility. 21 | Similar to `mount -t overlay`"); 22 | ap.refer(&mut target).add_argument("target", Parse, 23 | "The destination directory for mount").required(); 24 | ap.refer(&mut upper).add_argument("upperdir", Parse, 25 | "The upper (writable) directory").required(); 26 | ap.refer(&mut work).add_argument("workdir", ParseOption, 27 | "The workdir, must be on the same filesystem as upperdir"); 28 | ap.refer(&mut lowerdirs).add_argument("lowerdir", Collect, 29 | "The source layers of the overlay").required(); 30 | ap.parse_args_or_exit(); 31 | } 32 | match libmount::Overlay::writable( 33 | lowerdirs.iter().map(|x| x.as_ref()), &upper, 34 | work.unwrap_or(upper.join("tmp")), target) 35 | .mount() 36 | { 37 | Ok(()) => {} 38 | Err(e) => { 39 | error!("{}", e); 40 | exit(1); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/remount.rs: -------------------------------------------------------------------------------- 1 | extern crate libmount; 2 | extern crate argparse; 3 | extern crate env_logger; 4 | #[macro_use] extern crate log; 5 | 6 | use std::path::PathBuf; 7 | use std::process::exit; 8 | 9 | use argparse::{ArgumentParser, Parse, StoreTrue}; 10 | 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let mut path = PathBuf::new(); 15 | let mut bind = false; 16 | let mut readonly = false; 17 | let mut nodev = false; 18 | let mut noexec = false; 19 | let mut nosuid = false; 20 | { 21 | let mut ap = ArgumentParser::new(); 22 | ap.set_description("Remount utility. Similar to `mount -o remount` \ 23 | but keeps current mount point options"); 24 | ap.refer(&mut path).add_argument("path", Parse, 25 | "Directory for remounting").required(); 26 | ap.refer(&mut bind).add_option(&["--bind"], StoreTrue, 27 | "Set bind mount option"); 28 | ap.refer(&mut readonly).add_option(&["--readonly"], StoreTrue, 29 | "Set readonly mount option"); 30 | ap.refer(&mut nodev).add_option(&["--nodev"], StoreTrue, 31 | "Set nodev mount option"); 32 | ap.refer(&mut noexec).add_option(&["--noexec"], StoreTrue, 33 | "Set noexec mount option"); 34 | ap.refer(&mut nosuid).add_option(&["--nosuid"], StoreTrue, 35 | "Set nosuid mount option"); 36 | ap.parse_args_or_exit(); 37 | } 38 | match libmount::Remount::new(path) 39 | .bind(bind) 40 | .readonly(readonly) 41 | .nodev(nodev) 42 | .noexec(noexec) 43 | .nosuid(nosuid) 44 | .remount() 45 | { 46 | Ok(()) => {} 47 | Err(e) => { 48 | error!("{}", e); 49 | exit(1); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/tmpfs.rs: -------------------------------------------------------------------------------- 1 | extern crate libmount; 2 | extern crate argparse; 3 | extern crate env_logger; 4 | #[macro_use] extern crate log; 5 | 6 | use std::path::PathBuf; 7 | use std::process::exit; 8 | 9 | use argparse::{ArgumentParser, Parse, StoreOption}; 10 | 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let mut target = PathBuf::new(); 15 | let mut size = None::; 16 | let mut mode = None::; 17 | let mut uid = None::; 18 | let mut gid = None::; 19 | { 20 | let mut ap = ArgumentParser::new(); 21 | ap.set_description("Tmpfs mount utility. Similar to `mount --tmpfs`"); 22 | ap.refer(&mut target).add_argument("target", Parse, 23 | "Target directory to mount tmpfs to").required(); 24 | ap.refer(&mut size).add_option(&["--size"], StoreOption, 25 | "Set size of the filesystem"); 26 | ap.refer(&mut mode).add_option(&["--mode"], StoreOption, 27 | "Set mode of the root directory"); 28 | ap.refer(&mut uid).add_option(&["--uid"], StoreOption, 29 | "Set uid of the directory"); 30 | ap.refer(&mut gid).add_option(&["--gid"], StoreOption, 31 | "Set gid of the directory"); 32 | ap.parse_args_or_exit(); 33 | } 34 | let mut mnt = libmount::Tmpfs::new(target); 35 | if let Some(x) = size { mnt = mnt.size_bytes(x); }; 36 | if let Some(ref x) = mode { 37 | mnt = mnt.mode(u32::from_str_radix(x, 8).expect("valid octal mode")); 38 | } 39 | if let Some(x) = uid { mnt = mnt.uid(x); } 40 | if let Some(x) = gid { mnt = mnt.gid(x); } 41 | match mnt.mount() { 42 | Ok(()) => {} 43 | Err(e) => { 44 | error!("{}", e); 45 | exit(1); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/bind.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::ffi::{CStr, CString, OsStr}; 3 | use std::os::unix::ffi::OsStrExt; 4 | use std::path::Path; 5 | 6 | use nix::mount::{MsFlags, mount}; 7 | 8 | use {OSError, Error}; 9 | use util::{path_to_cstring, as_path}; 10 | use explain::{Explainable, exists, user}; 11 | use remount::Remount; 12 | 13 | 14 | /// A mount bind definition 15 | /// 16 | /// By default bind mount is recursive (it's what you want most of the time). 17 | /// 18 | /// Also recursive mounts can be used in user namespaces. 19 | #[derive(Debug, Clone)] 20 | pub struct BindMount { 21 | source: CString, 22 | target: CString, 23 | recursive: bool, 24 | readonly: bool, 25 | } 26 | 27 | impl BindMount { 28 | /// Create a new, recursive bind mount 29 | /// 30 | /// You can disable recursion with a `non_recursive()` method 31 | pub fn new, B: AsRef>(source: A, target: B) 32 | -> BindMount 33 | { 34 | BindMount { 35 | source: path_to_cstring(source.as_ref()), 36 | target: path_to_cstring(target.as_ref()), 37 | recursive: true, 38 | readonly: false, 39 | } 40 | } 41 | /// Toggle recursion 42 | pub fn recursive(mut self, flag: bool) -> BindMount { 43 | self.recursive = flag; 44 | self 45 | } 46 | /// If set to `true` makes bind-mount readonly 47 | /// 48 | /// Few notes: 49 | /// 50 | /// 1. This makes additional `mount` call (`Remount().readonly()`) 51 | /// 2. If remount fails mount bind is left on the filesystem, no cleanup 52 | /// is done 53 | /// 3. If set to `false` is option is no-op (does **not** remount `rw`) 54 | pub fn readonly(mut self, flag: bool) -> BindMount { 55 | self.readonly = flag; 56 | self 57 | } 58 | 59 | /// Execute a bind mount 60 | pub fn bare_mount(self) -> Result<(), OSError> { 61 | let mut flags = MsFlags::MS_BIND; 62 | if self.recursive { 63 | flags = flags | MsFlags::MS_REC; 64 | } 65 | if let Err(err) = mount( 66 | Some(&*self.source), 67 | &*self.target, 68 | None::<&CStr>, 69 | flags, 70 | None::<&CStr>, 71 | ) { 72 | return Err(OSError::from_nix(err, Box::new(self))); 73 | } 74 | if self.readonly { 75 | try!(Remount::new(OsStr::from_bytes(self.target.as_bytes())) 76 | .bind(true) 77 | .readonly(true) 78 | .bare_remount()); 79 | } 80 | Ok(()) 81 | } 82 | 83 | /// Execute a bind mount and explain the error immediately 84 | pub fn mount(self) -> Result<(), Error> { 85 | self.bare_mount().map_err(OSError::explain) 86 | } 87 | } 88 | 89 | impl fmt::Display for BindMount { 90 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 91 | if self.recursive { 92 | try!(write!(fmt, "recursive ")); 93 | } 94 | write!(fmt, "bind mount {:?} -> {:?}", 95 | as_path(&self.source), as_path(&self.target)) 96 | } 97 | } 98 | 99 | impl Explainable for BindMount { 100 | fn explain(&self) -> String { 101 | [ 102 | format!("source: {}", exists(as_path(&self.source))), 103 | format!("target: {}", exists(as_path(&self.target))), 104 | format!("{}", user()), 105 | ].join(", ") 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::fmt; 3 | use std::error::Error as StdError; 4 | 5 | use {OSError, Error, MountError}; 6 | use remount::RemountError; 7 | 8 | impl OSError { 9 | /// Convert error to the one providing extra useful information 10 | pub fn explain(self) -> Error { 11 | let text = self.1.explain(); 12 | match self.0 { 13 | MountError::Io(e) => Error(self.1, e, text), 14 | MountError::Remount(RemountError::Io(msg, io_err)) => { 15 | Error(self.1, io_err, format!("{}, {}", msg, text)) 16 | }, 17 | MountError::Remount(err) => { 18 | let text = format!("{}, {}", &err, text); 19 | let err = Box::new(err); 20 | Error(self.1, 21 | io::Error::new(io::ErrorKind::InvalidData, err), 22 | text) 23 | }, 24 | } 25 | } 26 | } 27 | 28 | impl fmt::Display for OSError { 29 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 30 | write!(fmt, "{}: {}", self.1, self.0) 31 | } 32 | } 33 | 34 | impl StdError for OSError { 35 | fn cause(&self) -> Option<&StdError> { 36 | Some(&self.0) 37 | } 38 | fn description(&self) -> &str { 39 | self.0.description() 40 | } 41 | } 42 | 43 | impl fmt::Display for Error { 44 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 45 | write!(fmt, "{}: {} ({})", self.0, self.1, self.2) 46 | } 47 | } 48 | 49 | impl StdError for Error { 50 | fn cause(&self) -> Option<&StdError> { 51 | Some(&self.1) 52 | } 53 | fn description(&self) -> &str { 54 | self.1.description() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/escape.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailhook/libmount/0c824aa4a6be0f254606262c87d3ca0608bedc26/src/escape.rs -------------------------------------------------------------------------------- /src/explain.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::fs::File; 3 | use std::fmt::{Display, Debug}; 4 | use std::path::Path; 5 | 6 | use nix::unistd::getuid; 7 | 8 | 9 | pub trait Explainable: Display + Debug { 10 | fn explain(&self) -> String; 11 | } 12 | 13 | pub fn exists(path: &Path) -> &'static str { 14 | if path.exists() { 15 | "exists" 16 | } else { 17 | "missing" 18 | } 19 | } 20 | 21 | pub fn user() -> &'static str { 22 | let uid = getuid(); 23 | if u32::from(uid) == 0 { 24 | let mut buf = String::with_capacity(100); 25 | match File::open("/proc/self/uid_map") 26 | .and_then(|mut f| f.read_to_string(&mut buf)) 27 | { 28 | Ok(_) => { 29 | if buf == " 0 0 4294967295\n" { 30 | "superuser" 31 | } else { 32 | "mapped-root" 33 | } 34 | } 35 | Err(_) => { 36 | "privileged" 37 | } 38 | } 39 | } else { 40 | "regular-user" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # libmount 2 | //! 3 | //! [Documentation](https://docs.rs/libmount) | 4 | //! [Github](https://github.com/tailhook/libmount) | 5 | //! [Crate](https://crates.io/crates/libmount) 6 | //! 7 | //! This library has two major goals: 8 | //! 9 | //! 1. Add type-safe interface for mount() system call 10 | //! 2. Add very good explanation of what's wrong when the call fails 11 | //! 12 | //! So we have two error types: 13 | //! 14 | //! 1. `OSError` holds mount info and errno 15 | //! 2. `Error` is returned by `OSError::explain()` 16 | //! 17 | //! The first one is returned by `bare_mount()` the second by `mount()`, and 18 | //! using latter is preffered for most situations. Unless performance is 19 | //! too critical (i.e. you are doing thousands of *failing* mounts per second). 20 | //! On the success path there is no overhead. 21 | //! 22 | #![warn(missing_debug_implementations)] 23 | #![warn(missing_docs)] 24 | 25 | extern crate libc; 26 | extern crate nix; 27 | #[macro_use] extern crate quick_error; 28 | 29 | mod util; 30 | mod error; 31 | mod explain; 32 | mod bind; 33 | mod overlay; 34 | mod tmpfs; 35 | mod modify; 36 | mod remount; 37 | pub mod mountinfo; 38 | 39 | use std::io; 40 | 41 | use explain::Explainable; 42 | use remount::RemountError; 43 | pub use bind::BindMount; 44 | pub use overlay::Overlay; 45 | pub use tmpfs::Tmpfs; 46 | pub use modify::Move; 47 | pub use remount::Remount; 48 | 49 | quick_error! { 50 | #[derive(Debug)] 51 | enum MountError { 52 | Io(err: io::Error) { 53 | cause(err) 54 | from() 55 | } 56 | Remount(err: RemountError) { 57 | cause(err) 58 | from() 59 | } 60 | } 61 | } 62 | 63 | /// The raw os error 64 | /// 65 | /// This is a wrapper around `io::Error` providing `explain()` method 66 | /// 67 | /// Note: you need to explain as fast as possible, because during explain 68 | /// library makes some probes for different things in filesystem, and if 69 | /// anything changes it may give incorrect results. 70 | /// 71 | /// You should always `explain()` the errors, unless you are trying lots of 72 | /// mounts for bruteforcing or other similar thing and you are concerned of 73 | /// performance. Usually library does `stat()` and similar things which are 74 | /// much faster than mount anyway. Also explaining is zero-cost in the success 75 | /// path. 76 | /// 77 | #[derive(Debug)] 78 | pub struct OSError(MountError, Box); 79 | 80 | impl OSError { 81 | fn from_remount(err: RemountError, explain: Box) -> OSError { 82 | OSError(MountError::Remount(err), explain) 83 | } 84 | 85 | fn from_nix(err: nix::Error, explain: Box) -> OSError { 86 | OSError( 87 | MountError::Io( 88 | err.as_errno().map_or_else(|| io::Error::new(io::ErrorKind::Other, err), io::Error::from), 89 | ), 90 | explain, 91 | ) 92 | } 93 | } 94 | 95 | /// The error holder which contains as much information about why failure 96 | /// happens as the library implementors could gain 97 | /// 98 | /// This type only provides `Display` for now, but some programmatic interface 99 | /// is expected in future. 100 | #[derive(Debug)] 101 | pub struct Error(Box, io::Error, String); 102 | -------------------------------------------------------------------------------- /src/modify.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::ffi::{CStr, CString}; 3 | use std::path::Path; 4 | 5 | use nix::mount::{MsFlags, mount}; 6 | 7 | use {OSError, Error}; 8 | use util::{path_to_cstring, as_path}; 9 | use explain::{Explainable, exists}; 10 | 11 | /// A move operation definition 12 | /// 13 | /// This is a similar to `mount --move` and allows to atomically move mount 14 | /// point from one place to another 15 | #[derive(Debug, Clone)] 16 | pub struct Move { 17 | source: CString, 18 | target: CString, 19 | } 20 | 21 | impl Move { 22 | /// Create a new Move operation 23 | pub fn new, B: AsRef>(source: A, target: B) -> Move { 24 | Move { 25 | source: path_to_cstring(source.as_ref()), 26 | target: path_to_cstring(target.as_ref()), 27 | } 28 | } 29 | 30 | /// Execute a move-mountpoint operation 31 | pub fn bare_move_mountpoint(self) 32 | -> Result<(), OSError> 33 | { 34 | mount(Some(&*self.source), &*self.target, None::<&CStr>, MsFlags::MS_MOVE, None::<&CStr>) 35 | .map_err(|err| OSError::from_nix(err, Box::new(self))) 36 | } 37 | 38 | /// Execute a move mountpoint operation and explain the error immediately 39 | pub fn move_mountpoint(self) -> Result<(), Error> { 40 | self.bare_move_mountpoint().map_err(OSError::explain) 41 | } 42 | } 43 | 44 | impl fmt::Display for Move { 45 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 46 | write!(fmt, "move {:?} -> {:?}", 47 | as_path(&self.source), as_path(&self.target)) 48 | } 49 | } 50 | 51 | impl Explainable for Move { 52 | fn explain(&self) -> String { 53 | [ 54 | format!("source: {}", exists(as_path(&self.source))), 55 | format!("target: {}", exists(as_path(&self.target))), 56 | ].join(", ") 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/mountinfo.rs: -------------------------------------------------------------------------------- 1 | //! This module contains parser for /proc/PID/mountinfo 2 | //! 3 | use std; 4 | use std::fmt; 5 | use std::ffi::{OsStr, OsString}; 6 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; 7 | use std::borrow::Cow; 8 | use std::error::Error; 9 | 10 | use nix::mount::MsFlags; 11 | 12 | use libc::c_ulong; 13 | 14 | /// Error parsing a single entry of mountinfo file 15 | #[derive(Debug)] 16 | pub(crate) struct ParseRowError(pub(crate) String); 17 | 18 | impl fmt::Display for ParseRowError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | write!(f, "Parse error: {}", self.0) 21 | } 22 | } 23 | 24 | impl Error for ParseRowError { 25 | fn description(&self) -> &str { 26 | return &self.0; 27 | } 28 | } 29 | 30 | /// Mountinfo file parsing error 31 | #[derive(Debug)] 32 | pub struct ParseError { 33 | msg: String, 34 | row_num: usize, 35 | row: String, 36 | } 37 | 38 | impl ParseError { 39 | fn new(msg: String, row_num: usize, row: String) -> ParseError { 40 | ParseError { 41 | msg: msg, 42 | row_num: row_num, 43 | row: row, 44 | } 45 | } 46 | } 47 | 48 | impl fmt::Display for ParseError { 49 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 50 | write!(f, "Parse error at line {}: {}\n{}", 51 | self.row_num, self.description(), self.row) 52 | } 53 | } 54 | 55 | impl Error for ParseError { 56 | fn description(&self) -> &str { 57 | return &self.msg; 58 | } 59 | } 60 | 61 | /// A parser class for mountinfo file 62 | #[derive(Debug)] 63 | pub struct Parser<'a> { 64 | data: &'a [u8], 65 | row_num: usize, 66 | exhausted: bool, 67 | } 68 | 69 | #[allow(dead_code)] 70 | impl<'a> Parser<'a> { 71 | /// Create a new parser 72 | /// 73 | /// `data` should contain whole contents of `mountinfo` file of any process 74 | pub fn new(data: &'a [u8]) -> Parser<'a> { 75 | Parser { 76 | data: data, 77 | row_num: 0, 78 | exhausted: false, 79 | } 80 | } 81 | } 82 | 83 | /// A single entry returned by mountpoint parser 84 | #[allow(missing_docs)] // self-descriptive / described by man page 85 | #[derive(Debug)] 86 | pub struct MountPoint<'a> { 87 | pub mount_id: c_ulong, 88 | pub parent_id: c_ulong, 89 | pub major: c_ulong, 90 | pub minor: c_ulong, 91 | pub root: Cow<'a, OsStr>, 92 | pub mount_point: Cow<'a, OsStr>, 93 | pub mount_options: Cow<'a, OsStr>, 94 | // TODO: we might need some enum which will have three states: 95 | // empty, single Cow value or a vector Vec> 96 | pub optional_fields: Cow<'a, OsStr>, 97 | pub fstype: Cow<'a, OsStr>, 98 | pub mount_source: Cow<'a, OsStr>, 99 | pub super_options: Cow<'a, OsStr>, 100 | } 101 | 102 | impl<'a> MountPoint<'a> { 103 | /// Returns flags of the mountpoint as a numeric value 104 | /// 105 | /// This value matches linux `MsFlags::MS_*` flags as passed into mount syscall 106 | pub fn get_flags(&self) -> c_ulong { 107 | self.get_mount_flags().bits() as c_ulong 108 | } 109 | 110 | pub(crate) fn get_mount_flags(&self) -> MsFlags { 111 | let mut flags = MsFlags::empty(); 112 | for opt in self.mount_options.as_bytes().split(|c| *c == b',') { 113 | let opt = OsStr::from_bytes(opt); 114 | if opt == OsStr::new("ro") { flags |= MsFlags::MS_RDONLY } 115 | else if opt == OsStr::new("nosuid") { flags |= MsFlags::MS_NOSUID } 116 | else if opt == OsStr::new("nodev") { flags |= MsFlags::MS_NODEV } 117 | else if opt == OsStr::new("noexec") { flags |= MsFlags::MS_NOEXEC } 118 | else if opt == OsStr::new("mand") { flags |= MsFlags::MS_MANDLOCK } 119 | else if opt == OsStr::new("sync") { flags |= MsFlags::MS_SYNCHRONOUS } 120 | else if opt == OsStr::new("dirsync") { flags |= MsFlags::MS_DIRSYNC } 121 | else if opt == OsStr::new("noatime") { flags |= MsFlags::MS_NOATIME } 122 | else if opt == OsStr::new("nodiratime") { flags |= MsFlags::MS_NODIRATIME } 123 | else if opt == OsStr::new("relatime") { flags |= MsFlags::MS_RELATIME } 124 | else if opt == OsStr::new("strictatime") { flags |= MsFlags::MS_STRICTATIME } 125 | } 126 | flags 127 | } 128 | } 129 | 130 | impl<'a> Iterator for Parser<'a> { 131 | type Item = Result, ParseError>; 132 | 133 | fn next(&mut self) -> Option { 134 | if self.exhausted { 135 | return None; 136 | } 137 | 138 | loop { 139 | match self.data.iter().position(|c| *c == b'\n') { 140 | Some(ix) => { 141 | self.row_num += 1; 142 | let row = &self.data[..ix]; 143 | self.data = &self.data[ix + 1..]; 144 | let res = match parse_mount_point(row) { 145 | Ok(None) => continue, 146 | Ok(Some(v)) => Ok(v), 147 | Err(e) => Err(ParseError::new(e.0, self.row_num, 148 | String::from_utf8_lossy(row).into_owned())), 149 | }; 150 | return Some(res); 151 | }, 152 | None => { 153 | self.exhausted = true; 154 | let res = match parse_mount_point(self.data) { 155 | Ok(None) => return None, 156 | Ok(Some(v)) => Ok(v), 157 | Err(e) => Err(ParseError::new(e.0, self.row_num, 158 | String::from_utf8_lossy(self.data).into_owned())), 159 | }; 160 | return Some(res); 161 | }, 162 | } 163 | } 164 | } 165 | } 166 | 167 | pub(crate) fn parse_mount_point<'a>(row: &'a [u8]) 168 | -> Result>, ParseRowError> 169 | { 170 | let row = rstrip_cr(&row); 171 | if is_comment_line(row) { 172 | return Ok(None); 173 | } 174 | 175 | let (mount_id, row) = try!(parse_int(row)); 176 | let (parent_id, row) = try!(parse_int(row)); 177 | let (major, minor, row) = try!(parse_major_minor(row)); 178 | let (root, row) = try!(parse_os_str(row)); 179 | let (mount_point, row) = try!(parse_os_str(row)); 180 | let (mount_options, row) = try!(parse_os_str(row)); 181 | let (optional_fields, row) = try!(parse_optional(row)); 182 | let (fstype, row) = try!(parse_os_str(row)); 183 | let (mount_source, row) = try!(parse_os_str(row)); 184 | let (super_options, _) = try!(parse_os_str(row)); 185 | // TODO: should we ignore extra fields? 186 | Ok(Some(MountPoint { 187 | mount_id: mount_id, 188 | parent_id: parent_id, 189 | major: major, 190 | minor: minor, 191 | root: root, 192 | mount_point: mount_point, 193 | mount_options: mount_options, 194 | optional_fields: optional_fields, 195 | fstype: fstype, 196 | mount_source: mount_source, 197 | super_options: super_options, 198 | })) 199 | } 200 | 201 | fn is_comment_line(row: &[u8]) -> bool { 202 | if row.is_empty() { 203 | return true; 204 | } 205 | for c in row { 206 | if *c == b' ' || *c == b'\t' { 207 | continue; 208 | } 209 | if *c == b'#' { 210 | return true; 211 | } 212 | return false; 213 | } 214 | return false; 215 | } 216 | 217 | fn rstrip_cr(row: &[u8]) -> &[u8] { 218 | if let Some((&b'\r', tail)) = row.split_last() { 219 | tail 220 | } else { 221 | row 222 | } 223 | } 224 | 225 | fn parse_field<'a>(data: &'a [u8], delimit: &'a [u8]) 226 | -> Result<(&'a [u8], &'a [u8]), ParseRowError> 227 | { 228 | if data.is_empty() { 229 | return Err(ParseRowError(format!("Expected more fields"))); 230 | } 231 | let data = lstrip_whitespaces(data); 232 | Ok(split_by(data, delimit)) 233 | } 234 | 235 | fn parse_os_str<'a>(data: &'a [u8]) 236 | -> Result<(Cow<'a, OsStr>, &'a [u8]), ParseRowError> 237 | { 238 | let (field, tail) = try!(parse_field(data, b" ")); 239 | Ok((unescape_octals(OsStr::from_bytes(field)), tail)) 240 | } 241 | 242 | fn parse_int(data: &[u8]) 243 | -> Result<(c_ulong, &[u8]), ParseRowError> 244 | { 245 | let (field, tail) = try!(parse_field(data, b" ")); 246 | let v = try!(std::str::from_utf8(field).map_err(|e| { 247 | ParseRowError(format!("Cannot parse integer {:?}: {}", 248 | String::from_utf8_lossy(field).into_owned(), e))})); 249 | 250 | let v = try!(c_ulong::from_str_radix(v, 10).map_err(|e| { 251 | ParseRowError(format!("Cannot parse integer {:?}: {}", 252 | String::from_utf8_lossy(field).into_owned(), e))})); 253 | Ok((v, tail)) 254 | } 255 | 256 | fn parse_major_minor(data: &[u8]) 257 | -> Result<(c_ulong, c_ulong, &[u8]), ParseRowError> 258 | { 259 | let (major_field, data) = try!(parse_field(data, b":")); 260 | let (minor_field, tail) = try!(parse_field(data, b" ")); 261 | let (major, _) = try!(parse_int(major_field)); 262 | let (minor, _) = try!(parse_int(minor_field)); 263 | Ok((major, minor, tail)) 264 | } 265 | 266 | fn parse_optional<'a>(data: &'a [u8]) 267 | -> Result<(Cow<'a, OsStr>, &'a [u8]), ParseRowError> 268 | { 269 | let (field, tail) = try!(parse_field(data, b"- ")); 270 | let field = rstrip_whitespaces(field); 271 | Ok((unescape_octals(OsStr::from_bytes(field)), tail)) 272 | } 273 | 274 | fn lstrip_whitespaces(v: &[u8]) -> &[u8] { 275 | for (i, c) in v.iter().enumerate() { 276 | if *c != b' ' { 277 | return &v[i..]; 278 | } 279 | } 280 | return &v[0..0]; 281 | } 282 | 283 | fn rstrip_whitespaces(v: &[u8]) -> &[u8] { 284 | for (i, c) in v.iter().enumerate().rev() { 285 | if *c != b' ' { 286 | return &v[..i + 1]; 287 | } 288 | } 289 | return &v[0..0]; 290 | } 291 | 292 | fn split_by<'a, 'b>(v: &'a [u8], needle: &'b [u8]) -> (&'a [u8], &'a [u8]) { 293 | if needle.len() > v.len() { 294 | return (&v[0..], &v[0..0]); 295 | } 296 | let mut i = 0; 297 | while i <= v.len() - needle.len() { 298 | let (head, tail) = v.split_at(i); 299 | if tail.starts_with(needle) { 300 | return (head, &tail[needle.len()..]); 301 | } 302 | i += 1; 303 | } 304 | return (&v[0..], &v[0..0]); 305 | } 306 | 307 | fn unescape_octals(s: &OsStr) -> Cow { 308 | let (mut i, has_escapes) = { 309 | let bytes = s.as_bytes(); 310 | let mut i = 0; 311 | while i < bytes.len() { 312 | if is_octal_encoding(&bytes[i..]) { 313 | break; 314 | } 315 | i += 1; 316 | } 317 | (i, i < bytes.len()) 318 | }; 319 | if !has_escapes { 320 | return Cow::Borrowed(s); 321 | } 322 | 323 | let mut v: Vec = vec!(); 324 | let bytes = s.as_bytes(); 325 | v.extend_from_slice(&bytes[..i]); 326 | while i < bytes.len() { 327 | if is_octal_encoding(&bytes[i..]) { 328 | let c = parse_octal(&bytes[i + 1..]); 329 | v.push(c); 330 | i += 4; 331 | } else { 332 | v.push(bytes[i]); 333 | i += 1; 334 | } 335 | } 336 | Cow::Owned(OsString::from_vec(v)) 337 | } 338 | 339 | fn is_octal_encoding(v: &[u8]) -> bool { 340 | v.len() >= 4 && v[0] == b'\\' 341 | && is_oct(v[1]) && is_oct(v[2]) && is_oct(v[3]) 342 | } 343 | 344 | fn is_oct(c: u8) -> bool { 345 | c >= b'0' && c <= b'7' 346 | } 347 | 348 | fn parse_octal(v: &[u8]) -> u8 { 349 | ((v[0] & 7) << 6) + ((v[1] & 7) << 3) + (v[2] & 7) 350 | } 351 | 352 | #[cfg(test)] 353 | mod test { 354 | use std::path::Path; 355 | use std::ffi::OsStr; 356 | use std::os::unix::ffi::OsStrExt; 357 | 358 | use nix::mount::MsFlags; 359 | 360 | use super::{Parser, ParseError}; 361 | use super::{is_octal_encoding, parse_octal, unescape_octals}; 362 | 363 | #[test] 364 | fn test_is_octal_encoding() { 365 | assert!(is_octal_encoding(b"\\000")); 366 | assert!(is_octal_encoding(b"\\123")); 367 | assert!(is_octal_encoding(b"\\777")); 368 | assert!(!is_octal_encoding(b"")); 369 | assert!(!is_octal_encoding(b"\\")); 370 | assert!(!is_octal_encoding(b"000")); 371 | assert!(!is_octal_encoding(b"\\00")); 372 | assert!(!is_octal_encoding(b"\\800")); 373 | } 374 | 375 | #[test] 376 | fn test_parse_octal() { 377 | assert_eq!(parse_octal(b"000"), 0); 378 | assert_eq!(parse_octal(b"123"), 83); 379 | assert_eq!(parse_octal(b"377"), 255); 380 | // mount utility just ignores overflowing 381 | assert_eq!(parse_octal(b"777"), 255); 382 | } 383 | 384 | #[test] 385 | fn test_unescape_octals() { 386 | assert_eq!(unescape_octals(OsStr::new("\\000")), OsStr::from_bytes(b"\x00")); 387 | assert_eq!(unescape_octals(OsStr::new("\\00")), OsStr::new("\\00")); 388 | assert_eq!(unescape_octals(OsStr::new("test\\040data")), OsStr::new("test data")); 389 | } 390 | 391 | #[test] 392 | fn test_mount_info_parser_proc() { 393 | let content = b"19 24 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw"; 394 | let mut parser = Parser::new(&content[..]); 395 | let mount_point = parser.next().unwrap().unwrap(); 396 | assert_eq!(mount_point.mount_id, 19); 397 | assert_eq!(mount_point.parent_id, 24); 398 | assert_eq!(mount_point.major, 0); 399 | assert_eq!(mount_point.minor, 4); 400 | assert_eq!(mount_point.root, Path::new("/")); 401 | assert_eq!(mount_point.mount_point, Path::new("/proc")); 402 | assert_eq!(mount_point.mount_options, OsStr::new("rw,nosuid,nodev,noexec,relatime")); 403 | assert_eq!(mount_point.optional_fields, OsStr::new("shared:12")); 404 | assert_eq!(mount_point.fstype, OsStr::new("proc")); 405 | assert_eq!(mount_point.mount_source, OsStr::new("proc")); 406 | assert_eq!(mount_point.super_options, OsStr::new("rw")); 407 | assert_eq!(mount_point.get_mount_flags(), MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_RELATIME); 408 | assert!(parser.next().is_none()); 409 | } 410 | 411 | #[test] 412 | fn test_mount_info_parser_comment() { 413 | let content = b"# Test comment\n\ 414 | \t # Another shifted comment\n\ 415 | 19 24 0:4 / /#proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw"; 416 | let mut parser = Parser::new(&content[..]); 417 | let mount_point = parser.next().unwrap().unwrap(); 418 | assert_eq!(mount_point.mount_id, 19); 419 | assert_eq!(mount_point.parent_id, 24); 420 | assert_eq!(mount_point.major, 0); 421 | assert_eq!(mount_point.minor, 4); 422 | assert_eq!(mount_point.root, Path::new("/")); 423 | assert_eq!(mount_point.mount_point, Path::new("/#proc")); 424 | assert_eq!(mount_point.mount_options, OsStr::new("rw,nosuid,nodev,noexec,relatime")); 425 | assert_eq!(mount_point.optional_fields, OsStr::new("shared:12")); 426 | assert_eq!(mount_point.fstype, OsStr::new("proc")); 427 | assert_eq!(mount_point.mount_source, OsStr::new("proc")); 428 | assert_eq!(mount_point.super_options, OsStr::new("rw")); 429 | assert_eq!(mount_point.get_mount_flags(), MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_RELATIME); 430 | assert!(parser.next().is_none()); 431 | } 432 | 433 | #[test] 434 | fn test_mount_info_parser_missing_optional_fields() { 435 | let content = b"335 294 0:56 / /proc rw,relatime - proc proc rw"; 436 | let mut parser = Parser::new(&content[..]); 437 | let mount_point = parser.next().unwrap().unwrap(); 438 | assert_eq!(mount_point.mount_id, 335); 439 | assert_eq!(mount_point.parent_id, 294); 440 | assert_eq!(mount_point.major, 0); 441 | assert_eq!(mount_point.minor, 56); 442 | assert_eq!(mount_point.root, Path::new("/")); 443 | assert_eq!(mount_point.mount_point, Path::new("/proc")); 444 | assert_eq!(mount_point.mount_options, OsStr::new("rw,relatime")); 445 | assert_eq!(mount_point.optional_fields, OsStr::new("")); 446 | assert_eq!(mount_point.fstype, OsStr::new("proc")); 447 | assert_eq!(mount_point.mount_source, OsStr::new("proc")); 448 | assert_eq!(mount_point.super_options, OsStr::new("rw")); 449 | assert_eq!(mount_point.get_mount_flags(), MsFlags::MS_RELATIME); 450 | assert!(parser.next().is_none()); 451 | } 452 | 453 | #[test] 454 | fn test_mount_info_parser_more_optional_fields() { 455 | let content = b"335 294 0:56 / /proc rw,relatime shared:12 master:1 - proc proc rw"; 456 | let mut parser = Parser::new(&content[..]); 457 | let mount_point = parser.next().unwrap().unwrap(); 458 | assert_eq!(mount_point.mount_id, 335); 459 | assert_eq!(mount_point.parent_id, 294); 460 | assert_eq!(mount_point.major, 0); 461 | assert_eq!(mount_point.minor, 56); 462 | assert_eq!(mount_point.root, Path::new("/")); 463 | assert_eq!(mount_point.mount_point, Path::new("/proc")); 464 | assert_eq!(mount_point.mount_options, OsStr::new("rw,relatime")); 465 | assert_eq!(mount_point.optional_fields, OsStr::new("shared:12 master:1")); 466 | assert_eq!(mount_point.fstype, OsStr::new("proc")); 467 | assert_eq!(mount_point.mount_source, OsStr::new("proc")); 468 | assert_eq!(mount_point.super_options, OsStr::new("rw")); 469 | assert_eq!(mount_point.get_mount_flags(), MsFlags::MS_RELATIME); 470 | assert!(parser.next().is_none()); 471 | } 472 | 473 | #[test] 474 | fn test_mount_info_parser_escaping() { 475 | let content = br"76 24 8:6 / /home/my\040super\011name\012\134 rw,relatime shared:29 - ext4 /dev/sda1 rw,data=ordered"; 476 | let mut parser = Parser::new(&content[..]); 477 | let mount_point = parser.next().unwrap().unwrap(); 478 | assert_eq!(mount_point.mount_id, 76); 479 | assert_eq!(mount_point.parent_id, 24); 480 | assert_eq!(mount_point.major, 8); 481 | assert_eq!(mount_point.minor, 6); 482 | assert_eq!(mount_point.root, Path::new("/")); 483 | assert_eq!(mount_point.mount_point, Path::new("/home/my super\tname\n\\")); 484 | assert_eq!(mount_point.mount_options, OsStr::new("rw,relatime")); 485 | assert_eq!(mount_point.optional_fields, OsStr::new("shared:29")); 486 | assert_eq!(mount_point.fstype, OsStr::new("ext4")); 487 | assert_eq!(mount_point.mount_source, OsStr::new("/dev/sda1")); 488 | assert_eq!(mount_point.super_options, OsStr::new("rw,data=ordered")); 489 | assert_eq!(mount_point.get_mount_flags(), MsFlags::MS_RELATIME); 490 | assert!(parser.next().is_none()); 491 | } 492 | 493 | #[test] 494 | fn test_mount_info_parser_non_utf8() { 495 | let content = b"22 24 0:19 / /\xff rw shared:5 - tmpfs tmpfs rw,mode=755"; 496 | let mut parser = Parser::new(&content[..]); 497 | let mount_point = parser.next().unwrap().unwrap(); 498 | assert_eq!(mount_point.mount_point, Path::new(OsStr::from_bytes(b"/\xff"))); 499 | assert_eq!(mount_point.mount_options, OsStr::new("rw")); 500 | assert_eq!(mount_point.fstype, OsStr::new("tmpfs")); 501 | assert_eq!(mount_point.mount_source, OsStr::new("tmpfs")); 502 | assert_eq!(mount_point.get_mount_flags(), MsFlags::empty()); 503 | assert!(parser.next().is_none()); 504 | } 505 | 506 | #[test] 507 | fn test_mount_info_parser_crlf() { 508 | let content = b"26 20 0:21 / /tmp rw shared:4 - tmpfs tmpfs rw\r\n\ 509 | \n\ 510 | \r\n\ 511 | 27 22 0:22 / /tmp rw,nosuid,nodev shared:6 - tmpfs tmpfs rw\r"; 512 | let mut parser = Parser::new(&content[..]); 513 | let mount_point = parser.next().unwrap().unwrap(); 514 | assert_eq!(mount_point.mount_point, Path::new("/tmp")); 515 | assert_eq!(mount_point.mount_options, OsStr::new("rw")); 516 | assert_eq!(mount_point.super_options, OsStr::new("rw")); 517 | assert_eq!(mount_point.get_mount_flags(), MsFlags::empty()); 518 | let mount_point = parser.next().unwrap().unwrap(); 519 | assert_eq!(mount_point.mount_point, Path::new("/tmp")); 520 | assert_eq!(mount_point.mount_options, OsStr::new("rw,nosuid,nodev")); 521 | assert_eq!(mount_point.super_options, OsStr::new("rw")); 522 | assert_eq!(mount_point.get_mount_flags(), MsFlags::MS_NOSUID | MsFlags::MS_NODEV); 523 | assert!(parser.next().is_none()); 524 | } 525 | 526 | #[test] 527 | fn test_mount_info_parser_incomplete_row() { 528 | let content = b"19 24 0:4 / /proc rw,relatime shared:12 - proc proc"; 529 | let mut parser = Parser::new(&content[..]); 530 | let mount_info_res = parser.next().unwrap(); 531 | assert!(mount_info_res.is_err()); 532 | match mount_info_res { 533 | Err(ParseError {ref msg, ..}) => { 534 | assert_eq!(msg, "Expected more fields"); 535 | }, 536 | _ => panic!("Expected incomplete row error") 537 | } 538 | assert!(parser.next().is_none()); 539 | } 540 | 541 | #[test] 542 | fn test_mount_info_parser_invalid_int() { 543 | let content = b"19 24b 0:4 / /proc rw,relatime - proc proc rw"; 544 | let mut parser = Parser::new(&content[..]); 545 | let mount_info_res = parser.next().unwrap(); 546 | assert!(mount_info_res.is_err()); 547 | match mount_info_res { 548 | Err(ParseError {ref msg, ..}) => { 549 | assert!(msg.starts_with("Cannot parse integer \"24b\":")); 550 | }, 551 | _ => panic!("Expected invalid row error") 552 | } 553 | assert!(parser.next().is_none()); 554 | } 555 | 556 | #[test] 557 | fn test_mount_info_parser_overflowed_int() { 558 | let content = b"111111111111111111111"; 559 | let mut parser = Parser::new(&content[..]); 560 | let mount_info_res = parser.next().unwrap(); 561 | assert!(mount_info_res.is_err()); 562 | match mount_info_res { 563 | Err(ParseError {ref msg, ..}) => { 564 | assert!(msg.starts_with("Cannot parse integer \"111111111111111111111\"")); 565 | }, 566 | _ => panic!("Expected invalid row error") 567 | } 568 | assert!(parser.next().is_none()); 569 | } 570 | 571 | #[test] 572 | fn test_mount_info_parser_invalid_escape() { 573 | let content = b"19 24 0:4 / /proc\\1 rw,relatime - proc proc rw"; 574 | let mut parser = Parser::new(&content[..]); 575 | let mount_point = parser.next().unwrap().unwrap(); 576 | assert_eq!(mount_point.mount_point, Path::new("/proc\\1")); 577 | assert!(parser.next().is_none()); 578 | } 579 | 580 | #[test] 581 | fn test_mount_info_parser_overflowed_escape() { 582 | let content = b"19 24 0:4 / /proc\\400 rw,nosuid,nodev,noexec,relatime - proc proc rw"; 583 | let mut parser = Parser::new(&content[..]); 584 | let mount_point = parser.next().unwrap().unwrap(); 585 | assert_eq!(mount_point.mount_point, Path::new(OsStr::from_bytes(b"/proc\x00"))); 586 | assert!(parser.next().is_none()); 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /src/overlay.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::path::{Path, PathBuf}; 3 | use std::fs::metadata; 4 | use std::ffi::{CStr, CString}; 5 | use std::os::unix::fs::MetadataExt; 6 | use std::os::unix::ffi::OsStrExt; 7 | 8 | use nix::mount::{MsFlags, mount}; 9 | 10 | use util::{path_to_cstring, as_path}; 11 | use {OSError, Error}; 12 | use explain::{Explainable, exists, user}; 13 | 14 | 15 | /// An overlay mount point 16 | /// 17 | /// This requires linux kernel of at least 3.18. 18 | /// 19 | /// To use overlayfs in user namespace you need a kernel patch (which is 20 | /// enabled by default in ubuntu). At least this is still true for mainline 21 | /// kernel 4.5.0. 22 | #[derive(Debug, Clone)] 23 | pub struct Overlay { 24 | lowerdirs: Vec, 25 | upperdir: Option, 26 | workdir: Option, 27 | target: CString, 28 | } 29 | 30 | impl Overlay { 31 | /// A constructor for read-only overlayfs mount 32 | /// 33 | /// You must have at least two directories in the list (for single 34 | /// dir it might be equal to bind mount, but kernel return EINVAL for 35 | /// such options). 36 | /// 37 | /// The top-most directory will be first in the list. 38 | pub fn readonly<'x, I, T>(dirs: I, target: T) -> Overlay 39 | where I: Iterator, T: AsRef 40 | { 41 | Overlay { 42 | lowerdirs: dirs.map(|x| x.to_path_buf()).collect(), 43 | upperdir: None, 44 | workdir: None, 45 | target: path_to_cstring(target.as_ref()), 46 | } 47 | } 48 | /// A constructor for writable overlayfs mount 49 | /// 50 | /// The upperdir and workdir must be on the same filesystem. 51 | /// 52 | /// The top-most directory will be first in the list of lowerdirs. 53 | pub fn writable<'x, I, B, C, D>(lowerdirs: I, upperdir: B, 54 | workdir: C, target: D) 55 | -> Overlay 56 | where I: Iterator, B: AsRef, 57 | C: AsRef, D: AsRef, 58 | { 59 | Overlay { 60 | lowerdirs: lowerdirs.map(|x| x.to_path_buf()).collect(), 61 | upperdir: Some(upperdir.as_ref().to_path_buf()), 62 | workdir: Some(workdir.as_ref().to_path_buf()), 63 | target: path_to_cstring(target.as_ref()), 64 | } 65 | } 66 | 67 | /// Execute an overlay mount 68 | pub fn bare_mount(self) -> Result<(), OSError> { 69 | let mut options = Vec::new(); 70 | options.extend(b"lowerdir="); 71 | for (i, p) in self.lowerdirs.iter().enumerate() { 72 | if i != 0 { 73 | options.push(b':') 74 | } 75 | append_escape(&mut options, p); 76 | } 77 | if let (Some(u), Some(w)) = (self.upperdir.as_ref(), self.workdir.as_ref()) { 78 | options.extend(b",upperdir="); 79 | append_escape(&mut options, u); 80 | options.extend(b",workdir="); 81 | append_escape(&mut options, w); 82 | } 83 | mount( 84 | Some(CStr::from_bytes_with_nul(b"overlay\0").unwrap()), 85 | &*self.target, 86 | Some(CStr::from_bytes_with_nul(b"overlay\0").unwrap()), 87 | MsFlags::empty(), 88 | Some(&*options), 89 | ).map_err(|err| OSError::from_nix(err, Box::new(self))) 90 | } 91 | 92 | /// Execute an overlay mount and explain the error immediately 93 | pub fn mount(self) -> Result<(), Error> { 94 | self.bare_mount().map_err(OSError::explain) 95 | } 96 | } 97 | 98 | /// Escape the path to put it into options string for overlayfs 99 | /// 100 | /// The rules here are not documented anywhere as far as I know and was 101 | /// derived experimentally. 102 | fn append_escape(dest: &mut Vec, path: &Path) { 103 | for &byte in path.as_os_str().as_bytes().iter() { 104 | match byte { 105 | // This is escape char 106 | b'\\' => { dest.push(b'\\'); dest.push(b'\\'); } 107 | // This is used as a path separator in lowerdir 108 | b':' => { dest.push(b'\\'); dest.push(b':'); } 109 | // This is used as a argument separator 110 | b',' => { dest.push(b'\\'); dest.push(b','); } 111 | x => dest.push(x), 112 | } 113 | } 114 | } 115 | 116 | impl fmt::Display for Overlay { 117 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 118 | if let (Some(udir), Some(wdir)) = 119 | (self.upperdir.as_ref(), self.workdir.as_ref()) 120 | { 121 | write!(fmt, "overlayfs \ 122 | {},upperdir={:?},workdir={:?} -> {:?}", 123 | self.lowerdirs.iter().map(|x| format!("{:?}", x)) 124 | .collect::>().join(":"), 125 | udir, wdir, as_path(&self.target)) 126 | } else { 127 | write!(fmt, "overlayfs \ 128 | {} -> {:?}", 129 | self.lowerdirs.iter().map(|x| format!("{:?}", x)) 130 | .collect::>().join(":"), 131 | as_path(&self.target)) 132 | } 133 | } 134 | } 135 | 136 | impl Explainable for Overlay { 137 | fn explain(&self) -> String { 138 | let mut info = self.lowerdirs.iter() 139 | .map(|x| format!("{:?}: {}", x, exists(x))) 140 | .collect::>(); 141 | if let (Some(udir), Some(wdir)) = 142 | (self.upperdir.as_ref(), self.workdir.as_ref()) 143 | { 144 | let umeta = metadata(&udir).ok(); 145 | let wmeta = metadata(&wdir).ok(); 146 | info.push(format!("upperdir: {}", exists(&udir))); 147 | info.push(format!("workdir: {}", exists(&wdir))); 148 | 149 | if let (Some(u), Some(w)) = (umeta, wmeta) { 150 | info.push(format!("{}", if u.dev() == w.dev() 151 | { "same-fs" } else { "different-fs" })); 152 | } 153 | if udir.starts_with(wdir) { 154 | info.push("upperdir-prefix-of-workdir".to_string()); 155 | } else if wdir.starts_with(udir) { 156 | info.push("workdir-prefix-of-upperdir".to_string()); 157 | } 158 | info.push(format!("target: {}", exists(as_path(&self.target)))); 159 | } 160 | if self.lowerdirs.len() < 1 { 161 | info.push("no-lowerdirs".to_string()); 162 | } else if self.upperdir.is_none() && self.lowerdirs.len() < 2 { 163 | info.push("single-lowerdir".to_string()); 164 | } 165 | info.push(user().to_string()); 166 | info.join(", ") 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /src/remount.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::fmt; 3 | use std::ffi::CStr; 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::path::{Path, PathBuf}; 7 | use std::env::current_dir; 8 | use std::default::Default; 9 | 10 | use nix::mount::{MsFlags, mount}; 11 | 12 | use {OSError, Error}; 13 | use util::path_to_cstring; 14 | use explain::{Explainable, exists, user}; 15 | use mountinfo::{parse_mount_point}; 16 | 17 | /// A remount definition 18 | /// 19 | /// Usually it is used to change mount flags for a mounted filesystem. 20 | /// Especially to make a readonly filesystem writable or vice versa. 21 | #[derive(Debug, Clone)] 22 | pub struct Remount { 23 | path: PathBuf, 24 | flags: MountFlags, 25 | } 26 | 27 | #[derive(Debug, Clone, Default)] 28 | struct MountFlags { 29 | pub bind: Option, 30 | pub readonly: Option, 31 | pub nodev: Option, 32 | pub noexec: Option, 33 | pub nosuid: Option, 34 | pub noatime: Option, 35 | pub nodiratime: Option, 36 | pub relatime: Option, 37 | pub strictatime: Option, 38 | pub dirsync: Option, 39 | pub synchronous: Option, 40 | pub mandlock: Option, 41 | } 42 | 43 | impl MountFlags { 44 | fn apply_to_flags(&self, flags: MsFlags) -> MsFlags { 45 | let mut flags = flags; 46 | flags = apply_flag(flags, MsFlags::MS_BIND, self.bind); 47 | flags = apply_flag(flags, MsFlags::MS_RDONLY, self.readonly); 48 | flags = apply_flag(flags, MsFlags::MS_NODEV, self.nodev); 49 | flags = apply_flag(flags, MsFlags::MS_NOEXEC, self.noexec); 50 | flags = apply_flag(flags, MsFlags::MS_NOSUID, self.nosuid); 51 | flags = apply_flag(flags, MsFlags::MS_NOATIME, self.noatime); 52 | flags = apply_flag(flags, MsFlags::MS_NODIRATIME, self.nodiratime); 53 | flags = apply_flag(flags, MsFlags::MS_RELATIME, self.relatime); 54 | flags = apply_flag(flags, MsFlags::MS_STRICTATIME, self.strictatime); 55 | flags = apply_flag(flags, MsFlags::MS_DIRSYNC, self.dirsync); 56 | flags = apply_flag(flags, MsFlags::MS_SYNCHRONOUS, self.synchronous); 57 | flags = apply_flag(flags, MsFlags::MS_MANDLOCK, self.mandlock); 58 | flags 59 | } 60 | } 61 | 62 | fn apply_flag(flags: MsFlags, flag: MsFlags, set: Option) -> MsFlags { 63 | match set { 64 | Some(true) => flags | flag, 65 | Some(false) => flags & !flag, 66 | None => flags, 67 | } 68 | } 69 | 70 | quick_error! { 71 | #[derive(Debug)] 72 | pub enum RemountError { 73 | Io(msg: String, err: io::Error) { 74 | cause(err) 75 | display("{}: {}", msg, err) 76 | description(err.description()) 77 | from(err: io::Error) -> (String::new(), err) 78 | } 79 | ParseMountInfo(err: String) { 80 | display("{}", err) 81 | from() 82 | } 83 | UnknownMountPoint(path: PathBuf) { 84 | display("Cannot find mount point: {:?}", path) 85 | } 86 | } 87 | } 88 | 89 | impl Remount { 90 | /// Create a new Remount operation 91 | /// 92 | /// By default it doesn't modify any flags. So is basically useless, you 93 | /// should set some flags to make it effective. 94 | pub fn new>(path: A) -> Remount { 95 | Remount { 96 | path: path.as_ref().to_path_buf(), 97 | flags: Default::default(), 98 | } 99 | } 100 | /// Set bind flag 101 | /// Note: remount readonly doesn't work without MS_BIND flag 102 | /// inside unpriviledged user namespaces 103 | pub fn bind(mut self, flag: bool) -> Remount { 104 | self.flags.bind = Some(flag); 105 | self 106 | } 107 | /// Set readonly flag 108 | pub fn readonly(mut self, flag: bool) -> Remount { 109 | self.flags.readonly = Some(flag); 110 | self 111 | } 112 | /// Set nodev flag 113 | pub fn nodev(mut self, flag: bool) -> Remount { 114 | self.flags.nodev = Some(flag); 115 | self 116 | } 117 | /// Set noexec flag 118 | pub fn noexec(mut self, flag: bool) -> Remount { 119 | self.flags.noexec = Some(flag); 120 | self 121 | } 122 | /// Set nosuid flag 123 | pub fn nosuid(mut self, flag: bool) -> Remount { 124 | self.flags.nosuid = Some(flag); 125 | self 126 | } 127 | /// Set noatime flag 128 | pub fn noatime(mut self, flag: bool) -> Remount { 129 | self.flags.noatime = Some(flag); 130 | self 131 | } 132 | /// Set nodiratime flag 133 | pub fn nodiratime(mut self, flag: bool) -> Remount { 134 | self.flags.nodiratime = Some(flag); 135 | self 136 | } 137 | /// Set relatime flag 138 | pub fn relatime(mut self, flag: bool) -> Remount { 139 | self.flags.relatime = Some(flag); 140 | self 141 | } 142 | /// Set strictatime flag 143 | pub fn strictatime(mut self, flag: bool) -> Remount { 144 | self.flags.strictatime = Some(flag); 145 | self 146 | } 147 | /// Set dirsync flag 148 | pub fn dirsync(mut self, flag: bool) -> Remount { 149 | self.flags.dirsync = Some(flag); 150 | self 151 | } 152 | /// Set synchronous flag 153 | pub fn synchronous(mut self, flag: bool) -> Remount { 154 | self.flags.synchronous = Some(flag); 155 | self 156 | } 157 | /// Set mandlock flag 158 | pub fn mandlock(mut self, flag: bool) -> Remount { 159 | self.flags.mandlock = Some(flag); 160 | self 161 | } 162 | 163 | /// Execute a remount 164 | pub fn bare_remount(self) -> Result<(), OSError> { 165 | let mut flags = match get_mountpoint_flags(&self.path) { 166 | Ok(flags) => flags, 167 | Err(e) => { 168 | return Err(OSError::from_remount(e, Box::new(self))); 169 | }, 170 | }; 171 | flags = self.flags.apply_to_flags(flags) | MsFlags::MS_REMOUNT; 172 | mount( 173 | None::<&CStr>, 174 | &*path_to_cstring(&self.path), 175 | None::<&CStr>, 176 | flags, 177 | None::<&CStr>, 178 | ).map_err(|err| OSError::from_nix(err, Box::new(self))) 179 | } 180 | 181 | /// Execute a remount and explain the error immediately 182 | pub fn remount(self) -> Result<(), Error> { 183 | self.bare_remount().map_err(OSError::explain) 184 | } 185 | } 186 | 187 | impl fmt::Display for MountFlags { 188 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 189 | let mut prefix = ""; 190 | if let Some(true) = self.bind { 191 | try!(write!(fmt, "{}bind", prefix)); 192 | prefix = ","; 193 | } 194 | if let Some(true) = self.readonly { 195 | try!(write!(fmt, "{}ro", prefix)); 196 | prefix = ","; 197 | } 198 | if let Some(true) = self.nodev { 199 | try!(write!(fmt, "{}nodev", prefix)); 200 | prefix = ","; 201 | } 202 | if let Some(true) = self.noexec { 203 | try!(write!(fmt, "{}noexec", prefix)); 204 | prefix = ","; 205 | } 206 | if let Some(true) = self.nosuid { 207 | try!(write!(fmt, "{}nosuid", prefix)); 208 | prefix = ","; 209 | } 210 | if let Some(true) = self.noatime { 211 | try!(write!(fmt, "{}noatime", prefix)); 212 | prefix = ","; 213 | } 214 | if let Some(true) = self.nodiratime { 215 | try!(write!(fmt, "{}nodiratime", prefix)); 216 | prefix = ","; 217 | } 218 | if let Some(true) = self.relatime { 219 | try!(write!(fmt, "{}relatime", prefix)); 220 | prefix = ","; 221 | } 222 | if let Some(true) = self.strictatime { 223 | try!(write!(fmt, "{}strictatime", prefix)); 224 | prefix = ","; 225 | } 226 | if let Some(true) = self.dirsync { 227 | try!(write!(fmt, "{}dirsync", prefix)); 228 | prefix = ","; 229 | } 230 | if let Some(true) = self.synchronous { 231 | try!(write!(fmt, "{}sync", prefix)); 232 | prefix = ","; 233 | } 234 | if let Some(true) = self.mandlock { 235 | try!(write!(fmt, "{}mand", prefix)); 236 | } 237 | Ok(()) 238 | } 239 | } 240 | 241 | impl fmt::Display for Remount { 242 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 243 | if !self.flags.apply_to_flags(MsFlags::empty()).is_empty() { 244 | try!(write!(fmt, "{} ", self.flags)); 245 | } 246 | write!(fmt, "remount {:?}", &self.path) 247 | } 248 | } 249 | 250 | impl Explainable for Remount { 251 | fn explain(&self) -> String { 252 | [ 253 | format!("path: {}", exists(&self.path)), 254 | format!("{}", user()), 255 | ].join(", ") 256 | } 257 | } 258 | 259 | fn get_mountpoint_flags(path: &Path) -> Result { 260 | let mount_path = if path.is_absolute() { 261 | path.to_path_buf() 262 | } else { 263 | let mut mpath = try!(current_dir()); 264 | mpath.push(path); 265 | mpath 266 | }; 267 | let mut mountinfo_content = Vec::with_capacity(4 * 1024); 268 | let mountinfo_path = Path::new("/proc/self/mountinfo"); 269 | let mut mountinfo_file = try!(File::open(mountinfo_path) 270 | .map_err(|e| RemountError::Io( 271 | format!("Cannot open file: {:?}", mountinfo_path), e))); 272 | try!(mountinfo_file.read_to_end(&mut mountinfo_content) 273 | .map_err(|e| RemountError::Io( 274 | format!("Cannot read file: {:?}", mountinfo_path), e))); 275 | match get_mountpoint_flags_from(&mountinfo_content, &mount_path) { 276 | Ok(Some(flags)) => Ok(flags), 277 | Ok(None) => Err(RemountError::UnknownMountPoint(mount_path)), 278 | Err(e) => Err(e), 279 | } 280 | } 281 | 282 | fn get_mountpoint_flags_from(content: &[u8], path: &Path) 283 | -> Result, RemountError> 284 | { 285 | // iterate from the end of the mountinfo file 286 | for line in content.split(|c| *c == b'\n').rev() { 287 | let entry = parse_mount_point(line) 288 | .map_err(|e| RemountError::ParseMountInfo(e.0))?; 289 | if let Some(mount_point) = entry { 290 | if mount_point.mount_point == path { 291 | return Ok(Some(mount_point.get_mount_flags())); 292 | } 293 | } 294 | } 295 | Ok(None) 296 | } 297 | 298 | #[cfg(test)] 299 | mod test { 300 | use std::path::Path; 301 | use std::ffi::OsStr; 302 | use std::os::unix::ffi::OsStrExt; 303 | 304 | use nix::mount::MsFlags; 305 | 306 | use Error; 307 | use super::{Remount, RemountError, MountFlags}; 308 | use super::{get_mountpoint_flags, get_mountpoint_flags_from}; 309 | 310 | #[test] 311 | fn test_mount_flags() { 312 | let flags = MountFlags { 313 | bind: Some(true), 314 | readonly: Some(true), 315 | nodev: Some(true), 316 | noexec: Some(true), 317 | nosuid: Some(true), 318 | noatime: Some(true), 319 | nodiratime: Some(true), 320 | relatime: Some(true), 321 | strictatime: Some(true), 322 | dirsync: Some(true), 323 | synchronous: Some(true), 324 | mandlock: Some(true), 325 | }; 326 | let bits = (MsFlags::MS_BIND | MsFlags::MS_RDONLY | MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | 327 | MsFlags::MS_NOATIME | MsFlags::MS_NODIRATIME | MsFlags::MS_RELATIME | MsFlags::MS_STRICTATIME | 328 | MsFlags::MS_DIRSYNC | MsFlags::MS_SYNCHRONOUS | MsFlags::MS_MANDLOCK).bits(); 329 | assert_eq!(flags.apply_to_flags(MsFlags::empty()).bits(), bits); 330 | 331 | let flags = MountFlags { 332 | bind: Some(false), 333 | readonly: Some(false), 334 | nodev: Some(false), 335 | noexec: Some(false), 336 | nosuid: Some(false), 337 | noatime: Some(false), 338 | nodiratime: Some(false), 339 | relatime: Some(false), 340 | strictatime: Some(false), 341 | dirsync: Some(false), 342 | synchronous: Some(false), 343 | mandlock: Some(false), 344 | }; 345 | assert_eq!(flags.apply_to_flags(MsFlags::from_bits_truncate(bits)).bits(), 0); 346 | 347 | let flags = MountFlags::default(); 348 | assert_eq!(flags.apply_to_flags(MsFlags::from_bits_truncate(0)).bits(), 0); 349 | assert_eq!(flags.apply_to_flags(MsFlags::from_bits_truncate(bits)).bits(), bits); 350 | } 351 | 352 | #[test] 353 | fn test_remount() { 354 | let remount = Remount::new("/"); 355 | assert_eq!(format!("{}", remount), "remount \"/\""); 356 | 357 | let remount = Remount::new("/").readonly(true).nodev(true); 358 | assert_eq!(format!("{}", remount), "ro,nodev remount \"/\""); 359 | } 360 | 361 | #[test] 362 | fn test_get_mountpoint_flags_from() { 363 | let content = b"19 24 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw"; 364 | let flags = get_mountpoint_flags_from(&content[..], Path::new("/proc")).unwrap().unwrap(); 365 | assert_eq!(flags, MsFlags::MS_NODEV | MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME); 366 | } 367 | 368 | #[test] 369 | fn test_get_mountpoint_flags_from_dups() { 370 | let content = b"11 18 0:4 / /tmp rw shared:28 - tmpfs tmpfs rw\n\ 371 | 12 18 0:6 / /tmp rw,nosuid,nodev shared:29 - tmpfs tmpfs rw\n"; 372 | let flags = get_mountpoint_flags_from(&content[..], Path::new("/tmp")).unwrap().unwrap(); 373 | assert_eq!(flags, MsFlags::MS_NOSUID | MsFlags::MS_NODEV); 374 | } 375 | 376 | #[test] 377 | fn test_get_mountpoint_flags() { 378 | assert!(get_mountpoint_flags(Path::new("/")).is_ok()); 379 | } 380 | 381 | #[test] 382 | fn test_get_mountpoint_flags_unknown() { 383 | let mount_point = Path::new(OsStr::from_bytes(b"/\xff")); 384 | let error = get_mountpoint_flags(mount_point).unwrap_err(); 385 | match error { 386 | RemountError::UnknownMountPoint(p) => assert_eq!(p, mount_point), 387 | _ => panic!(), 388 | } 389 | } 390 | 391 | #[test] 392 | fn test_remount_unknown_mountpoint() { 393 | let remount = Remount::new("/non-existent"); 394 | let error = remount.remount().unwrap_err(); 395 | let Error(_, e, msg) = error; 396 | match e.get_ref() { 397 | Some(e) => { 398 | assert_eq!( 399 | e.to_string(), 400 | "Cannot find mount point: \"/non-existent\""); 401 | }, 402 | _ => panic!(), 403 | } 404 | assert!(msg.starts_with( 405 | "Cannot find mount point: \"/non-existent\", path: missing, ")); 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/tmpfs.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Write, Cursor}; 2 | use std::fmt; 3 | use std::str::from_utf8; 4 | use std::ffi::{CString, CStr}; 5 | use std::path::Path; 6 | 7 | use libc::{uid_t, gid_t, mode_t}; 8 | use nix::mount::{MsFlags, mount}; 9 | 10 | use {OSError, Error}; 11 | use util::{path_to_cstring, as_path}; 12 | use explain::{Explainable, exists, user}; 13 | 14 | 15 | #[derive(Debug, Clone, Copy)] 16 | enum Size { 17 | Auto, 18 | Bytes(usize), 19 | Blocks(usize), 20 | } 21 | 22 | /// A tmpfs mount definition 23 | /// 24 | /// By default tmpfs is mounted with nosuid,nodev 25 | #[derive(Debug, Clone)] 26 | pub struct Tmpfs { 27 | target: CString, 28 | size: Size, 29 | nr_inodes: Option, 30 | mode: Option, 31 | uid: Option, 32 | gid: Option, 33 | flags: MsFlags, 34 | } 35 | 36 | impl Tmpfs { 37 | /// New tmpfs mount point with target path and default settngs 38 | pub fn new>(path: P) -> Tmpfs { 39 | Tmpfs { 40 | target: path_to_cstring(path.as_ref()), 41 | size: Size::Auto, 42 | nr_inodes: None, 43 | mode: None, 44 | uid: None, 45 | gid: None, 46 | flags: MsFlags::MS_NOSUID|MsFlags::MS_NODEV, 47 | } 48 | } 49 | /// Set size in bytes 50 | pub fn size_bytes(mut self, size: usize) -> Tmpfs { 51 | self.size = Size::Bytes(size); 52 | self 53 | } 54 | /// Set size in blocks of PAGE_CACHE_SIZE 55 | pub fn size_blocks(mut self, size: usize) -> Tmpfs { 56 | self.size = Size::Blocks(size); 57 | self 58 | } 59 | /// Maximum number of inodes 60 | pub fn nr_inodes(mut self, num: usize) -> Tmpfs { 61 | self.nr_inodes = Some(num); 62 | self 63 | } 64 | /// Set initial permissions of the root directory 65 | pub fn mode(mut self, mode: mode_t) -> Tmpfs { 66 | self.mode = Some(mode); 67 | self 68 | } 69 | /// Set initial owner of the root directory 70 | pub fn uid(mut self, uid: uid_t) -> Tmpfs { 71 | self.uid = Some(uid); 72 | self 73 | } 74 | /// Set initial group of the root directory 75 | pub fn gid(mut self, gid: gid_t) -> Tmpfs { 76 | self.gid = Some(gid); 77 | self 78 | } 79 | 80 | fn format_options(&self) -> Vec { 81 | let mut cur = Cursor::new(Vec::new()); 82 | match self.size { 83 | Size::Auto => {} 84 | Size::Bytes(x) => write!(cur, "size={}", x).unwrap(), 85 | Size::Blocks(x) => write!(cur, "nr_blocks={}", x).unwrap(), 86 | } 87 | if let Some(inodes) = self.nr_inodes { 88 | if cur.position() != 0 { 89 | cur.write(b",").unwrap(); 90 | } 91 | write!(cur, "nr_inodes={}", inodes).unwrap(); 92 | } 93 | if let Some(mode) = self.mode { 94 | if cur.position() != 0 { 95 | cur.write(b",").unwrap(); 96 | } 97 | write!(cur, "mode=0{:04o}", mode).unwrap(); 98 | } 99 | if let Some(uid) = self.uid { 100 | if cur.position() != 0 { 101 | cur.write(b",").unwrap(); 102 | } 103 | write!(cur, "uid={}", uid).unwrap(); 104 | } 105 | if let Some(gid) = self.gid { 106 | if cur.position() != 0 { 107 | cur.write(b",").unwrap(); 108 | } 109 | write!(cur, "gid={}", gid).unwrap(); 110 | } 111 | return cur.into_inner(); 112 | } 113 | 114 | /// Mount the tmpfs 115 | pub fn bare_mount(self) -> Result<(), OSError> { 116 | let mut options = self.format_options(); 117 | mount( 118 | Some(CStr::from_bytes_with_nul(b"tmpfs\0").unwrap()), 119 | &*self.target, 120 | Some(CStr::from_bytes_with_nul(b"tmpfs\0").unwrap()), 121 | self.flags, 122 | Some(&*options) 123 | ).map_err(|err| OSError::from_nix(err, Box::new(self))) 124 | } 125 | 126 | /// Mount the tmpfs and explain error immediately 127 | pub fn mount(self) -> Result<(), Error> { 128 | self.bare_mount().map_err(OSError::explain) 129 | } 130 | } 131 | 132 | impl fmt::Display for Tmpfs { 133 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 134 | let opts = self.format_options(); 135 | write!(fmt, "tmpfs {} -> {:?}", from_utf8(&opts).unwrap(), 136 | as_path(&self.target)) 137 | } 138 | } 139 | 140 | impl Explainable for Tmpfs { 141 | fn explain(&self) -> String { 142 | [ 143 | format!("target: {}", exists(as_path(&self.target))), 144 | format!("{}", user()), 145 | ].join(", ") 146 | } 147 | } 148 | 149 | 150 | mod test { 151 | #[cfg(test)] 152 | use super::Tmpfs; 153 | 154 | #[test] 155 | fn test_tmpfs_options() { 156 | let fs = Tmpfs::new("/tmp") 157 | .size_bytes(1 << 20) 158 | .nr_inodes(1024) 159 | .mode(0o1777) 160 | .uid(1000) 161 | .gid(1000); 162 | 163 | assert_eq!(fs.format_options(), 164 | "size=1048576,nr_inodes=1024,mode=01777,uid=1000,gid=1000".as_bytes()) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::ffi::{CStr, CString, OsStr}; 3 | use std::os::unix::ffi::OsStrExt; 4 | 5 | 6 | pub fn path_to_cstring(path: &Path) -> CString { 7 | return CString::new(path.as_os_str().as_bytes()).unwrap() 8 | } 9 | 10 | pub fn as_path(cstring: &CStr) -> &Path { 11 | OsStr::from_bytes(cstring.to_bytes()).as_ref() 12 | } 13 | -------------------------------------------------------------------------------- /vagga.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | 3 | cargo: !Command 4 | description: Run any cargo command 5 | container: ubuntu 6 | run: [cargo] 7 | 8 | make: !Command 9 | description: Build the library 10 | container: ubuntu 11 | run: [cargo, build] 12 | 13 | _bulk: !Command 14 | description: Run `bulk` command (for version bookkeeping) 15 | container: ubuntu 16 | run: [bulk] 17 | 18 | containers: 19 | 20 | ubuntu: 21 | setup: 22 | - !Ubuntu bionic 23 | - !Install [ca-certificates, build-essential, vim] 24 | 25 | - !TarInstall 26 | url: "https://static.rust-lang.org/dist/rust-1.26.0-x86_64-unknown-linux-gnu.tar.gz" 27 | script: "./install.sh --prefix=/usr \ 28 | --components=rustc,rust-std-x86_64-unknown-linux-gnu,cargo" 29 | - &bulk !Tar 30 | url: "https://github.com/tailhook/bulk/releases/download/v0.4.11/bulk-v0.4.11.tar.gz" 31 | sha256: b718bb8448e726690c94d98d004bf7575f7a429106ec26ad3faf11e0fd9a7978 32 | path: / 33 | 34 | environ: 35 | HOME: /work/target 36 | USER: pc 37 | --------------------------------------------------------------------------------