cntr-1.5.3/.cargo_vcs_info.json0000644000000001360000000000100120100ustar { "git": { "sha1": "f863c01b7a8a947c4e4a1dfc63218c399831022e" }, "path_in_vcs": "" }cntr-1.5.3/.github/dependabot.yml000064400000000000000000000004501046102023000147670ustar 00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" - package-ecosystem: "docker" directory: "/" schedule: interval: "daily" cntr-1.5.3/.github/workflows/ci.yml000064400000000000000000000026551046102023000153230ustar 00000000000000name: CI on: pull_request: branches: [master] types: [opened, reopened, synchronize] jobs: test: runs-on: ubuntu-latest strategy: matrix: target: - x86_64-unknown-linux-gnu - x86_64-unknown-linux-musl rust: - stable steps: - name: Checkout uses: actions/checkout@v4 - name: Install rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} override: true - name: Ready cache if: matrix.os == 'ubuntu-latest' run: sudo chown -R $(whoami):$(id -ng) ~/.cargo/ - name: Test uses: actions-rs/cargo@v1 with: command: test args: --target ${{ matrix.target }} lint: name: Linting (fmt + clippy) runs-on: ubuntu-latest strategy: matrix: rust: - stable steps: - name: Install rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true components: rustfmt, clippy - name: Checkout uses: actions/checkout@v4 - name: Clippy uses: actions-rs/cargo@v1 with: command: clippy - name: Format check uses: actions-rs/cargo@v1 with: command: fmt args: -- --check cntr-1.5.3/.github/workflows/publish.yml000064400000000000000000000032151046102023000163670ustar 00000000000000name: Publish on: push: tags: - '*' jobs: build: name: Publish binaries runs-on: ubuntu-latest strategy: matrix: target: - x86_64-unknown-linux-musl # TODO: fix other architectures support #- i686-unknown-linux-musl #- armv7-unknown-linux-musleabihf #- aarch64-unknown-linux-musl rust: [stable] steps: - name: Checkout uses: actions/checkout@v4 - name: Install rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} target: ${{ matrix.target }} override: true - name: Build run: cargo build --release --target ${{ matrix.target }} --locked - name: Package run: | version=$(basename ${{ github.ref }}) name="cntr-$version" depsname="cntr-src-$version" install -D target/${{ matrix.target }}/release/cntr dist/cntr-bin-$version-${{ matrix.target }} mkdir $depsname git archive HEAD | tar -x -C $depsname cargo vendor mv vendor $depsname/ tar -czvf dist/$depsname.tar.gz $depsname - name: Upload binaries to release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} file: dist/* tag: ${{ github.ref }} overwrite: true file_glob: true # publish-crates does not like this - name: Cleanup dist run: rm -r dist - uses: katyo/publish-crates@v2 with: registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} cntr-1.5.3/.github/workflows/update-flake-lock.yml000064400000000000000000000011701046102023000202070ustar 00000000000000name: update-flake-lock on: workflow_dispatch: # allows manual triggering schedule: - cron: '0 0 * * 1,4' # Run twice a week jobs: lockfile: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install Nix uses: cachix/install-nix-action@v24 with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - name: Update flake.lock uses: DeterminateSystems/update-flake-lock@v20 with: token: ${{ secrets.GITHUB_TOKEN }} pr-labels: | merge-queue cntr-1.5.3/.gitignore000064400000000000000000000000171046102023000125660ustar 00000000000000/target/ /book cntr-1.5.3/.mergify.yml000064400000000000000000000005441046102023000130460ustar 00000000000000queue_rules: - name: default merge_conditions: - check-success=buildbot/nix-eval defaults: actions: queue: allow_merging_configuration_change: true method: rebase pull_request_rules: - name: merge using the merge queue conditions: - base=master - label~=merge-queue|dependencies actions: queue: {} cntr-1.5.3/Cargo.lock0000644000000352710000000000100077730ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chashmap" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff41a3c2c1e39921b9003de14bf0439c7b63a9039637c291e1a64925d8ddfa45" dependencies = [ "owning_ref", "parking_lot 0.4.8", ] [[package]] name = "clap" version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_derive" version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "cntr" version = "1.5.3" dependencies = [ "chashmap", "clap", "cntr-fuse", "container-pid", "cpuprofiler", "lazy_static", "libc", "log", "nix", "parking_lot 0.12.1", "simple-error", ] [[package]] name = "cntr-fuse" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "410f62ca0589beaac5b106ae38765273b5aabc5bb23cc0bc553ed24e1c4d66ae" dependencies = [ "cntr-fuse-abi", "cntr-fuse-sys", "libc", "log", ] [[package]] name = "cntr-fuse-abi" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea6ecd4a8a57b0262d82c7521f498ecc01f60888091b1eb3b3c70ccafd2a25d" [[package]] name = "cntr-fuse-sys" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfb0da5a62f5c9c384041af4d81ab52c19b0064663d0b5b6626393af20c224f9" dependencies = [ "pkg-config", ] [[package]] name = "container-pid" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68d1dacc03e8237a068c9f700c4fddad103cb59b6f891ab401df10eb0bee4e76" dependencies = [ "libc", "simple-error", ] [[package]] name = "cpuprofiler" version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43f8479dbcfd2bbaa0c0c26779b913052b375981cdf533091f2127ea3d42e52b" dependencies = [ "error-chain", "lazy_static", "pkg-config", ] [[package]] name = "error-chain" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ "backtrace", "version_check", ] [[package]] name = "fuchsia-cprng" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "gimli" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "lock_api" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "maybe-uninit" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "miniz_oxide" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "nix" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags", "cfg-if", "libc", "memoffset", "pin-utils", ] [[package]] name = "object" version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] [[package]] name = "owning_ref" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdf84f41639e037b484f93433aa3897863b561ed65c6e59c7073d7c561710f37" dependencies = [ "stable_deref_trait", ] [[package]] name = "parking_lot" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "149d8f5b97f3c1133e3cfcd8886449959e856b557ff281e292b733d7c69e005e" dependencies = [ "owning_ref", "parking_lot_core 0.2.14", ] [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core 0.9.8", ] [[package]] name = "parking_lot_core" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4db1a8ccf734a7bce794cc19b3df06ed87ab2f3907036b693c68f56b4d4537fa" dependencies = [ "libc", "rand", "smallvec 0.6.14", "winapi", ] [[package]] name = "parking_lot_core" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec 1.11.1", "windows-targets", ] [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "proc-macro2" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" dependencies = [ "fuchsia-cprng", "libc", "rand_core 0.3.1", "rdrand", "winapi", ] [[package]] name = "rand_core" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" dependencies = [ "rand_core 0.4.2", ] [[package]] name = "rand_core" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rdrand" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" dependencies = [ "rand_core 0.3.1", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "simple-error" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8542b68b8800c3cda649d2c72d688b6907b30f1580043135d61669d4aad1c175" [[package]] name = "smallvec" version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" dependencies = [ "maybe-uninit", ] [[package]] name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "syn" version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" cntr-1.5.3/Cargo.toml0000644000000027700000000000100100140ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "cntr" version = "1.5.3" authors = ["Jörg Thalheim "] description = "A container debugging tool based on FUSE" readme = "README.md" categories = [ "development-tools::debugging", "os::unix-apis", ] license = "MIT" repository = "https://github.com/Mic92/cntr" [profile.release] lto = true [[bin]] name = "cntr" path = "src/bin/main.rs" [dependencies.chashmap] version = "2.2.2" [dependencies.clap] version = "4" features = [ "std", "cargo", "derive", "help", ] default-features = false [dependencies.cntr-fuse] version = "0.4.2" default-features = false [dependencies.container-pid] version = "0.2.0" [dependencies.cpuprofiler] version = "0.0.4" optional = true [dependencies.libc] version = "0.2.151" [dependencies.log] version = "0.4.20" [dependencies.nix] version = "0.26.2" [dependencies.parking_lot] version = "0.12.1" [dependencies.simple-error] version = "0.3.0" [dev-dependencies.lazy_static] version = "1.4" [features] profiling = [] verbose_fuse_test_log = [] cntr-1.5.3/Cargo.toml.orig000064400000000000000000000014701046102023000134710ustar 00000000000000[package] name = "cntr" edition = "2018" description = "A container debugging tool based on FUSE" version = "1.5.3" authors = ["Jörg Thalheim "] categories = ["development-tools::debugging", "os::unix-apis"] repository = "https://github.com/Mic92/cntr" license = "MIT" [dependencies] cpuprofiler = { version = "0.0.4", optional = true } clap = { version = "4", default-features = false, features = ["std", "cargo", "derive", "help"] } log = "0.4.20" libc = "0.2.151" parking_lot = "0.12.1" nix = "0.26.2" container-pid = "0.2.0" simple-error = "0.3.0" cntr-fuse = { version = "0.4.2", default-features = false } chashmap = "2.2.2" [[bin]] name = "cntr" path = "src/bin/main.rs" [profile.release] lto = true [features] verbose_fuse_test_log = [] profiling = [] [dev-dependencies] lazy_static = "1.4" cntr-1.5.3/Dockerfile000064400000000000000000000020241046102023000125700ustar 00000000000000# Example for a slim/fat container setup FROM rust:1.74.1 as cntr RUN rustup target add x86_64-unknown-linux-musl RUN curl -sL https://github.com/Mic92/docker-pid/releases/download/1.0.2/docker-pid-linux-amd64 \ > /usr/bin/docker-pid && \ chmod 755 /usr/bin/docker-pid COPY Cargo.toml Cargo.lock ./ # weird trick to cache crates RUN cargo build --release --target=x86_64-unknown-linux-musl || true COPY src ./src/ RUN cargo build --release --target=x86_64-unknown-linux-musl RUN strip target/x86_64-unknown-linux-musl/release/cntr -o /usr/bin/cntr FROM busybox WORKDIR /root/ COPY --from=cntr /usr/bin/cntr /usr/bin/cntr COPY --from=cntr /usr/bin/docker-pid /usr/bin/docker-pid ENTRYPOINT ["/usr/bin/cntr"] # Build with: # $ docker build . -t cntr # Assuming you have a container called mycontainer, you want to attach to (docker run --name mycontainer -ti --rm busybox sh) # you can then run: # $ sudo docker run --pid=host --privileged=true -v /var/run/docker.sock:/var/run/docker.sock -ti --rm cntr attach mycontainer /bin/sh cntr-1.5.3/LICENSE.md000064400000000000000000000020571046102023000122100ustar 00000000000000MIT License Copyright (c) 2018 Jörg Thalheim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. cntr-1.5.3/Makefile000064400000000000000000000004311046102023000122360ustar 00000000000000PREFIX = $(DESTDIR)/usr/local BINDIR = $(PREFIX)/bin INSTALL_PROGRAM ?= install CARGO ?= cargo TARGET = target/release/cntr all: $(TARGET) $(TARGET): $(CARGO) build --release install: all $(INSTALL_PROGRAM) -D $(TARGET) $(BINDIR)/cntr .PHONY: clean clean: rm -rf $(TARGET) cntr-1.5.3/README.md000064400000000000000000000344731046102023000120720ustar 00000000000000# cntr Say no to `$ apt install vim` in containers! `cntr` is a replacement for `docker exec` that brings all your developers tools with you. This is done by mounting the file system from one container or the host into the target container by creating a nested container with the help of a FUSE filesystem. This allows to ship minimal runtime image in production and limit the surface for exploits. Cntr was also published in [Usenix ATC 2018](https://www.usenix.org/conference/atc18/presentation/thalheim). See [bibtex](#bibtex) for citation. ## Demo In this two minute recording you learn all the basics of cntr: [![asciicast](https://asciinema.org/a/PQB5RnRGLIPn88a1R2MtPccdy.png)](https://asciinema.org/a/PQB5RnRGLIPn88a1R2MtPccdy) ## Features - For convenience cntr supports container names/identifier for the following container engines natively: * docker * podman * LXC * LXD * rkt * systemd-nspawn * containerd - For other container engines cntr also takes process ids (PIDs) instead of container names. ## Installation Cntr can be only supports linux. ### Pre-build static-linked binary For linux x86_64 we build static binaries for every release. More platforms can added on request. See the [release tab](https://github.com/Mic92/cntr/releases/) for pre-build tarballs. At runtime only commandline utils of the container engine in questions are required. ### Build from source All you need for compilation is rust + cargo. Checkout [rustup.rs](https://rustup.rs/) on how to get a working rust toolchain. Then run: Either: ```console $ cargo install cntr ``` Or the latest master: ```console $ cargo install --git https://github.com/Mic92/cntr ``` For offline builds we also provided a tarball with all dependencies bundled [here](https://github.com/Mic92/cntr/releases) for compilation with [cargo-vendor](https://github.com/alexcrichton/cargo-vendor). ## Usage At a high-level cntr provides two subcommands: `attach` and `exec`: - `attach`: Allows you to attach to a container with your own native shell/commands. Cntr will mount the container at `/var/lib/cntr`. The container itself will run unaffected as the mount changes are not visible to container processes. - Example: `cntr attach ` where `container_id` can be a container identifier or process id (see examples below). - `exec`: Once you are in the container, you can also run commands from the container filesystem itself. Since those might need their native mount layout at `/` instead of `/var/lib/cntr`, cntr provides `exec` subcommand to chroot to container again and also resets the environment variables that might have been changed by the shell. - Example: `cntr exec ` where `command` is an executable in the container **Note**: Cntr needs to run on the same host as the container. It does not work if the container is running in a virtual machine while cntr is running on the hypervisor. ```console $ cntr --help Cntr 1.5.1 Jörg Thalheim Enter or executed in container USAGE: cntr FLAGS: -h, --help Prints help information -V, --version Prints version information SUBCOMMANDS: attach Enter container exec Execute command in container filesystem help Prints this message or the help of the given subcommand(s) ``` ```console $ cntr attach --help cntr-attach 1.5.1 Jörg Thalheim Enter container USAGE: cntr attach [OPTIONS] [command]... FLAGS: -h, --help Prints help information OPTIONS: --effective-user effective username that should be owner of new created files on the host -t, --type Container types to try (sperated by ','). [default: all but command] [possible values: process_id, rkt, podman, docker, nspawn, lxc, lxd, containerd, command] ARGS: container id, container name or process id ... Command and its arguments to execute after attach. Consider prepending it with '-- ' to prevent parsing of '-x'-like flags. [default: $SHELL] ``` ```console $ cntr exec --help cntr-exec 1.5.1 Jörg Thalheim Execute command in container filesystem USAGE: cntr exec [command]... FLAGS: -h, --help Prints help information -V, --version Prints version information ARGS: ... Command and its arguments to execute after attach. Consider prepending it with '-- ' to prevent parsing of '-x'-like flags. [default: $SHELL] ``` ### Docker 1: Find out the container name/container id: ```console $ docker run --name boxbusy -ti busybox $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 55a93d71b53b busybox "sh" 22 seconds ago Up 20 seconds boxbusy ``` Either provide a container id... ```console $ cntr attach 55a93d71b53b [root@55a93d71b53b:/var/lib/cntr]# echo "I am in a container!" [root@55a93d71b53b:/var/lib/cntr]# ip addr 1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 40: eth0@if41: mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever [root@55a93d71b53b:/var/lib/cntr]# vim etc/resolv.conf ``` ...or the container name. Use `cntr exec` to execute container native commands (while running in the cntr shell). ```console $ cntr attach boxbusy [root@55a93d71b53b:/var/lib/cntr]# cntr exec -- sh -c 'busybox | head -1' ``` You can also use Dockerfile from this repo to build a docker container with cntr: ``` console $ docker build -f Dockerfile . -t cntr # boxbusy here is the name of the target container to attach to $ docker run --pid=host --privileged=true -v /var/run/docker.sock:/var/run/docker.sock -ti --rm cntr attach boxbusy /bin/sh ``` ### Podman See docker usage, just replace `docker` with the `podman` command. ### LXD 1: Create a container and start it ```console $ lxc image import images:/alpine/edge $ lxc launch images:alpine/edge $ lxc list +-----------------+---------+------+------+------------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +-----------------+---------+------+------+------------+-----------+ | amazed-sailfish | RUNNING | | | PERSISTENT | 0 | +-----------------+---------+------+------+------------+-----------+ ``` 2: Attach to the container with cntr ```console $ cntr attach amazed-sailfish $ cat etc/hostname amazed-sailfish ``` ### LXC 1: Create a container and start it ```console $ lxc-create --name ubuntu -t download -- -d ubuntu -r xenial -a amd64 $ lxc-start --name ubuntu -F ... Ubuntu 16.04.4 LTS ubuntu console ubuntu login: $ lxc-ls ubuntu ``` 2: Attach to container with cntr: ```console $ cntr attach ubuntu [root@ubuntu2:/var/lib/cntr]# cat etc/os-release NAME="Ubuntu" VERSION="16.04.4 LTS (Xenial Xerus)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 16.04.4 LTS" VERSION_ID="16.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" VERSION_CODENAME=xenial UBUNTU_CODENAME=xenial ``` ### rkt 1: Find out the container uuid: ```console $ rkt run --interactive=true docker://busybox $ rkt list UUID APP IMAGE NAME STATE CREATED STARTED NETWORKS c2d2e87e busybox registry-1.docker.io/library/busybox:latest running 6 minutes ago 6 minutes ago default:ip4=172.16.28.3 ``` 2: Attach with cntr ```console # make sure your container is still running! $ cntr attach c2d2e87e # Finally not the old ugly top! [gen0@rkt-c2d2e87e-e798-4341-ae93-26f6cbb7c017:/var/lib/cntr]# htop ... ``` With cntr you can also debug stage1 of rkt - even there is no support from rkt itself. ```console $ ps aux | grep stage1 joerg 13546 0.0 0.0 120808 1608 pts/12 S+ 11:10 0:00 grep --binary-files=without-match --directories=skip --color=auto stage1 root 22232 0.0 0.0 54208 2656 pts/7 S+ 10:54 0:00 stage1/rootfs/usr/lib/ld-linux-x86-64.so.2 stage1/rootfs/usr/bin/systemd-nspawn --boot --notify-ready=yes --register=true --link-journal=try-guest --quiet --uuid=c2d2e87e-e798-4341-ae93-26f6cbb7c017 --machine=rkt-c2d2e87e-e798-4341-ae93-26f6cbb7c017 --directory=stage1/rootfs --capability=CAP_AUDIT_WRITE,CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_FSETID,CAP_FOWNER,CAP_KILL,CAP_MKNOD,CAP_NET_RAW,CAP_NET_BIND_SERVICE,CAP_SETUID,CAP_SETGID,CAP_SETPCAP,CAP_SETFCAP,CAP_SYS_CHROOT -- --default-standard-output=tty --log-target=null --show-status=0 ``` Therefore we use the process id instead of the container uuid: ```console $ cntr attach 22232 # new and exiting territory! [root@turingmachine:/var/lib/cntr]# mount | grep pods sysfs on /var/lib/cntr/var/lib/rkt/pods/run/c2d2e87e-e798-4341-ae93-26f6cbb7c017/stage1/rootfs/sys type sysfs (ro,nosuid,nodev,noexec,relatime) tmpfs on /var/lib/cntr/var/lib/rkt/pods/run/c2d2e87e-e798-4341-ae93-26f6cbb7c017/stage1/rootfs/sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755) cgroup on /var/lib/cntr/var/lib/rkt/pods/run/c2d2e87e-e798-4341-ae93-26f6cbb7c017/stage1/rootfs/sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory) ``` ### systemd-nspawn 1: Start container ```console $ wget https://cloud-images.ubuntu.com/releases/16.04/release/ubuntu-16.04-server-cloudimg-amd64-root.tar.xz $ mkdir /var/lib/machines/ubuntu $ tar -xf ubuntu-16.04-server-cloudimg-amd64-root.tar.xz -C /var/lib/machines/ubuntu $ systemd-nspawn -b -M ubuntu $ machinectl list MACHINE CLASS SERVICE OS VERSION ADDRESSES ubuntu container systemd-nspawn ubuntu 16.04 - ``` 2: Attach ```console $ cntr attach ubuntu ``` ### Generic process id The minimal information needed by cntr is the process id of a container process you want to attach to. ```console # Did you now chromium uses namespaces too? $ ps aux | grep 'chromium --type=renderer' joerg 17498 11.7 1.0 1394504 174256 ? Sl 15:16 0:08 /usr/bin/chromium ``` In this case 17498 is the pid we are looking for. ```console $ cntr attach 17498 # looks quite similar to our system, but with less users [joerg@turingmachine cntr]$ ls -la / total 240 drwxr-xr-x 23 nobody nogroup 23 Mar 13 15:05 . drwxr-xr-x 23 nobody nogroup 23 Mar 13 15:05 .. drwxr-xr-x 2 nobody nogroup 3 Mar 13 15:14 bin drwxr-xr-x 4 nobody nogroup 16384 Jan 1 1970 boot drwxr-xr-x 24 nobody nogroup 4120 Mar 13 14:56 dev drwxr-xr-x 52 nobody nogroup 125 Mar 13 15:14 etc drwxr-xr-x 3 nobody nogroup 3 Jan 8 16:17 home drwxr-xr-x 8 nobody nogroup 8 Feb 9 22:10 mnt dr-xr-xr-x 306 nobody nogroup 0 Mar 13 09:38 proc drwx------ 22 nobody nogroup 43 Mar 13 15:09 root ... ``` ### Containerd For containerd integration the `ctr` binary is required. You can get a binary by running: ``` console $ GOPATH=$(mktemp -d) $ go get github.com/containerd/containerd/cmd/ctr $ $GOPATH/bin/ctr --help ``` Put the resulting `ctr` binary in your `$PATH` 1: Start container ```console $ ctr images pull docker.io/library/busybox:latest $ ctr run docker.io/library/busybox:latest boxbusy $ ctr tasks lists TASK PID STATUS boxbusy 24310 RUNNING ``` 2: Attach ```console $ cntr attach boxbusy ``` It's also possible to run cntr from a container itself. This repository contains a example Dockerfile for that: ```console $ docker build -f Dockerfile.example . -t cntr $ docker save cntr > cntr.tar $ ctr images import --base-name cntr ./cntr.tar ``` In this example we attach to containerd by process id. The process id of a task is given in `ctr tasks list`. ```console $ ctr run --privileged --with-ns pid:/proc/1/ns/pid --tty docker.io/library/cntr:latest cntr /usr/bin/cntr attach 31523 /bin/sh ``` To resolve containerd names one also would need to add the `ctr` binary (~12mb) to the Dockerfile. ## Additional Config ### ZFS `cntr` requires POSIX ACLs be enabled under ZFS. By default, Linux ZFS doesn't have POSIX ACLs enabled. This results in the following error when trying to `attach`: ```console unable to move container mounts to new mountpoint: EOPNOTSUPP: Operation not supported on transport endpoint ``` To enable POSIX ACLs on the ZFS dataset: ```console $ zfs set acltype=posixacl zpool/media $ zfs set xattr=sa zpool/media # optional, but encouraged for best performance ``` # How it works Cntr is container-agnostic: Instead of interfacing with container engines, it implements the underlying operating system API. It treats every container as a group of processes, that it can inherit properties from. Cntr inherits the following container properties: * Namespaces (mount, uts, pid, net, cgroup, ipc) * Cgroups * Apparamor/selinux * Capabilities * User/group ids * Environment variables * The following files: /etc/passwd, /etc/hostname, /etc/hosts, /etc/resolv.conf Under the hood it spawns a shell or user defined program that inherits the full context of the container and mount itself as a fuse filesystem. We extensively evaluated the correctness and performance of cntr's filesystem using [xfstests](https://github.com/Mic92/xfstests-cntr) and a wide range of filesystem performance benchmarks (iozone, pgbench, dbench, fio, fs-mark, postmark, ...) # Related projects - [nsenter](https://manpages.debian.org/testing/manpages-de/nsenter.1.de.html) - Only covers linux namespaces and the user is limited to tools installed in the containers - [toolbox](https://github.com/coreos/toolbox) - Does attach from a container to the host, this is the opposite of what Cntr is doing # Bibtex We published a paper with all technical details about Cntr in [Usenix ATC 2018](https://www.usenix.org/conference/atc18/presentation/thalheim). ```bibtex @inproceedings{cntr-atc18, author = {J{\"o}rg Thalheim and Pramod Bhatotia and Pedro Fonseca and Baris Kasikci}, title = {Cntr: Lightweight {OS} Containers}, booktitle = {2018 {USENIX} Annual Technical Conference ({USENIX} {ATC} 18)}, year = {2018}, } ``` cntr-1.5.3/default.nix000064400000000000000000000007041046102023000127450ustar 00000000000000{ pkgs ? import {} , src ? ./. }: with pkgs; pkgs.rustPlatform.buildRustPackage { name = "cntr"; inherit src; cargoLock.lockFile = ./Cargo.lock; nativeBuildInputs = [ pkgs.clippy ]; meta = with pkgs.lib; { description = "A container debugging tool based on FUSE"; homepage = "https://github.com/Mic92/cntr"; license = licenses.mit; maintainers = with maintainers; [ mic92 ]; platforms = platforms.unix; }; } cntr-1.5.3/examples/cntrfs.rs000064400000000000000000000056131046102023000142700ustar 00000000000000use cntr::fs::{CntrFs, CntrMountOptions}; #[cfg(feature = "profiling")] use cpuprofiler::PROFILER; use lazy_static::lazy_static; use log::{error, info}; use nix::sys::signal; use nix::{mount, unistd}; use simple_error::{try_with, SimpleError}; use std::env; use std::path::Path; use std::process; use std::sync::mpsc::{sync_channel, SyncSender}; use std::sync::Mutex; struct MountGuard { mount_point: String, } impl Drop for MountGuard { fn drop(&mut self) { let _ = mount::umount(self.mount_point.as_str()); } } pub type Result = std::result::Result; lazy_static! { static ref SIGNAL_SENDER: Mutex>> = Mutex::new(None); } extern "C" fn signal_handler(_: ::libc::c_int) { let sender = match SIGNAL_SENDER.lock().expect("cannot lock sender").take() { Some(s) => { info!("shutdown cntrfs"); s } None => { info!("received sigterm. stopping already in progress"); return; } }; if let Err(e) = sender.send(()) { error!("cannot notify main process: {}", e); } } pub fn setup_signal_handler(sender: SyncSender<()>) -> Result<()> { try_with!(SIGNAL_SENDER.lock(), "cannot get lock").replace(sender); let sig_action = signal::SigAction::new( signal::SigHandler::Handler(signal_handler), signal::SaFlags::empty(), signal::SigSet::empty(), ); unsafe { try_with!( signal::sigaction(signal::SIGINT, &sig_action), "unable to register SIGINT handler" ); try_with!( signal::sigaction(signal::SIGTERM, &sig_action), "unable to register SIGTERM handler" ); } Ok(()) } fn main() { if cfg!(feature = "verbose_fuse_test_log") { cntr::enable_debug_log().unwrap(); } let args: Vec = env::args().collect(); if args.len() < 3 { println!("USAGE: {} from_path to_path", args[0]); process::exit(1); } let res = unsafe { unistd::fork().unwrap() }; if let unistd::ForkResult::Parent { .. } = res { return; } #[cfg(feature = "profiling")] PROFILER.lock().unwrap().start("./cntrfs.profile").unwrap(); let cntr = CntrFs::new( &CntrMountOptions { prefix: &args[1], uid_map: cntr::DEFAULT_ID_MAP, gid_map: cntr::DEFAULT_ID_MAP, effective_uid: None, effective_gid: None, }, None, ) .unwrap(); cntr.mount(Path::new(&args[2]), &None).unwrap(); let guard = MountGuard { mount_point: args[2].clone(), }; cntr.spawn_sessions().unwrap(); let (sender, receiver) = sync_channel(1); setup_signal_handler(sender).unwrap(); // wait for exit signal let _ = receiver.recv(); drop(guard); #[cfg(feature = "profiling")] PROFILER.lock().unwrap().stop().unwrap(); } cntr-1.5.3/flake.lock000064400000000000000000000027111046102023000125350ustar 00000000000000{ "nodes": { "nixpkgs": { "locked": { "lastModified": 1703063446, "narHash": "sha256-e59l84kPNX+clUJUvHJ6+0C3Tw+gYeJn/QaH1dr/3kg=", "owner": "NixOS", "repo": "nixpkgs", "rev": "7c33cd6bf01cf09f77bfdf6741cddfffa2cd5640", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "nixpkgs": "nixpkgs", "utils": "utils" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } }, "utils": { "inputs": { "systems": "systems" }, "locked": { "lastModified": 1701680307, "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "owner": "numtide", "repo": "flake-utils", "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "type": "github" }, "original": { "owner": "numtide", "repo": "flake-utils", "type": "github" } } }, "root": "root", "version": 7 } cntr-1.5.3/flake.nix000064400000000000000000000021121046102023000123760ustar 00000000000000{ description = "A container debugging tool based on FUSE"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; utils.url = "github:numtide/flake-utils"; }; outputs = { self, nixpkgs, utils }: (utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { packages.cntr = pkgs.callPackage ./. { src = self; }; defaultPackage = self.packages.${system}.cntr; devShell = pkgs.mkShell { buildInputs = [ pkgs.cargo pkgs.cargo-watch pkgs.rustc pkgs.clippy pkgs.cargo-bloat pkgs.rust-analyzer ]; #buildInputs = [ pkgs.pkgsMusl.cargo pkgs.pkgsMusl.rustc ]; }; })) // { checks.x86_64-linux = let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; in { inherit (import ./vm-test.nix { makeTest = import (nixpkgs + "/nixos/tests/make-test-python.nix"); inherit pkgs; inherit (self.packages.${system}) cntr; }) docker podman; }; }; } cntr-1.5.3/renovate.json000064400000000000000000000001051046102023000133120ustar 00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json" } cntr-1.5.3/rustfmt.toml000064400000000000000000000000271046102023000132000ustar 00000000000000reorder_imports = true cntr-1.5.3/scripts/create-release.sh000075500000000000000000000011471046102023000155120ustar 00000000000000#!/usr/bin/env bash set -eu -o pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" cd $SCRIPT_DIR/.. version=${1:-} if [[ -z "$version" ]]; then echo "USAGE: $0 version" 2>/dev/null exit 1 fi if [[ "$(git symbolic-ref --short HEAD)" != "master" ]]; then echo "must be on master branch" 2>/dev/null exit 1 fi sed -i -e "0,/version =/ s!^version = \".*\"!version = \"${version}\"!" Cargo.toml git add Cargo.toml cargo build --release git add Cargo.lock git commit -m "bump version to ${version}" git tag "${version}" echo 'now run `git push --tags origin master`' cntr-1.5.3/scripts/drone-secrets.yml000064400000000000000000000034551046102023000155760ustar 00000000000000DRONE_TOKEN: ENC[AES256_GCM,data:kgK6O28yNAx6LuC7eBsI06jrnBlxO5Nx+5/qz7s7a7g=,iv:RjTr23dySD+PrDPIrYd3d5fLTlXOTyp81g65ERRxrI4=,tag:Qsyapamyr5yAnYGVVbjQIg==,type:str] sops: kms: [] gcp_kms: [] azure_kv: [] hc_vault: [] lastmodified: '2020-10-26T11:26:17Z' mac: ENC[AES256_GCM,data:O1cvb7JIM/1YxzG/xTcQjggOnMVVmuTyOzmbhoAH9KME6ffyNXwJYkPEuqlgmMf6DAUvyQ2K2YPCmsxc7kIW8fEgZLWd/vCVeeuACdmDDVEXF1Zih9CFT5qadfedbjwx4dTwj+rmbH+GNhNAx/nEzgCU+vjhnTHYrPXqtyckAPY=,iv:XeFq/hSxaPl2v4nScp+P54yyeV/l3WlXxN12naPv2ng=,tag:iIvPfueo1qVGRLjhhEn2ug==,type:str] pgp: - created_at: '2020-10-26T11:25:42Z' enc: | -----BEGIN PGP MESSAGE----- hQIMA+F5RUsgHiPKAQ//RQ1vIXamzU0lTQqOPbrO0sThG0qV+3T3SRgfYXrpsoHJ G+UZZ1QnUVqKHc6JWTgLQwadCm9R+FC72K4DYijHZydo4lMDtNcoowV5ih9jgfrq dxyuIPXOhlaP3E0PseAN1S/0ztNyyQOjD5ERcZczp36hSMpHiZB6elUk4Mij1DYU x3cpjaa2Pej7MMgxqNwb3Gi6g3xEInI2fEj3TzxShu3cFB/T+CkFvJERGD8O1UcS DtsGX8ys6GWVzpVJFBVJ3JzVE3f2ZPVJIEmksbgGq1+RgRMW64JwMrLpd7d8S7uV lSqFWVTMsARtkg2B52bm4o6tN5cl2eqx84c2QRX57Oy1dZLqtAAeLVqYBpUTKNWY 5VEd5Sr019i8QwEr1R28sxIpIor2Z4LuH00cKixylljRrYBng/meAirkKycDs2sH lRMe2kHnzxKsrgs4xcHTSgmBNxO/djd4QkbunFkAe+tInCC3dlY4QEmEPUybEgzX 6hAgQl99tjaqvapifhLdpIOQJzhWaR1ub3SZ4oCaGh7NPlqjXxDXnU4REHFSOSvh 01EGXzlmWGaLHwQmeRAPqjhvBbrVvjSUsiIdRxgi8mirZ0yjjWRDEZVy7Bmbjkvs ok5U/By2HYDT4yVMaJ2Tc0cJUpchJZzU7KSWbxtgNPb79YO32gNAQM8/5DO9r9bS XgGXPdfOr7YGR5m4D3WZuokBjh2OrYT64atSqkRYi3Y1Qn9ytcr/JknE+gnK7iz+ G6W3TOE1lKvHl5EaczsT2WKzscdxMyYHheY2V+8JzQByYiGg13d1L2wNK7e+chs= =j4m9 -----END PGP MESSAGE----- fp: F095F28F7B1BA4DD0D93554EE179454B201E23CA unencrypted_suffix: _unencrypted version: 3.6.1 cntr-1.5.3/scripts/pre-commit000075500000000000000000000055421046102023000142770ustar 00000000000000#!/usr/bin/env bash # Copyright 2016 Google Inc. All Rights Reserved. # # Licensed under the MIT License, . # This file may not be copied, modified, or distributed except according to those terms. # # Pre-commit hook for the cntr repository. To use this hook, copy it to .git/hooks in your # repository root. # # This precommit checks the following: # 1. All filenames are ascii # 2. There is no bad whitespace # 3. rustfmt is installed # 4. rustfmt is a noop on files that are in the index # # Options: # # - CNTR_SKIP_RUSTFMT, default = 0 # # Set this to 1 to skip running rustfmt # # Note that these options are most useful for testing the hooks themselves. Use git commit # --no-verify to skip the pre-commit hook altogether. RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color PREFIX="${GREEN}[PRECOMMIT]${NC}" FAILURE="${RED}FAILED${NC}" WARNING="${RED}[WARNING]${NC}" SKIPPED="${YELLOW}SKIPPED${NC}" SUCCESS="${GREEN}ok${NC}" if git rev-parse --verify HEAD &>/dev/null then against=HEAD else # Initial commit: diff against an empty tree object against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi FAILED=0 printf "${PREFIX} Checking that all filenames are ascii ... " # Note that the use of brackets around a tr range is ok here, (it's # even required, for portability to Solaris 10's /usr/bin/tr), since # the square bracket bytes happen to fall in the designated range. if test $(git diff --cached --name-only --diff-filter=A -z $against | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 then FAILED=1 printf "${FAILURE}\n" else printf "${SUCCESS}\n" fi printf "${PREFIX} Checking for bad whitespace ... " git diff-index --check --cached $against -- &>/dev/null if [ "$?" != 0 ]; then FAILED=1 printf "${FAILURE}\n" else printf "${SUCCESS}\n" fi printf "${PREFIX} Checking for rustfmt ... " command -v cargo fmt &>/dev/null if [ $? == 0 ]; then printf "${SUCCESS}\n" else printf "${FAILURE}\n" exit 1 fi printf "${PREFIX} Checking for shasum ... " command -v shasum &>/dev/null if [ $? == 0 ]; then printf "${SUCCESS}\n" else printf "${FAILURE}\n" exit 1 fi # Just check that running rustfmt doesn't do anything to the file. I do this instead of # modifying the file because I don't want to mess with the developer's index, which may # not only contain discrete files. printf "${PREFIX} Checking formatting ... " FMTRESULT=0 diff="" for file in $(git diff --name-only --cached); do if [ ${file: -3} == ".rs" ]; then diff="$diff$(cargo fmt -- --unstable-features --skip-children --check $file)" fi done if grep --quiet "^[-+]" <<< "$diff"; then FMTRESULT=1 fi if [ "${CNTR_SKIP_RUSTFMT}" == 1 ]; then printf "${SKIPPED}\n"$? elif [ ${FMTRESULT} != 0 ]; then FAILED=1 printf "${FAILURE}\n" echo "$diff" | sed 's/Using rustfmt config file.*$/d/' else printf "${SUCCESS}\n" fi exit ${FAILED} cntr-1.5.3/scripts/sign-drone.sh000075500000000000000000000005361046102023000146770ustar 00000000000000#!/usr/bin/env nix-shell #!nix-shell -i bash -p bash -p drone-cli -p sops set -xeuo pipefail DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" eval "$(sops -d --output-type dotenv ${DIR}/drone-secrets.yml)" cd "$DIR"/.. export DRONE_SERVER=https://drone.thalheim.io DRONE_TOKEN drone sign Mic92/cntr --save git add .drone.yml cntr-1.5.3/src/attach/child.rs000064400000000000000000000113571046102023000142730ustar 00000000000000use log::warn; use nix::sys::signal::{self, Signal}; use nix::unistd; use nix::unistd::{Gid, Uid}; use simple_error::{bail, try_with}; use std::convert::TryFrom; use std::env; use std::fs::File; use std::os::unix::io::IntoRawFd; use std::os::unix::prelude::*; use std::path::PathBuf; use std::process; use crate::capabilities; use crate::cgroup; use crate::cmd::Cmd; use crate::fs; use crate::ipc; use crate::lsm; use crate::mountns; use crate::namespace; use crate::procfs::ProcStatus; use crate::pty; use crate::result::Result; pub struct ChildOptions<'a> { pub command: Option, pub arguments: Vec, pub process_status: ProcStatus, pub mount_ready_sock: &'a ipc::Socket, pub fs: fs::CntrFs, pub home: Option, pub uid: Uid, pub gid: Gid, } pub fn run(options: &ChildOptions) -> Result<()> { let lsm_profile = try_with!( lsm::read_profile(options.process_status.global_pid), "failed to get lsm profile" ); let mount_label = if let Some(ref p) = lsm_profile { try_with!( p.mount_label(options.process_status.global_pid), "failed to read mount options" ) } else { None }; try_with!( cgroup::move_to(unistd::getpid(), options.process_status.global_pid), "failed to change cgroup" ); let cmd = Cmd::new( options.command.clone(), options.arguments.clone(), options.process_status.global_pid, options.home.clone(), )?; let supported_namespaces = try_with!( namespace::supported_namespaces(), "failed to list namespaces" ); if !supported_namespaces.contains(namespace::MOUNT.name) { bail!("the system has no support for mount namespaces") }; let mount_namespace = try_with!( namespace::MOUNT.open(options.process_status.global_pid), "could not access mount namespace" ); let mut other_namespaces = Vec::new(); let other_kinds = &[ namespace::UTS, namespace::CGROUP, namespace::PID, namespace::NET, namespace::IPC, namespace::USER, ]; for kind in other_kinds { if !supported_namespaces.contains(kind.name) { continue; } if kind.is_same(options.process_status.global_pid) { continue; } other_namespaces.push(try_with!( kind.open(options.process_status.global_pid), "failed to open {} namespace", kind.name )); } try_with!(mount_namespace.apply(), "failed to apply mount namespace"); mountns::setup( &options.fs, options.mount_ready_sock, mount_namespace, &mount_label, )?; let dropped_groups = if supported_namespaces.contains(namespace::USER.name) { unistd::setgroups(&[]).is_ok() } else { false }; for ns in other_namespaces { try_with!(ns.apply(), "failed to apply namespace"); } if supported_namespaces.contains(namespace::USER.name) { if let Err(e) = unistd::setgroups(&[]) { if !dropped_groups { try_with!(Err(e), "could not set groups"); } } try_with!(unistd::setgid(options.gid), "could not set group id"); try_with!(unistd::setuid(options.uid), "could not set user id"); } try_with!( capabilities::drop(options.process_status.effective_capabilities), "failed to apply capabilities" ); let pty_master = try_with!(pty::open_ptm(), "open pty master"); try_with!(pty::attach_pts(&pty_master), "failed to setup pty master"); // we have to destroy f manually, since we only borrow fd here. let f = unsafe { File::from_raw_fd(pty_master.as_raw_fd()) }; let res = options.mount_ready_sock.send(&[], &[&f]); f.into_raw_fd(); try_with!(res, "failed to send pty file descriptor to parent process"); if let Err(e) = env::set_current_dir("/var/lib/cntr") { warn!("failed to change directory to /var/lib/cntr: {}", e); } if let Some(profile) = lsm_profile { try_with!(profile.inherit_profile(), "failed to inherit lsm profile"); } let status = cmd.run()?; if let Some(signum) = status.signal() { let signal = try_with!( Signal::try_from(signum), "invalid signal received: {}", signum ); try_with!( signal::kill(unistd::getpid(), signal), "failed to send signal {:?} to own pid", signal ); } if let Some(code) = status.code() { process::exit(code); } eprintln!( "BUG! command exited successfully, \ but was neither terminated by a signal nor has an exit code" ); process::exit(1); } cntr-1.5.3/src/attach/mod.rs000064400000000000000000000055741046102023000137730ustar 00000000000000use crate::dotcntr; use crate::fs; use crate::ipc; use crate::procfs; use crate::result::Result; use crate::user_namespace::IdMap; use nix::unistd::{self, ForkResult, Pid, User}; use simple_error::{bail, try_with}; use std::fs::{create_dir_all, metadata}; use std::os::unix::prelude::*; mod child; mod parent; pub struct AttachOptions { pub command: Option, pub arguments: Vec, pub container_name: String, pub container_types: Vec>, pub effective_user: Option, } pub fn attach(opts: &AttachOptions) -> Result<()> { let container_pid = match container_pid::lookup_container_pid( opts.container_name.as_str(), &opts.container_types, ) { Ok(pid) => Pid::from_raw(pid), Err(e) => bail!("{}", e), }; let (uid_map, gid_map) = try_with!( IdMap::new_from_pid(container_pid), "failed to read usernamespace properties of {}", container_pid ); let metadata = try_with!( metadata(procfs::get_path().join(container_pid.to_string())), "failed to container uid/gid" ); let mut home = None; let mut effective_uid = None; let mut effective_gid = None; let container_uid = unistd::Uid::from_raw(uid_map.map_id_up(metadata.uid())); let container_gid = unistd::Gid::from_raw(gid_map.map_id_up(metadata.gid())); if let Some(ref passwd) = opts.effective_user { effective_uid = Some(passwd.uid); effective_gid = Some(passwd.gid); home = Some(passwd.dir.clone()); } let process_status = try_with!( procfs::status(container_pid), "failed to get status of target process" ); let dotcntr = try_with!(dotcntr::create(&process_status), "failed to setup /.cntr"); let cntrfs = try_with!( fs::CntrFs::new( &fs::CntrMountOptions { prefix: "/", uid_map, gid_map, effective_uid, effective_gid, }, Some(dotcntr), ), "cannot mount filesystem" ); try_with!( create_dir_all("/var/lib/cntr"), "failed to create /var/lib/cntr" ); let (parent_sock, child_sock) = try_with!(ipc::socket_pair(), "failed to set up ipc"); let res = unsafe { unistd::fork() }; match try_with!(res, "failed to fork") { ForkResult::Parent { child } => parent::run(child, &parent_sock, cntrfs), ForkResult::Child => { let child_opts = child::ChildOptions { command: opts.command.clone(), arguments: opts.arguments.clone(), mount_ready_sock: &child_sock, uid: container_uid, gid: container_gid, fs: cntrfs, process_status, home, }; child::run(&child_opts) } } } cntr-1.5.3/src/attach/parent.rs000064400000000000000000000035631046102023000145010ustar 00000000000000use nix::sys::signal::{self, Signal}; use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; use nix::unistd::Pid; use nix::{cmsg_space, unistd}; use simple_error::try_with; use std::os::unix::io::RawFd; use std::process; use crate::fs; use crate::ipc; use crate::mountns; use crate::pty; use crate::result::Result; pub fn run(pid: Pid, mount_ready_sock: &ipc::Socket, fs: fs::CntrFs) -> Result<()> { let ns = try_with!( mountns::MountNamespace::receive(mount_ready_sock), "failed to receive mount namespace from child" ); let sessions = fs.spawn_sessions(); let mut cmsgspace = cmsg_space!([RawFd; 1]); let (_, mut fds) = try_with!( mount_ready_sock.receive(1, &mut cmsgspace), "failed to receive pty file descriptor" ); assert!(fds.len() == 1); let fd = fds.pop().unwrap(); ns.cleanup(); loop { try_with!( pty::forward(&fd), "failed to forward terminal output of command" ); match waitpid(pid, Some(WaitPidFlag::WUNTRACED)) { Ok(WaitStatus::Signaled(child, Signal::SIGSTOP, _)) => { let _ = signal::kill(unistd::getpid(), Signal::SIGSTOP); let _ = signal::kill(child, Signal::SIGCONT); } Ok(WaitStatus::Signaled(_, signal, _)) => { try_with!( signal::kill(unistd::getpid(), signal), "failed to send signal {:?} to our own process", signal ); } Ok(WaitStatus::Exited(_, status)) => { process::exit(status); } Ok(what) => { panic!("unexpected wait event happened {:?}", what); } Err(e) => { drop(sessions); return try_with!(Err(e), "waitpid failed"); } }; } } cntr-1.5.3/src/bin/main.rs000064400000000000000000000141241046102023000134330ustar 00000000000000extern crate cntr; extern crate nix; use clap::builder::PossibleValue; use clap::{crate_authors, crate_version, Arg, ArgAction, ArgMatches, Command, ValueEnum}; use nix::unistd::User; use std::path::Path; use std::{env, process}; fn command_arg(index: usize) -> Arg { Arg::new("command") .help("Command and its arguments to execute after attach. Consider prepending it with '-- ' to prevent parsing of '-x'-like flags. [default: $SHELL]") .requires("command") .index(index) .action(ArgAction::Append) } fn parse_command_arg(args: &ArgMatches) -> (Option, Vec) { match args.get_many("command") { Some(args) => { let mut values: Vec = args.map(String::to_string).collect(); let command = values.remove(0); let command = match command.is_empty() { true => None, // indicates $SHELL default case false => Some(command), }; let arguments = values; (command, arguments) } None => (None, vec![]), // indicates $SHELL default case } } #[derive(clap::ValueEnum, Debug, Clone, Copy)] #[allow(non_camel_case_types)] pub enum ContainerType { process_id, rkt, podman, docker, nspawn, lxc, lxd, containerd, command, } impl ContainerType { pub fn possible_values() -> impl Iterator { ContainerType::value_variants() .iter() .filter_map(ValueEnum::to_possible_value) } } impl std::str::FromStr for ContainerType { type Err = String; fn from_str(s: &str) -> Result { for variant in Self::value_variants() { if variant.to_possible_value().unwrap().matches(s, false) { return Ok(*variant); } } Err(format!("Invalid variant: {}", s)) } } fn attach(args: &ArgMatches) { let (command, arguments) = parse_command_arg(args); let container_name = args.get_one::("id").unwrap().to_string(); // safe, because container id is .required let container_types = match args.get_many("type") { Some(args) => args .into_iter() .filter_map(|t: &ContainerType| cntr::lookup_container_type(&format!("{:?}", t))) .collect(), None => vec![], }; let mut options = cntr::AttachOptions { command, arguments, effective_user: None, container_types, container_name, }; if let Some(effective_username) = args.get_one::<&str>("effective-user") { match User::from_name(effective_username) { Ok(Some(passwd)) => { options.effective_user = Some(passwd); } Ok(None) => { eprintln!("no user with username '{}' found", effective_username); process::exit(1); } Err(e) => { eprintln!( "failed to to lookup user '{}' found: {}", effective_username, e ); process::exit(1); } }; }; if let Err(err) = cntr::attach(&options) { eprintln!("{}", err); process::exit(1); }; } fn exec(args: &ArgMatches, setcap: bool) { let (command, arguments) = parse_command_arg(args); if let Err(err) = cntr::exec(command, arguments, setcap) { eprintln!("{}", err); process::exit(1); } } fn main() { let attach_command = Command::new("attach") .about("Enter container") .version(crate_version!()) .author(crate_authors!("\n")) .arg_required_else_help(true) .disable_version_flag(true) .arg( Arg::new("effective-user") .long("effective-user") .action(ArgAction::Set) .value_parser(clap::builder::NonEmptyStringValueParser::new()) .value_name("EFFECTIVE_USER") .help("effective username that should be owner of new created files on the host"), ) .arg( Arg::new("type") .short('t') .long("type") .use_value_delimiter(true) .action(ArgAction::Append) .value_parser(clap::value_parser!(ContainerType)) .value_name("TYPE") .help("Container types to try (sperated by ','). [default: all but command]"), ) .arg( Arg::new("id") .help("container id, container name or process id") .required(true) .action(ArgAction::Set) .index(1), ) .arg(command_arg(2)); let exec_command = Command::new("exec") .about("Execute command in container filesystem") .version(crate_version!()) .author(crate_authors!("\n")) .arg(command_arg(1)) .arg_required_else_help(true); let main_app = Command::new("Cntr") .about("Enter or executed in container") .version(crate_version!()) .author(crate_authors!("\n")) .subcommand_required(true) .arg_required_else_help(true) .allow_external_subcommands(false) .subcommand(attach_command) .subcommand(exec_command.clone()); // find and run subcommand/app match std::env::current_exe() { Ok(exe) => { if exe == Path::new(cntr::SETCAP_EXE) { let matches = exec_command.get_matches(); exec(&matches, true); } else { let matches = main_app.get_matches(); match matches.subcommand() { Some(("exec", exec_matches)) => exec(exec_matches, false), Some(("attach", attach_matches)) => attach(attach_matches), Some((_, attach_matches)) => attach(attach_matches), None => unreachable!(), // because of AppSettings::SubCommandRequired }; } } Err(e) => { eprintln!("failed to resolve executable: {}", e); process::exit(1); } } } cntr-1.5.3/src/capabilities.rs000064400000000000000000000072751046102023000144010ustar 00000000000000use libc::{self, c_int, c_ulong}; use nix::errno::Errno; use simple_error::try_with; use std::fs::File; use std::io::Read; use std::mem; use std::mem::MaybeUninit; use std::path::Path; use std::ptr; use std::slice; use crate::procfs; use crate::result::Result; use crate::sys_ext::{prctl, setxattr}; pub const _LINUX_CAPABILITY_VERSION_1: u32 = 0x1998_0330; pub const _LINUX_CAPABILITY_VERSION_2: u32 = 0x2007_1026; pub const _LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522; pub const VFS_CAP_REVISION_1: u32 = 0x0100_0000; pub const VFS_CAP_REVISION_2: u32 = 0x0200_0000; pub const VFS_CAP_REVISION_MASK: u32 = 0xFF00_0000; pub const VFS_CAP_FLAGS_EFFECTIVE: u32 = 0x00_0001; pub const CAP_SYS_CHROOT: u32 = 18; pub const CAP_SYS_PTRACE: u32 = 19; #[repr(C)] struct cap_user_header_t { version: u32, pid: c_int, } #[repr(C)] struct cap_user_data_t { effective: u32, permitted: u32, inheritable: u32, } #[repr(C)] struct _vfs_cap_data { permitted: u32, inheritable: u32, } #[repr(C)] struct vfs_cap_data { magic_etc: u32, data: [_vfs_cap_data; 2], effective: [u32; 2], version: i8, } pub fn has_chroot() -> Result { let status = try_with!( procfs::status(nix::unistd::getpid()), "Failed to get capabilities" ); Ok(status.effective_capabilities & (1 << CAP_SYS_CHROOT) > 0) } pub fn set_chroot_capability(path: &Path) -> Result<()> { let header: MaybeUninit = mem::MaybeUninit::uninit(); let res = unsafe { libc::syscall( libc::SYS_capget, &header, ptr::null() as *const cap_user_data_t, ) }; let header: cap_user_header_t = unsafe { header.assume_init() }; try_with!(Errno::result(res), "Failed to get capability version"); let (magic, size) = match u32::from_le(header.version) | VFS_CAP_REVISION_MASK { _LINUX_CAPABILITY_VERSION_1 => (VFS_CAP_REVISION_1, 4 * (1 + 2)), // at the moment _LINUX_CAPABILITY_VERSION_2|_LINUX_CAPABILITY_VERSION_3 _ => (VFS_CAP_REVISION_2, 4 * (1 + 2 * 2)), }; let data = vfs_cap_data { magic_etc: u32::to_le(magic | VFS_CAP_FLAGS_EFFECTIVE), data: [ (_vfs_cap_data { permitted: 1 << CAP_SYS_CHROOT, inheritable: 0, }), (_vfs_cap_data { permitted: 0, inheritable: 0, }), ], effective: [1 << CAP_SYS_CHROOT, 0], version: 0, }; let datap: *const vfs_cap_data = &data; let bytep: *const u8 = datap as *const _; let bytes: &[u8] = unsafe { slice::from_raw_parts(bytep, size) }; try_with!( setxattr(path, "security.capability", bytes, 0), "setxattr failed" ); Ok(()) } fn last_capability() -> Result { let path = "/proc/sys/kernel/cap_last_cap"; let mut f = try_with!(File::open(path), "failed to open {}", path); let mut contents = String::new(); try_with!(f.read_to_string(&mut contents), "failed to read {}", path); contents.pop(); // remove newline Ok(try_with!( contents.parse::(), "failed to parse capability, got: '{}'", contents )) } pub fn drop(inheritable_capabilities: c_ulong) -> Result<()> { // we need chroot at the moment for `exec` command let inheritable = inheritable_capabilities | 1 << CAP_SYS_CHROOT | 1 << CAP_SYS_PTRACE; let last_capability = try_with!(last_capability(), "failed to read capability limit"); for cap in 0..last_capability { if (inheritable & (1 << cap)) == 0 { // TODO: do not ignore result let _ = prctl(libc::PR_CAPBSET_DROP, cap, 0, 0, 0); } } Ok(()) } cntr-1.5.3/src/cgroup.rs000064400000000000000000000074151046102023000132430ustar 00000000000000use log::warn; use nix::unistd; use simple_error::try_with; use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::io::{BufRead, BufReader}; use std::path::PathBuf; use crate::procfs; use crate::result::Result; fn get_subsystems() -> Result> { let path = "/proc/cgroups"; let f = try_with!(File::open(path), "failed to open /proc/cgroups"); let reader = BufReader::new(f); let mut subsystems: Vec = Vec::new(); for l in reader.lines() { let line = try_with!(l, "failed to read /proc/cgroups"); if line.starts_with('#') { continue; } let fields: Vec<&str> = line.split('\t').collect(); if fields.len() >= 4 && fields[3] != "0" { subsystems.push(fields[0].to_string()); } } Ok(subsystems) } fn get_mounts() -> Result> { let subsystems = try_with!(get_subsystems(), "failed to obtain cgroup subsystems"); let path = "/proc/self/mountinfo"; // example: // // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) let f = try_with!(File::open(path), "failed to read /proc/self/mountinfo"); let reader = BufReader::new(f); let mut mountpoints: HashMap = HashMap::new(); for l in reader.lines() { let line = try_with!(l, "failed to read '{}'", path); let fields: Vec<&str> = line.split(' ').collect(); if fields.len() < 11 || fields[9] != "cgroup" { continue; } for option in fields[10].split(',') { let name = option.strip_prefix("name=").unwrap_or(option).to_string(); if !subsystems.contains(&name) { mountpoints.insert(name, fields[4].to_string()); } } } Ok(mountpoints) } fn get_cgroups(pid: unistd::Pid) -> Result> { let path = procfs::get_path().join(format!("{}/cgroup", pid)); let f = try_with!(File::open(&path), "failed to read {}", path.display()); let reader = BufReader::new(f); let mut cgroups: Vec = Vec::new(); for l in reader.lines() { let line = try_with!(l, "failed to read '{}'", path.display()); let fields: Vec<&str> = line.split(":/").collect(); if fields.len() >= 2 { cgroups.push(fields[1].to_string()); } } Ok(cgroups) } fn cgroup_path(cgroup: &str, mountpoints: &HashMap) -> Option { for c in cgroup.split(',') { let m = mountpoints.get(c); if let Some(path) = m { let mut tasks_path = PathBuf::from(path); tasks_path.push(cgroup); tasks_path.push("tasks"); return Some(tasks_path); } } None } // TODO add implementation for unified cgroups, cgmanager, lxcfs // -> on the long run everything will be done with unified cgroups hopefully pub fn move_to(pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> { let cgroups = try_with!( get_cgroups(target_pid), "failed to get cgroups of {}", target_pid ); let mountpoints = try_with!(get_mounts(), "failed to get cgroup mountpoints"); for cgroup in cgroups { let p = cgroup_path(&cgroup, &mountpoints); if let Some(path) = p { match File::create(&path) { Ok(mut buffer) => { try_with!( write!(buffer, "{}", pid), "failed to enter {} cgroup", cgroup ); } Err(err) => { warn!("failed to enter {} namespace: {}", cgroup, err); } } } } Ok(()) } cntr-1.5.3/src/cmd.rs000064400000000000000000000067421046102023000125110ustar 00000000000000use log::warn; use nix::{self, unistd}; use simple_error::try_with; use std::collections::HashMap; use std::env; use std::ffi::OsString; use std::fs::File; use std::io; use std::io::{BufRead, BufReader}; use std::os::unix::ffi::OsStringExt; use std::os::unix::process::CommandExt; use std::path::PathBuf; use std::process::{Command, ExitStatus}; use crate::procfs; use crate::result::Result; pub struct Cmd { environment: HashMap, command: String, arguments: Vec, home: Option, } fn read_environment(pid: unistd::Pid) -> Result> { let path = procfs::get_path().join(pid.to_string()).join("environ"); let f = try_with!(File::open(&path), "failed to open {}", path.display()); let reader = BufReader::new(f); let res: HashMap = reader .split(b'\0') .filter_map(|var| { let var = match var { Ok(var) => var, Err(_) => return None, }; let tuple: Vec<&[u8]> = var.splitn(2, |b| *b == b'=').collect(); if tuple.len() != 2 { return None; } Some(( OsString::from_vec(Vec::from(tuple[0])), OsString::from_vec(Vec::from(tuple[1])), )) }) .collect(); Ok(res) } impl Cmd { pub fn new( command: Option, args: Vec, pid: unistd::Pid, home: Option, ) -> Result { let arguments = if command.is_none() { vec![String::from("-l")] } else { args }; let command = command.unwrap_or_else(|| env::var("SHELL").unwrap_or_else(|_| String::from("sh"))); let variables = try_with!( read_environment(pid), "could not inherit environment variables of container" ); Ok(Cmd { command, arguments, environment: variables, home, }) } pub fn run(mut self) -> Result { let default_path = OsString::from("/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"); self.environment.insert( OsString::from("PATH"), env::var_os("PATH").unwrap_or(default_path), ); if let Some(path) = self.home { self.environment .insert(OsString::from("HOME"), path.into_os_string()); } let cmd = Command::new(self.command) .args(self.arguments) .envs(self.environment) .status(); Ok(try_with!(cmd, "failed to run `sh -l`")) } pub fn exec_chroot(self) -> Result<()> { let err = unsafe { Command::new(&self.command) .args(self.arguments) .envs(self.environment) .pre_exec(|| { if let Err(e) = unistd::chroot("/var/lib/cntr") { warn!("failed to chroot to /var/lib/cntr: {}", e); return Err(io::Error::from_raw_os_error(e as i32)); } if let Err(e) = env::set_current_dir("/") { warn!("failed to change directory to /"); return Err(e); } Ok(()) }) .exec() }; try_with!(Err(err), "failed to execute `{}`", self.command); Ok(()) } } cntr-1.5.3/src/dirent.rs000064400000000000000000000056241046102023000132310ustar 00000000000000//! Directory Stream functions //! //! [Further reading and details on the C API](http://man7.org/linux/man-pages/man3/opendir.3.html) use libc::{c_long, DIR}; use nix::errno::Errno; use std::convert::AsRef; use std::mem; #[cfg(any(target_os = "linux", target_os = "android"))] use libc::{dirent64, readdir64}; #[cfg(not(any(target_os = "linux", target_os = "android")))] use libc::{dirent as dirent64, readdir as readdir64}; #[cfg(not(any(target_os = "ios", target_os = "macos")))] use std::os::unix::io::RawFd; /// Directory Stream object #[allow(missing_debug_implementations)] pub struct DirectoryStream(*mut DIR); impl AsRef for DirectoryStream { fn as_ref(&self) -> &DIR { unsafe { &*self.0 } } } /// Consumes directory stream and return underlying directory pointer. /// /// The pointer must be deallocated manually using `libc::closedir` impl From for *mut DIR { fn from(e: DirectoryStream) -> Self { let dirp = e.0; mem::forget(e); dirp } } impl Drop for DirectoryStream { fn drop(&mut self) { unsafe { libc::closedir(self.0) }; } } /// A directory entry #[allow(missing_debug_implementations)] pub struct DirectoryEntry<'a>(&'a dirent64); impl<'a> AsRef for DirectoryEntry<'a> { fn as_ref(&self) -> &dirent64 { self.0 } } /// Opens a directory stream corresponding to the directory name. /// /// The stream is positioned at the first entry in the directory. pub fn opendir(name: &P) -> nix::Result { let dirp = name.with_nix_path(|cstr| unsafe { libc::opendir(cstr.as_ptr()) })?; if dirp.is_null() { Err(nix::Error::last()) } else { Ok(DirectoryStream(dirp)) } } /// Returns the next directory entry in the directory stream. /// /// It returns `Some(None)` on reaching the end of the directory stream. pub fn readdir(dir: &mut DirectoryStream) -> nix::Result> { let dirent = unsafe { Errno::clear(); readdir64(dir.0) }; if dirent.is_null() { match Errno::last() { Errno::UnknownErrno => Ok(None), _ => Err(nix::Error::last()), } } else { Ok(Some(DirectoryEntry(unsafe { &*dirent }))) } } /// Sets the location in the directory stream from which the next `readdir` call will start. /// /// The `loc` argument should be a value returned by a previous call to `telldir` #[cfg(not(any(target_os = "android")))] pub fn seekdir(dir: &mut DirectoryStream, loc: c_long) { unsafe { libc::seekdir(dir.0, loc) }; } /// Returns the current location associated with the directory stream. #[cfg(not(any(target_os = "android")))] pub fn telldir(dir: &mut DirectoryStream) -> c_long { unsafe { libc::telldir(dir.0) } } pub fn dirfd(dir: &mut DirectoryStream) -> nix::Result { let res = unsafe { libc::dirfd(dir.0) }; Errno::result(res) } cntr-1.5.3/src/dotcntr.rs000064400000000000000000000045631046102023000134220ustar 00000000000000use libc::pid_t; use nix::fcntl::{self, OFlag}; use nix::sys::stat; use nix::unistd::Pid; use simple_error::try_with; use std::fs::{self, File}; use std::io::prelude::*; use std::os::unix::prelude::*; use std::{ fs::{set_permissions, Permissions}, os::unix::fs::PermissionsExt, }; use crate::capabilities; use crate::procfs::ProcStatus; use crate::result::Result; use crate::tmp; /// Hidden directory with CAP_CHROOT enabled cntr-exec binary pub struct DotcntrDir { pub file: File, pub dir: tmp::TempDir, } impl DotcntrDir { pub fn write_pid_file(&self, target_pid: Pid) -> Result<()> { let path = self.dir.path().join("pid"); let mut file = try_with!(File::create(&path), "failed to create {}", path.display()); let raw_pid: pid_t = target_pid.into(); try_with!( file.write_all(format!("{}", raw_pid).as_bytes()), "failed to write {}", path.display() ); Ok(()) } pub fn write_setcap_exe(&self) -> Result<()> { let path = self.dir.path().join("cntr-exec"); try_with!( fs::copy("/proc/self/exe", &path), "failed to copy /proc/self/exe to {}", path.display() ); try_with!( capabilities::set_chroot_capability(&path), "Failed set file capability CAP_SYS_CHROOT on {}", path.display() ); Ok(()) } } pub fn create(process_status: &ProcStatus) -> Result { let dotcntr_dir = try_with!(tmp::tempdir(), "failed to create temporary directory"); let permissions = Permissions::from_mode(0o755); try_with!( set_permissions(dotcntr_dir.path(), permissions), "cannot change permissions of '{}'", dotcntr_dir.path().display() ); let dotcntr_fd = try_with!( fcntl::open( dotcntr_dir.path(), OFlag::O_RDONLY | OFlag::O_CLOEXEC, stat::Mode::all(), ), "failed to open '{}' directory", dotcntr_dir.path().display() ); let dotcntr_file = unsafe { File::from_raw_fd(dotcntr_fd) }; let d = DotcntrDir { file: dotcntr_file, dir: dotcntr_dir, }; try_with!(d.write_setcap_exe(), "failed to create setcap executable"); try_with!( d.write_pid_file(process_status.local_pid), "failed to create pid file" ); Ok(d) } cntr-1.5.3/src/exec.rs000064400000000000000000000027711046102023000126700ustar 00000000000000use libc::pid_t; use nix::unistd::Pid; use simple_error::try_with; use std::fs::File; use std::io::prelude::*; use std::os::unix::process::CommandExt; use std::process::Command; use crate::capabilities; use crate::cmd::Cmd; use crate::result::Result; pub const SETCAP_EXE: &str = "/.cntr/cntr-exec"; pub const EXEC_PID_FILE: &str = "/.cntr/pid"; pub fn exec(exe: Option, mut args: Vec, has_setcap: bool) -> Result<()> { if !has_setcap { let has_chroot = try_with!( capabilities::has_chroot(), "failed to check if process has chroot capability" ); if !has_chroot { if let Some(e) = exe { args.insert(0, e); } try_with!( Err(Command::new(SETCAP_EXE).args(args).exec()), "failed to start capability wrapper" ); // BUG! return Ok(()); } } let mut f = try_with!( File::open(EXEC_PID_FILE), "failed to open {}", EXEC_PID_FILE ); let mut pid_string = String::new(); try_with!( f.read_to_string(&mut pid_string), "failed to read {}", EXEC_PID_FILE ); let pid = try_with!( pid_string.parse::(), "failed to parse pid {} in pid file {}", pid_string, EXEC_PID_FILE ); let cmd = Cmd::new(exe, args, Pid::from_raw(pid), None)?; try_with!(cmd.exec_chroot(), "failed to execute command in container"); Ok(()) } cntr-1.5.3/src/files.rs000064400000000000000000000022621046102023000130410ustar 00000000000000use nix::fcntl::OFlag; use std::fs::create_dir_all; use std::fs::File; use std::io; use std::os::unix::prelude::*; use std::path::Path; #[derive(PartialOrd, Eq, PartialEq)] pub enum FdState { None, Readable, ReadWritable, } pub fn fd_path(fd: &Fd) -> String { format!("/proc/self/fd/{}", fd.raw()) } pub fn mkdir_p>(path: &P) -> io::Result<()> { if let Err(e) = create_dir_all(path) { if e.kind() != io::ErrorKind::AlreadyExists { return Err(e); } } Ok(()) } impl From for FdState { fn from(flags: OFlag) -> FdState { if flags & OFlag::O_RDWR == OFlag::O_RDWR { FdState::ReadWritable } else if flags & OFlag::O_RDONLY == OFlag::O_RDONLY { FdState::Readable } else { FdState::None } } } pub struct Fd { pub file: File, pub state: FdState, } impl Fd { pub fn new(fd: RawFd, state: FdState) -> Fd { Fd { file: unsafe { File::from_raw_fd(fd) }, state, } } pub fn raw(&self) -> RawFd { self.file.as_raw_fd() } pub fn path(&self) -> String { fd_path(self) } } cntr-1.5.3/src/fs.rs000064400000000000000000001160631046102023000123540ustar 00000000000000use chashmap::CHashMap; use cntr_fuse::{ self, FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyEmpty, ReplyEntry, ReplyIoctl, ReplyLseek, ReplyOpen, ReplyRead, ReplyStatfs, ReplyWrite, ReplyXattr, Request, }; use libc::{self, c_long, c_ulong, dev_t, off_t}; use log::debug; use nix::errno::Errno; use nix::fcntl::{self, AtFlags, OFlag}; use nix::sys::stat; use nix::sys::stat::SFlag; use nix::sys::time::{TimeSpec as NixTimeSpec, TimeValLike}; use nix::sys::uio::{pread, pwrite}; use nix::unistd::{Gid, Uid}; use nix::{self, unistd}; use parking_lot::{Mutex, RwLock}; use simple_error::try_with; use std::cmp; use std::collections::HashMap; use std::ffi::{CStr, OsStr}; use std::fs::File; use std::io; use std::mem; use std::os::unix::prelude::*; use std::path::Path; use std::sync::Arc; use std::thread::{self, JoinHandle}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::vec::Vec; use std::{u32, u64}; use crate::dirent; use crate::dotcntr::DotcntrDir; use crate::files::{fd_path, Fd, FdState}; use crate::fsuid; use crate::fusefd; use crate::inode::Inode; use crate::num_cpus; use crate::result::Result; use crate::sys_ext::{ fchownat, fstatvfs, fuse_getxattr, fuse_listxattr, fuse_readlinkat, fuse_removexattr, fuse_setxattr, futimens, ioctl, ioctl_read, ioctl_write, linkat, mknodat, renameat2, setrlimit, utimensat, Rlimit, UtimeSpec, }; use crate::user_namespace::IdMap; const FH_MAGIC: char = 'F'; const DIRP_MAGIC: char = 'D'; pub const POSIX_ACL_DEFAULT_XATTR: &str = "system.posix_acl_default"; #[derive(Hash, Eq, PartialEq, Clone, Debug)] struct InodeKey { ino: u64, dev: u64, } struct DirP { magic: char, dp: dirent::DirectoryStream, offset: c_long, entry: Option, } struct Fh { magic: char, fd: Fd, } impl Fh { fn new(fd: Fd) -> Box { Box::new(Fh { magic: FH_MAGIC, fd, }) } } struct InodeCounter { next_number: u64, generation: u64, } pub struct CntrFs { prefix: String, root_inode: Arc, dotcntr: Arc>, inode_mapping: Arc>>, inodes: Arc>>, inode_counter: Arc>, effective_uid: Option, effective_gid: Option, fuse_fd: RawFd, uid_map: IdMap, gid_map: IdMap, } enum ReplyDirectory { Directory(cntr_fuse::ReplyDirectory), DirectoryPlus(cntr_fuse::ReplyDirectoryPlus), } impl ReplyDirectory { pub fn ok(self) { match self { ReplyDirectory::Directory(r) => r.ok(), ReplyDirectory::DirectoryPlus(r) => r.ok(), } } pub fn error(self, err: libc::c_int) { match self { ReplyDirectory::Directory(r) => r.error(err), ReplyDirectory::DirectoryPlus(r) => r.error(err), } } } const TTL: Duration = Duration::from_secs(1); macro_rules! tryfuse { ($result:expr, $reply:expr) => { match $result { Ok(val) => val, Err(err) => { debug!("return error {} on {}:{}", err, file!(), line!()); return $reply.error(err as i32); } } }; } // TODO: evaluate if this option increases performance fn posix_fadvise(fd: RawFd) -> nix::Result<()> { let res = unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_NOREUSE) }; Errno::result(res).map(drop) } pub struct CntrMountOptions<'a> { pub prefix: &'a str, pub uid_map: IdMap, pub gid_map: IdMap, pub effective_uid: Option, pub effective_gid: Option, } pub enum LookupFile<'a> { Donate(File), Borrow(&'a File), } impl<'a> AsRawFd for LookupFile<'a> { fn as_raw_fd(&self) -> RawFd { match *self { LookupFile::Donate(ref f) => f.as_raw_fd(), LookupFile::Borrow(f) => f.as_raw_fd(), } } } impl<'a> LookupFile<'a> { fn into_raw_fd(self) -> nix::Result { match self { LookupFile::Donate(f) => Ok(f.into_raw_fd()), LookupFile::Borrow(f) => unistd::dup(f.as_raw_fd()), } } } fn open_static_dnode(static_ino: u64, path: &Path) -> Result> { let fd = try_with!( fcntl::open(path, OFlag::O_RDONLY | OFlag::O_CLOEXEC, stat::Mode::all()), "failed to open backing filesystem '{}'", path.display() ); Ok(Arc::new(Inode { fd: RwLock::new(Fd::new(fd, FdState::Readable)), kind: FileType::Directory, ino: static_ino, dev: static_ino, nlookup: RwLock::new(2), has_default_acl: RwLock::new(None), })) } impl CntrFs { pub fn new(options: &CntrMountOptions, dotcntr: Option) -> Result { let fuse_fd = try_with!(fusefd::open(), "failed to initialize fuse"); let limit = Rlimit { rlim_cur: 1_048_576, rlim_max: 1_048_576, }; try_with!( setrlimit(libc::RLIMIT_NOFILE, &limit), "Cannot raise file descriptor limit" ); Ok(CntrFs { prefix: String::from(options.prefix), root_inode: open_static_dnode(cntr_fuse::FUSE_ROOT_ID, Path::new(options.prefix))?, dotcntr: Arc::new(dotcntr), inode_mapping: Arc::new(Mutex::new(HashMap::::new())), inodes: Arc::new(CHashMap::>::new()), inode_counter: Arc::new(RwLock::new(InodeCounter { next_number: 3, generation: 0, })), uid_map: options.uid_map, gid_map: options.gid_map, fuse_fd: fuse_fd.into_raw_fd(), effective_uid: options.effective_uid, effective_gid: options.effective_gid, }) } pub fn uid_map(&self) -> IdMap { self.uid_map } pub fn gid_map(&self) -> IdMap { self.gid_map } fn create_file( &self, req: &Request, parent: u64, name: &OsStr, mut mode: u32, umask: u32, flags: u32, ) -> nix::Result { let parent_inode = self.inode(parent)?; let has_default_acl = parent_inode.check_default_acl()?; let parent_fd = parent_inode.fd.read(); self.set_user_group(req); let oflag = fcntl::OFlag::from_bits_truncate(flags as i32); if !has_default_acl { mode &= !umask; } let create_mode = stat::Mode::from_bits_truncate(mode); let fd = fcntl::openat( parent_fd.raw(), name, oflag | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC, create_mode, )?; Ok(fd) } pub fn spawn_sessions(self) -> Result>>> { let mut sessions = Vec::new(); // numbers of sessions is optimized for cached read let num_sessions = cmp::max(num_cpus::get() / 2, 1) as usize; for _ in 0..num_sessions { debug!("spawn worker"); let cntrfs = CntrFs { prefix: self.prefix.clone(), root_inode: Arc::clone(&self.root_inode), dotcntr: Arc::clone(&self.dotcntr), fuse_fd: self.fuse_fd, inode_mapping: Arc::clone(&self.inode_mapping), inodes: Arc::clone(&self.inodes), inode_counter: Arc::clone(&self.inode_counter), uid_map: self.uid_map, gid_map: self.gid_map, effective_uid: self.effective_uid, effective_gid: self.effective_gid, }; let max_background = num_sessions as u16; let res = cntr_fuse::Session::new_from_fd( cntrfs, self.fuse_fd, Path::new(""), max_background, max_background, ); let session = try_with!(res, "failed to inherit fuse session"); let guard = thread::spawn(move || { let mut se = session; se.run() }); sessions.push(guard); } Ok(sessions) } pub fn mount(&self, mountpoint: &Path, selinux_context: &Option) -> Result<()> { let context = if let Some(ref context) = selinux_context { format!("context=\"{}\"", context) } else { "".to_owned() }; let mount_flags = format!( "fd={},rootmode=40000,user_id=0,group_id=0,allow_other,default_permissions,{}", self.fuse_fd, context ); try_with!( nix::mount::mount( Some(self.prefix.as_str()), mountpoint, Some("fuse.cntr"), nix::mount::MsFlags::empty(), Some(mount_flags.as_str()), ), "failed to mount fuse" ); Ok(()) } #[allow(clippy::too_many_arguments)] fn setattr_inner( &mut self, ino: u64, fd: &Fd, mode: Option, uid: Option, gid: Option, size: Option, atime: cntr_fuse::UtimeSpec, mtime: cntr_fuse::UtimeSpec, ) -> nix::Result<()> { if let Some(bits) = mode { let mode = stat::Mode::from_bits_truncate(bits); stat::fchmod(fd.raw(), mode)?; } if uid.is_some() || gid.is_some() { let _uid = uid.map(|u| Uid::from_raw(self.uid_map.map_id_up(u))); let _gid = gid.map(|g| Gid::from_raw(self.gid_map.map_id_up(g))); fchownat(fd.raw(), "", _uid, _gid, AtFlags::AT_EMPTY_PATH)?; } if let Some(s) = size { unistd::ftruncate(fd.raw(), s as off_t)?; } if mtime != cntr_fuse::UtimeSpec::Omit || atime != cntr_fuse::UtimeSpec::Omit { let inode = self.inode(ino)?; set_time(&inode, fd, &mtime, &atime)?; } Ok(()) } fn generic_readdir(&mut self, ino: u64, fh: u64, offset: u64, mut reply: ReplyDirectory) { fsuid::set_root(); let dirp = unsafe { &mut (*(fh as *mut DirP)) }; assert!(dirp.magic == DIRP_MAGIC); if (offset as c_long) != dirp.offset { dirent::seekdir(&mut dirp.dp, offset as c_long); dirp.entry = None; dirp.offset = 0; } while { if dirp.entry.is_none() { dirp.entry = tryfuse!(dirent::readdir(&mut dirp.dp), reply).map(|v| *v.as_ref()); } match dirp.entry { None => false, Some(entry) => { dirp.offset = dirent::telldir(&mut dirp.dp); let name = unsafe { CStr::from_ptr(entry.d_name.as_ptr()) }; dirp.entry = None; match reply { ReplyDirectory::Directory(ref mut r) => r.add( entry.d_ino, dirp.offset as i64, dtype_kind(entry.d_type), OsStr::from_bytes(name.to_bytes()), ), ReplyDirectory::DirectoryPlus(ref mut r) => { match self.lookup_inode(ino, OsStr::from_bytes(name.to_bytes())) { Ok((attr, generation)) => r.add( entry.d_ino, dirp.offset as i64, OsStr::from_bytes(name.to_bytes()), &TTL, &attr, generation, ), _ => true, } } } } } } {} reply.ok() } pub fn set_user_group(&self, req: &Request) { let real_uid = self.uid_map.map_id_up(req.uid()); let uid = self.effective_uid.map_or(real_uid, |u| u.into()); let real_gid = self.gid_map.map_id_up(req.gid()); let gid = self.effective_gid.map_or(real_gid, |g| g.into()); fsuid::set_user_group(uid, gid); } fn attr_from_stat(&self, attr: stat::FileStat) -> FileAttr { let ctime = UNIX_EPOCH + Duration::new(attr.st_ctime as u64, attr.st_ctime_nsec as u32); FileAttr { ino: attr.st_ino as u64, // replaced by ino pointer size: attr.st_size as u64, blocks: attr.st_blocks as u64, atime: UNIX_EPOCH + Duration::new(attr.st_atime as u64, attr.st_atime_nsec as u32), mtime: UNIX_EPOCH + Duration::new(attr.st_mtime as u64, attr.st_mtime_nsec as u32), ctime, crtime: ctime, uid: self.uid_map.map_id_down(attr.st_uid), gid: self.gid_map.map_id_down(attr.st_gid), perm: attr.st_mode as u16, kind: inode_kind(stat::SFlag::from_bits_truncate(attr.st_mode)), nlink: attr.st_nlink as u32, rdev: attr.st_rdev as u32, // Flags (OS X only, see chflags(2)) flags: 0, } } fn inode(&self, ino: u64) -> nix::Result> { assert!(ino > 0); if ino == cntr_fuse::FUSE_ROOT_ID { Ok(Arc::clone(&self.root_inode)) } else { match self.inodes.get(&ino) { Some(inode) => Ok(Arc::clone(&inode)), None => Err(Errno::ESTALE), } } } fn mutable_inode(&mut self, ino: u64) -> nix::Result> { let inode = self.inode(ino)?; inode.upgrade_fd(&FdState::Readable)?; Ok(inode) } fn next_inode_number(&self) -> (u64, u64) { let mut counter = self.inode_counter.write(); let next_number = counter.next_number; counter.next_number += 1; if next_number == 0 { counter.next_number = cntr_fuse::FUSE_ROOT_ID + 1; counter.generation += 1; } (next_number, counter.generation) } fn lookup_from_fd(&mut self, new_file: LookupFile) -> nix::Result<(FileAttr, u64)> { let _stat = stat::fstat(new_file.as_raw_fd())?; let mut attr = self.attr_from_stat(_stat); let key = InodeKey { ino: attr.ino, dev: _stat.st_dev, }; let mut inode_mapping = self.inode_mapping.lock(); if let Some(ino) = inode_mapping.get(&key) { if let Some(inode) = self.inodes.get_mut(ino) { *inode.nlookup.write() += 1; let counter = self.inode_counter.read(); attr.ino = *ino; return Ok((attr, counter.generation)); } else { panic!("BUG! could not find inode {} also its mapping exists.", ino); }; } let (next_number, generation) = self.next_inode_number(); let fd = RwLock::new(Fd::new( new_file.into_raw_fd()?, if attr.kind == FileType::Symlink || attr.kind == FileType::BlockDevice { // we cannot open a symlink read/writable FdState::Readable } else { FdState::None }, )); let inode = Arc::new(Inode { fd, kind: attr.kind, ino: attr.ino, dev: _stat.st_dev, nlookup: RwLock::new(1), has_default_acl: RwLock::new(None), }); assert!(self.inodes.insert(next_number, inode).is_none()); attr.ino = next_number; inode_mapping.insert(key, next_number); Ok((attr, generation)) } pub fn lookup_inode(&mut self, parent: u64, name: &OsStr) -> nix::Result<(FileAttr, u64)> { fsuid::set_root(); if parent == cntr_fuse::FUSE_ROOT_ID && name == ".cntr" { let dotcntr = Arc::clone(&self.dotcntr); if let Some(ref dotcntr) = *dotcntr { return self.lookup_from_fd(LookupFile::Borrow(&dotcntr.file)); } } let parent_inode = self.inode(parent)?; let parent_fd = parent_inode.fd.read(); let fd = fcntl::openat( parent_fd.raw(), name, OFlag::O_PATH | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC, stat::Mode::empty(), )?; let file = unsafe { File::from_raw_fd(fd) }; self.lookup_from_fd(LookupFile::Donate(file)) } } fn get_filehandle<'a>(fh: u64) -> &'a Fh { let handle = unsafe { &mut (*(fh as *mut Fh)) }; assert!(handle.magic == FH_MAGIC); handle } fn to_utimespec(time: &cntr_fuse::UtimeSpec) -> UtimeSpec { match *time { cntr_fuse::UtimeSpec::Omit => UtimeSpec::Omit, cntr_fuse::UtimeSpec::Now => UtimeSpec::Now, cntr_fuse::UtimeSpec::Time(time) => { let d = time.duration_since(SystemTime::UNIX_EPOCH).unwrap(); let t = NixTimeSpec::seconds(d.as_secs() as i64) + NixTimeSpec::nanoseconds(d.subsec_nanos() as i64); UtimeSpec::Time(t) } } } fn set_time( inode: &Inode, fd: &Fd, mtime: &cntr_fuse::UtimeSpec, atime: &cntr_fuse::UtimeSpec, ) -> nix::Result<()> { if inode.kind == FileType::Symlink { // FIXME: fs_perms 660 99 99 100 99 t 1 return NOPERM for // utime(file) as user 100:99 when file is owned by 99:99 let path = fd_path(fd); utimensat( libc::AT_FDCWD, Path::new(&path), &to_utimespec(mtime), &to_utimespec(atime), fcntl::AtFlags::empty(), )?; } else { futimens(fd.raw(), &to_utimespec(mtime), &to_utimespec(atime))?; } Ok(()) } fn dtype_kind(dtype: u8) -> FileType { match dtype { libc::DT_UNKNOWN => FileType::Unknown, libc::DT_BLK => FileType::BlockDevice, libc::DT_CHR => FileType::CharDevice, libc::DT_DIR => FileType::Directory, libc::DT_FIFO => FileType::NamedPipe, libc::DT_LNK => FileType::Symlink, libc::DT_SOCK => FileType::Socket, libc::DT_REG => FileType::RegularFile, _ => panic!( "BUG! got unknown d_entry type received from d_type: {}", dtype ), } } fn inode_kind(mode: SFlag) -> FileType { match mode { SFlag::S_IFBLK => FileType::BlockDevice, SFlag::S_IFCHR => FileType::CharDevice, SFlag::S_IFDIR => FileType::Directory, SFlag::S_IFIFO => FileType::NamedPipe, SFlag::S_IFLNK => FileType::Symlink, SFlag::S_IFREG => FileType::RegularFile, SFlag::S_IFSOCK => FileType::Socket, _ => panic!("Got unexpected File type with value: {}", mode.bits()), } } impl Filesystem for CntrFs { fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { fsuid::set_root(); let (attr, generation) = tryfuse!(self.lookup_inode(parent, name), reply); reply.entry(&TTL, &attr, generation); } fn forget(&mut self, _req: &Request, ino: u64, nlookup: u64) { fsuid::set_root(); let mut inode_mapping = self.inode_mapping.lock(); let key = match self.inodes.get_mut(&ino) { Some(ref mut inode) => { let mut old_nlookup = inode.nlookup.write(); assert!(*old_nlookup >= nlookup); *old_nlookup -= nlookup; if *old_nlookup != 0 { return; }; InodeKey { ino: inode.ino, dev: inode.dev, } } None => return, }; self.inodes.remove(&ino); inode_mapping.remove(&key); } fn destroy(&mut self, _req: &Request) { fsuid::set_root(); self.inodes.clear(); } fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { fsuid::set_root(); let inode = tryfuse!(self.inode(ino), reply); let fd = inode.fd.read(); let mut attr = self.attr_from_stat(tryfuse!(stat::fstat(fd.raw()), reply)); attr.ino = ino; reply.attr(&TTL, &attr); } fn setattr( &mut self, _req: &Request, ino: u64, mode: Option, uid: Option, gid: Option, size: Option, atime: cntr_fuse::UtimeSpec, mtime: cntr_fuse::UtimeSpec, fh: Option, _crtime: Option, // only mac os x _chgtime: Option, // only mac os x _bkuptime: Option, // only mac os x _flags: Option, // only mac os x reply: ReplyAttr, ) { fsuid::set_root(); { if let Some(pointer) = fh { let fd = &get_filehandle(pointer).fd; tryfuse!( self.setattr_inner(ino, fd, mode, uid, gid, size, atime, mtime), reply ); } else { let inode = tryfuse!(self.inode(ino), reply); let state = if size.is_some() { FdState::ReadWritable } else { FdState::Readable }; tryfuse!(inode.upgrade_fd(&state), reply); let fd = inode.fd.read(); tryfuse!( self.setattr_inner(ino, &fd, mode, uid, gid, size, atime, mtime), reply ); }; } self.getattr(_req, ino, reply) } fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) { fsuid::set_root(); let inode = tryfuse!(self.inode(ino), reply); let fd = inode.fd.read(); let target = tryfuse!(fuse_readlinkat(fd.raw()), reply); reply.data(&target.into_vec()); } fn mknod( &mut self, req: &Request, parent: u64, name: &OsStr, mut mode: u32, umask: u32, rdev: u32, reply: ReplyEntry, ) { { let inode = tryfuse!(self.inode(parent), reply); let has_default_acl = tryfuse!(inode.check_default_acl(), reply); if !has_default_acl { mode &= !umask; } self.set_user_group(req); let kind = stat::SFlag::from_bits_truncate(mode); let perm = stat::Mode::from_bits_truncate(mode); let fd = inode.fd.read(); tryfuse!( mknodat(&fd.raw(), name, kind, perm, dev_t::from(rdev)), reply ); } self.lookup(req, parent, name, reply); } fn mkdir( &mut self, req: &Request, parent: u64, name: &OsStr, mut mode: u32, umask: u32, reply: ReplyEntry, ) { { let inode = tryfuse!(self.inode(parent), reply); let has_default_acl = tryfuse!(inode.check_default_acl(), reply); if !has_default_acl { mode &= !umask; } self.set_user_group(req); let perm = stat::Mode::from_bits_truncate(mode); let fd = inode.fd.read(); tryfuse!(stat::mkdirat(fd.raw(), name, perm), reply); } self.lookup(req, parent, name, reply); } fn unlink(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { fsuid::set_root(); let inode = tryfuse!(self.inode(parent), reply); let fd = inode.fd.read(); let res = unistd::unlinkat(Some(fd.raw()), name, unistd::UnlinkatFlags::NoRemoveDir); tryfuse!(res, reply); reply.ok(); } fn rmdir(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) { fsuid::set_root(); let inode = tryfuse!(self.inode(parent), reply); let fd = inode.fd.read(); tryfuse!( unistd::unlinkat(Some(fd.raw()), name, unistd::UnlinkatFlags::RemoveDir), reply ); reply.ok(); } fn symlink( &mut self, req: &Request, parent: u64, name: &OsStr, link: &Path, reply: ReplyEntry, ) { self.set_user_group(req); { let inode = tryfuse!(self.inode(parent), reply); let fd = inode.fd.read(); let res = unistd::symlinkat(link, Some(fd.raw()), name); tryfuse!(res, reply); } self.lookup(req, parent, name, reply); } fn rename( &mut self, req: &Request, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr, reply: ReplyEmpty, ) { self.set_user_group(req); let parent_inode = tryfuse!(self.inode(parent), reply); let parent_fd = parent_inode.fd.read(); let new_inode = tryfuse!(self.inode(newparent), reply); let new_fd = new_inode.fd.read(); tryfuse!( fcntl::renameat(Some(parent_fd.raw()), name, Some(new_fd.raw()), newname), reply ); reply.ok(); } fn rename2( &mut self, req: &Request, parent: u64, name: &OsStr, newparent: u64, newname: &OsStr, flags: u32, reply: ReplyEmpty, ) { self.set_user_group(req); let parent_inode = tryfuse!(self.inode(parent), reply); let parent_fd = parent_inode.fd.read(); let new_inode = tryfuse!(self.inode(newparent), reply); let new_fd = new_inode.fd.read(); let res = renameat2(parent_fd.raw(), name, new_fd.raw(), newname, flags); tryfuse!(res, reply); reply.ok(); } fn link( &mut self, req: &Request, ino: u64, newparent: u64, newname: &OsStr, reply: ReplyEntry, ) { fsuid::set_root(); { let source_inode = tryfuse!(self.inode(ino), reply); let source_fd = source_inode.fd.read(); let newparent_inode = tryfuse!(self.inode(newparent), reply); let newparent_fd = newparent_inode.fd.read(); let res = linkat( source_fd.raw(), "", newparent_fd.raw(), newname, AtFlags::AT_EMPTY_PATH, ); tryfuse!(res, reply); } // just do a lookup for simplicity self.lookup(req, newparent, newname, reply); } fn open(&mut self, _req: &Request, ino: u64, flags: u32, reply: ReplyOpen) { fsuid::set_root(); let mut oflags = fcntl::OFlag::from_bits_truncate(flags as i32); let inode = tryfuse!(self.inode(ino), reply); let fd = inode.fd.read(); let path = fd_path(&fd); // ignore write only or append flags because we have writeback cache enabled // and the kernel will also read from file descriptors opened as read. oflags = (oflags & !OFlag::O_NOFOLLOW & !OFlag::O_APPEND) | OFlag::O_CLOEXEC; if oflags & OFlag::O_WRONLY == OFlag::O_WRONLY { oflags = (oflags & !OFlag::O_WRONLY) | OFlag::O_RDWR; } let res = tryfuse!( fcntl::open(Path::new(&path), oflags, stat::Mode::empty()), reply ); // avoid double caching tryfuse!(posix_fadvise(res), reply); let fh = Fh::new(Fd::new(res, FdState::from(oflags))); reply.opened( Box::into_raw(fh) as u64, cntr_fuse::consts::FOPEN_KEEP_CACHE, ); // freed by close } fn read( &mut self, _req: &Request, _ino: u64, fh: u64, offset: i64, size: u32, reply: ReplyRead, ) { fsuid::set_root(); let mut v = vec![0; size as usize]; let buf = v.as_mut_slice(); tryfuse!( pread(get_filehandle(fh).fd.raw(), buf, offset as off_t), reply ); reply.data(buf); } fn write( &mut self, _req: &Request, _ino: u64, fh: u64, offset: i64, data: &[u8], _flags: u32, reply: ReplyWrite, ) { fsuid::set_root(); let dst_fd = get_filehandle(fh).fd.raw(); let written = tryfuse!(pwrite(dst_fd, data, offset as off_t), reply); reply.written(written as u32); } fn flush(&mut self, _req: &Request, _ino: u64, fh: u64, _lock_owner: u64, reply: ReplyEmpty) { fsuid::set_root(); let handle = get_filehandle(fh); match unistd::dup(handle.fd.raw()) { Ok(fd) => { tryfuse!(unistd::close(fd), reply); reply.ok(); } Err(_) => reply.error(libc::EIO), }; } fn release( &mut self, _req: &Request, _ino: u64, fh: u64, _flags: u32, _lock_owner: u64, _flush: bool, reply: ReplyEmpty, ) { fsuid::set_root(); unsafe { drop(Box::from_raw(fh as *mut Fh)) }; reply.ok(); } fn fsync(&mut self, _req: &Request, _ino: u64, fh: u64, datasync: bool, reply: ReplyEmpty) { fsuid::set_root(); let handle = get_filehandle(fh); let fd = handle.fd.raw(); if datasync { tryfuse!(unistd::fsync(fd), reply); } else { tryfuse!(unistd::fdatasync(fd), reply); } reply.ok(); } fn opendir(&mut self, _req: &Request, ino: u64, _flags: u32, reply: ReplyOpen) { fsuid::set_root(); let inode = tryfuse!(self.mutable_inode(ino), reply); let fd = inode.fd.read(); let path = fd_path(&fd); let dp = tryfuse!(dirent::opendir(Path::new(&path)), reply); let dirp = Box::new(DirP { magic: DIRP_MAGIC, dp, offset: 0, entry: None, }); reply.opened(Box::into_raw(dirp) as u64, 0); // freed by releasedir } fn readdir( &mut self, _req: &Request, ino: u64, fh: u64, offset: i64, reply: cntr_fuse::ReplyDirectory, ) { self.generic_readdir(ino, fh, offset as u64, ReplyDirectory::Directory(reply)) } fn readdirplus( &mut self, _req: &Request, ino: u64, fh: u64, offset: u64, reply: cntr_fuse::ReplyDirectoryPlus, ) { self.generic_readdir(ino, fh, offset, ReplyDirectory::DirectoryPlus(reply)) } fn releasedir(&mut self, _req: &Request, _ino: u64, fh: u64, _flags: u32, reply: ReplyEmpty) { fsuid::set_root(); let dirp = unsafe { Box::from_raw(fh as *mut DirP) }; assert!(dirp.magic == DIRP_MAGIC); // dirp out-of-scope -> closedir(dirp.dp) reply.ok(); } fn fsyncdir(&mut self, _req: &Request, _ino: u64, fh: u64, datasync: bool, reply: ReplyEmpty) { fsuid::set_root(); let dirp = unsafe { &mut (*(fh as *mut DirP)) }; assert!(dirp.magic == DIRP_MAGIC); let fd = tryfuse!(dirent::dirfd(&mut dirp.dp), reply); if datasync { tryfuse!(unistd::fsync(fd), reply); } else { tryfuse!(unistd::fdatasync(fd), reply); } reply.ok(); } fn statfs(&mut self, _req: &Request, ino: u64, reply: ReplyStatfs) { fsuid::set_root(); let inode = tryfuse!(self.mutable_inode(ino), reply); let fd = inode.fd.read(); let stat = tryfuse!(fstatvfs(fd.raw()), reply); reply.statfs( stat.f_blocks, stat.f_bfree, stat.f_bavail, stat.f_files, stat.f_ffree, stat.f_bsize as u32, stat.f_namemax as u32, stat.f_frsize as u32, ); } fn getxattr(&mut self, _req: &Request, ino: u64, name: &OsStr, size: u32, reply: ReplyXattr) { fsuid::set_root(); let inode = tryfuse!(self.inode(ino), reply); let fd = inode.fd.read(); if size == 0 { let res = fuse_getxattr(&fd, inode.kind, name, &mut []); let size = match res { Ok(val) => val, Err(err) => { debug!("return error {} on {}:{}", err, file!(), line!()); return reply.error(err as i32); } }; reply.size(size as u32); } else { let mut buf = vec![0; size as usize]; let res = fuse_getxattr(&fd, inode.kind, name, buf.as_mut_slice()); let size = match res { Ok(val) => val, Err(err) => { debug!("return error {} on {}:{}", err, file!(), line!()); return reply.error(err as i32); } }; reply.data(&buf[..size]); } } fn listxattr(&mut self, _req: &Request, ino: u64, size: u32, reply: ReplyXattr) { fsuid::set_root(); let inode = tryfuse!(self.inode(ino), reply); let fd = inode.fd.read(); if size == 0 { let res = fuse_listxattr(&fd, inode.kind, &mut []); let size = tryfuse!(res, reply); reply.size(size as u32); } else { let mut buf = vec![0; size as usize]; let size = tryfuse!(fuse_listxattr(&fd, inode.kind, buf.as_mut_slice()), reply); reply.data(&buf[..size]); } } fn setxattr( &mut self, _req: &Request, ino: u64, name: &OsStr, value: &[u8], flags: u32, _position: u32, reply: ReplyEmpty, ) { fsuid::set_root(); let inode = tryfuse!(self.inode(ino), reply); let fd = inode.fd.read(); if name == POSIX_ACL_DEFAULT_XATTR { let mut default_acl = inode.has_default_acl.write(); tryfuse!(fuse_setxattr(&fd, inode.kind, name, value, flags), reply); *default_acl = Some(true); } else { tryfuse!(fuse_setxattr(&fd, inode.kind, name, value, flags), reply); } reply.ok(); } fn removexattr(&mut self, _req: &Request, ino: u64, name: &OsStr, reply: ReplyEmpty) { fsuid::set_root(); let inode = tryfuse!(self.inode(ino), reply); let fd = inode.fd.read(); if name == POSIX_ACL_DEFAULT_XATTR { let mut default_acl = inode.has_default_acl.write(); tryfuse!(fuse_removexattr(&fd, inode.kind, name), reply); *default_acl = Some(false); } else { tryfuse!(fuse_removexattr(&fd, inode.kind, name), reply); } reply.ok(); } fn access(&mut self, _req: &Request, ino: u64, mask: u32, reply: ReplyEmpty) { fsuid::set_root(); let inode = tryfuse!(self.inode(ino), reply); let mode = unistd::AccessFlags::from_bits_truncate(mask as i32); tryfuse!( unistd::access(fd_path(&inode.fd.read()).as_str(), mode), reply ); reply.ok(); } fn create( &mut self, req: &Request, parent: u64, name: &OsStr, mode: u32, umask: u32, flags: u32, reply: ReplyCreate, ) { let fd = tryfuse!( self.create_file(req, parent, name, mode, umask, flags), reply ); let new_file = unsafe { File::from_raw_fd(fd) }; let (attr, generation) = tryfuse!(self.lookup_from_fd(LookupFile::Borrow(&new_file)), reply); let fh = Fh::new(Fd::new(new_file.into_raw_fd(), FdState::Readable)); let fp = Box::into_raw(fh) as u64; // freed by close reply.created(&TTL, &attr, generation, fp, flags); } // we do not support remote locking at the moment and rely on the kernel //use fuse::ReplyLock; //fn getlk( // &mut self, // _req: &Request, // _ino: u64, // fh: u64, // _lock_owner: u64, // start: u64, // end: u64, // typ: u32, // pid: u32, // reply: ReplyLock, //) { // fsuid::set_root(); // let handle = get_filehandle(fh); // let mut flock = libc::flock { // l_type: typ as i16, // l_whence: 0, // l_start: start as i64, // l_len: (end - start) as i64, // l_pid: pid as i32, // }; // tryfuse!( // fcntl::fcntl(handle.fd.raw(), fcntl::F_GETLK(&mut flock)), // reply // ); // reply.locked( // flock.l_start as u64, // (flock.l_start + flock.l_len) as u64, // flock.l_type as u32, // flock.l_pid as u32, // ) //} //fn setlk( // &mut self, // _req: &Request, // _ino: u64, // fh: u64, // _lock_owner: u64, // start: u64, // end: u64, // typ: u32, // pid: u32, // _sleep: bool, // reply: ReplyEmpty, //) { // fsuid::set_root(); // let handle = get_filehandle(fh); // let flock = libc::flock { // l_type: typ as i16, // l_whence: 0, // l_start: start as i64, // l_len: (end - start) as i64, // l_pid: pid as i32, // }; // tryfuse!(fcntl::fcntl(handle.fd.raw(), fcntl::F_SETLK(&flock)), reply); // reply.ok() //} /// Preallocate or deallocate space to a file fn fallocate( &mut self, _req: &Request, _ino: u64, fh: u64, offset: u64, length: u64, mode: u32, reply: ReplyEmpty, ) { fsuid::set_root(); let handle = get_filehandle(fh); let flags = fcntl::FallocateFlags::from_bits_truncate(mode as i32); tryfuse!( fcntl::fallocate(handle.fd.raw(), flags, offset as off_t, length as off_t), reply ); reply.ok(); } fn ioctl( &mut self, _req: &Request, _ino: u64, fh: u64, flags: u32, _cmd: u32, in_data: Option<&[u8]>, out_size: u32, reply: ReplyIoctl, ) { fsuid::set_root(); let fd = if (flags & cntr_fuse::consts::FUSE_IOCTL_DIR) > 0 { let dirp = unsafe { &mut (*(fh as *mut DirP)) }; assert!(dirp.magic == DIRP_MAGIC); tryfuse!(dirent::dirfd(&mut dirp.dp), reply) } else { get_filehandle(fh).fd.raw() }; let cmd = c_ulong::from(_cmd); if out_size > 0 { let mut out = vec![0; out_size as usize]; if let Some(data) = in_data { out[..data.len()].clone_from_slice(data); } tryfuse!(ioctl_read(fd, cmd, out.as_mut_slice()), reply); reply.ioctl(0, out.as_slice()); } else if let Some(data) = in_data { tryfuse!(ioctl_write(fd, cmd, data), reply); reply.ioctl(0, &[]); } else { tryfuse!(ioctl(fd, cmd), reply); reply.ioctl(0, &[]); } } fn lseek( &mut self, _req: &Request, _ino: u64, fh: u64, offset: i64, whence: u32, reply: ReplyLseek, ) { fsuid::set_root(); let fd = get_filehandle(fh).fd.raw(); let new_offset = tryfuse!( unistd::lseek64(fd, offset, unsafe { mem::transmute(whence as i32) }), reply ); reply.offset(new_offset); } } cntr-1.5.3/src/fsuid.rs000064400000000000000000000013171046102023000130510ustar 00000000000000use libc::{setfsgid, setfsuid, uid_t}; use std::cell::Cell; const CURRENT_FSUID: uid_t = (-1_i32) as uid_t; thread_local! { static FSUID : Cell = Cell::new(unsafe { setfsuid(CURRENT_FSUID) as u32 }); static FSGID : Cell = Cell::new(unsafe { setfsgid(CURRENT_FSUID) as u32 }); } pub fn set_root() { set_user_group(0, 0); } pub fn set_user_group(uid: u32, gid: u32) { // setfsuid/setfsgid set no error on failure FSUID.with(|fsuid| { if fsuid.get() != uid { unsafe { setfsuid(uid) }; } fsuid.set(uid); }); FSGID.with(|fsgid| { if fsgid.get() != gid { unsafe { setfsgid(gid) }; } fsgid.set(gid); }); } cntr-1.5.3/src/fusefd.rs000064400000000000000000000017721046102023000132200ustar 00000000000000use nix::fcntl::OFlag; use nix::sys::stat::{self, Mode, SFlag}; use nix::{self, errno, fcntl}; use simple_error::{bail, try_with}; use std::fs::File; use std::os::unix::prelude::*; use crate::result::Result; pub fn open() -> Result { let res = fcntl::open("/dev/fuse", OFlag::O_RDWR, stat::Mode::empty()); match res { Ok(fd) => { let file = unsafe { File::from_raw_fd(fd) }; return Ok(file); } Err(errno::Errno::ENOENT) => {} Err(err) => bail!("failed to open /dev/fuse: {}", err), }; try_with!( stat::mknod( "/dev/fuse", SFlag::S_IFCHR, Mode::S_IRUSR | Mode::S_IWUSR, stat::makedev(10, 229), ), "failed to create temporary fuse character device" ); let file = unsafe { File::from_raw_fd(try_with!( fcntl::open("/dev/fuse", OFlag::O_RDWR, stat::Mode::empty()), "failed to open fuse device" )) }; Ok(file) } cntr-1.5.3/src/inode.rs000064400000000000000000000032611046102023000130350ustar 00000000000000use cntr_fuse::FileType; use nix::fcntl; use nix::fcntl::OFlag; use nix::sys::stat; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use std::ffi::OsStr; use std::path::Path; use crate::files::{fd_path, Fd, FdState}; use crate::fs::POSIX_ACL_DEFAULT_XATTR; use crate::fsuid; use crate::sys_ext::fuse_getxattr; pub struct Inode { pub fd: RwLock, pub kind: FileType, pub ino: u64, pub dev: u64, pub nlookup: RwLock, pub has_default_acl: RwLock>, } impl Inode { pub fn upgrade_fd(&self, state: &FdState) -> nix::Result<()> { let fd = self.fd.upgradable_read(); if fd.state >= *state { return Ok(()); } let mut fd = RwLockUpgradableReadGuard::upgrade(fd); let perm = if *state == FdState::ReadWritable { OFlag::O_RDWR } else { OFlag::O_RDONLY }; let flags = perm | OFlag::O_CLOEXEC | OFlag::O_NONBLOCK; let path = fd_path(&fd); let new_fd = Fd::new( fcntl::open(Path::new(&path), flags, stat::Mode::empty())?, FdState::from(flags), ); *fd = new_fd; Ok(()) } pub fn check_default_acl(&self) -> nix::Result { fsuid::set_root(); let state = self.has_default_acl.upgradable_read(); if let Some(s) = *state { return Ok(s); } let mut state = RwLockUpgradableReadGuard::upgrade(state); self.upgrade_fd(&FdState::Readable)?; let fd = self.fd.read(); let res = fuse_getxattr(&fd, self.kind, OsStr::new(POSIX_ACL_DEFAULT_XATTR), &mut []); *state = Some(res.is_ok()); Ok(res.is_ok()) } } cntr-1.5.3/src/ipc.rs000064400000000000000000000051061046102023000125120ustar 00000000000000use nix::errno::Errno; use nix::sys::socket::*; use simple_error::try_with; use std::fs::File; use std::io::{IoSlice, IoSliceMut}; use std::os::unix::prelude::*; use crate::result::Result; pub struct Socket { fd: File, } const NONE: Option<&UnixAddr> = None; impl Socket { pub fn send(&self, messages: &[&[u8]], files: &[&File]) -> Result<()> { let iov: Vec = messages.iter().map(|m| IoSlice::new(m)).collect(); let fds: Vec = files.iter().map(|f| f.as_raw_fd()).collect(); let cmsg = if files.is_empty() { vec![] } else { vec![ControlMessage::ScmRights(&fds)] }; try_with!( sendmsg(self.fd.as_raw_fd(), &iov, &cmsg, MsgFlags::empty(), NONE), "sendmsg failed" ); Ok(()) } pub fn receive( &self, message_length: usize, cmsgspace: &mut Vec, ) -> Result<(Vec, Vec)> { let mut msg_buf = vec![0; message_length]; let received; let mut files: Vec = Vec::with_capacity(1); { let mut iov = vec![IoSliceMut::new(&mut msg_buf)]; loop { let res = recvmsg::( self.fd.as_raw_fd(), &mut iov[..], Some(&mut *cmsgspace), MsgFlags::empty(), ); match res { Err(Errno::EAGAIN) | Err(Errno::EINTR) => continue, Err(e) => return try_with!(Err(e), "recvmsg failed"), Ok(msg) => { for cmsg in msg.cmsgs() { if let ControlMessageOwned::ScmRights(fds) = cmsg { for fd in fds { files.push(unsafe { File::from_raw_fd(fd) }) } } } received = msg.bytes; break; } }; } } msg_buf.resize(received, 0); Ok((msg_buf, files)) } } pub fn socket_pair() -> Result<(Socket, Socket)> { let res = socketpair( AddressFamily::Unix, SockType::Datagram, None, SockFlag::SOCK_CLOEXEC, ); let (parent_fd, child_fd) = try_with!(res, "failed to create socketpair"); Ok(( Socket { fd: unsafe { File::from_raw_fd(parent_fd) }, }, Socket { fd: unsafe { File::from_raw_fd(child_fd) }, }, )) } cntr-1.5.3/src/lib.rs000064400000000000000000000010341046102023000125010ustar 00000000000000pub use container_pid::{lookup_container_type, AVAILABLE_CONTAINER_TYPES}; pub use logging::enable_debug_log; pub use user_namespace::DEFAULT_ID_MAP; mod attach; mod capabilities; mod cgroup; mod cmd; mod dirent; mod dotcntr; mod exec; mod files; pub mod fs; mod fsuid; mod fusefd; mod inode; mod ipc; mod logging; mod lsm; mod mount_context; mod mountns; pub mod namespace; mod num_cpus; mod procfs; mod pty; mod result; mod sys_ext; mod tmp; mod user_namespace; pub use attach::{attach, AttachOptions}; pub use exec::{exec, SETCAP_EXE}; cntr-1.5.3/src/logging.rs000064400000000000000000000007711046102023000133700ustar 00000000000000struct Logger; impl log::Log for Logger { fn enabled(&self, _metadata: &log::Metadata) -> bool { true } fn log(&self, record: &log::Record) { if self.enabled(record.metadata()) { eprintln!("{} - {}", record.level(), record.args()); } } fn flush(&self) {} } static LOGGER: Logger = Logger; pub fn enable_debug_log() -> Result<(), log::SetLoggerError> { log::set_logger(&LOGGER)?; log::set_max_level(log::LevelFilter::Debug); Ok(()) } cntr-1.5.3/src/lsm.rs000064400000000000000000000106671046102023000125420ustar 00000000000000use nix::unistd::Pid; use simple_error::try_with; use std::fs::{File, OpenOptions}; use std::io::prelude::*; use std::io::BufReader; use std::io::ErrorKind; use std::path::{Path, PathBuf}; use crate::mount_context; use crate::procfs; use crate::result::Result; #[derive(PartialEq, Eq)] enum LSMKind { AppArmor, SELinux, } impl LSMKind { pub fn profile_path(&self, pid: Option) -> PathBuf { match *self { LSMKind::AppArmor => { let process = pid.map_or(String::from("self"), |p| p.to_string()); procfs::get_path().join(process).join("attr/current") } LSMKind::SELinux => { let process = pid.map_or(String::from("thread-self"), |p| p.to_string()); procfs::get_path().join(process).join("attr/exec") } } } } pub struct LSMProfile { label: String, kind: LSMKind, label_file: File, } fn is_apparmor_enabled() -> Result { let aa_path = "/sys/module/apparmor/parameters/enabled"; match File::open(aa_path) { Ok(mut file) => { let mut contents = String::new(); try_with!( file.read_to_string(&mut contents), "failed to read {}", aa_path ); Ok(contents == "Y\n") } Err(err) => { if err.kind() != ErrorKind::NotFound { try_with!(Err(err), "failed to open {}", aa_path); } Ok(false) } } } fn is_selinux_enabled() -> Result { let file = try_with!( File::open("/proc/filesystems"), "failed to open /proc/filesystems" ); let reader = BufReader::new(file); for line in reader.lines() { let l = try_with!(line, "failed to read from /proc/filesystems"); if l.contains("selinuxfs") { return Ok(true); } } Ok(false) } fn check_type() -> Result> { if try_with!( is_apparmor_enabled(), "failed to check availability of apparmor" ) { Ok(Some(LSMKind::AppArmor)) } else if try_with!( is_selinux_enabled(), "failed to check availability of selinux" ) { Ok(Some(LSMKind::SELinux)) } else { Ok(None) } } fn read_proclabel(path: &Path, kind: &LSMKind) -> Result { let mut attr = String::new(); let mut file = try_with!(File::open(path), "failed to open {}", path.display()); try_with!( file.read_to_string(&mut attr), "failed to read {}", path.display() ); if *kind == LSMKind::AppArmor { let fields: Vec<&str> = attr.trim_end().splitn(2, ' ').collect(); Ok(fields[0].to_owned()) } else { Ok(attr) } } pub fn read_profile(pid: Pid) -> Result> { let kind = check_type()?; if let Some(kind) = kind { let target_path = kind.profile_path(Some(pid)); let target_label = try_with!( read_proclabel(&target_path, &kind), "failed to get security label of target process" ); let own_path = kind.profile_path(None); let own_label = try_with!( read_proclabel(&own_path, &kind), "failed to get own security label" ); if target_label == own_label { // nothing to do return Ok(None); } let res = OpenOptions::new().write(true).open(&own_path); return Ok(Some(LSMProfile { kind, label: target_label, label_file: try_with!(res, "failed to open {}", own_path.display()), })); } Ok(None) } impl LSMProfile { pub fn inherit_profile(mut self) -> Result<()> { let attr = match self.kind { LSMKind::AppArmor => format!("changeprofile {}", self.label), LSMKind::SELinux => self.label, }; let res = self.label_file.write_all(attr.as_bytes()); try_with!(res, "failed to write '{}' to /proc/self/attr/current", attr); Ok(()) } pub fn mount_label(&self, pid: Pid) -> Result> { match self.kind { LSMKind::AppArmor => Ok(None), LSMKind::SELinux => { let context = try_with!( mount_context::parse_selinux_context(pid), "failed to parse selinux mount options" ); Ok(Some(context)) } } } } cntr-1.5.3/src/mount_context.rs000064400000000000000000000031331046102023000146430ustar 00000000000000use nix::unistd::Pid; use simple_error::{bail, try_with}; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use crate::procfs; use crate::result::Result; //$ cat /proc/self/mounts // tmpfs /proc/kcore tmpfs rw,context="system_u:object_r:container_file_t:s0:c125,c287",nosuid,mode=755 0 0 fn find_mount_options(p: Pid) -> Result { let path = procfs::get_path().join(format!("{}/mounts", p)); let f = try_with!(File::open(&path), "failed to open {}", path.display()); let reader = BufReader::new(f); for line in reader.lines() { let line = try_with!(line, "failed to read {}", path.display()); let line = line.trim(); let mut tokens = line .split_terminator(|s: char| s == ' ' || s == '\t') .filter(|s| s != &""); if let Some(mountpoint) = tokens.nth(1) { if let Some(options) = tokens.nth(1) { if mountpoint == "/" { return Ok(String::from(options)); } } } } bail!("did not find / in {}", path.display()) } pub fn parse_selinux_context(p: Pid) -> Result { let options = try_with!(find_mount_options(p), "failed to parse mount options of /"); let needle = "context=\""; if let Some(index) = options.find(needle) { if let Some(context) = options[(index + needle.len())..].split('"').next() { return Ok(String::from(context)); } else { bail!("missing quotes selinux context: {}", options); }; } bail!("no selinux mount option found for / entry: {}", options) } cntr-1.5.3/src/mountns.rs000064400000000000000000000153551046102023000134510ustar 00000000000000use log::warn; use nix::mount::MsFlags; use nix::sched::CloneFlags; use nix::{cmsg_space, mount, sched, unistd}; use simple_error::try_with; use std::fs; use std::io; use std::os::unix::prelude::*; use std::path::PathBuf; use std::{ ffi::OsStr, fs::{set_permissions, Permissions}, }; use crate::files::mkdir_p; use crate::fs::CntrFs; use crate::ipc; use crate::namespace; use crate::result::Result; use crate::tmp; pub struct MountNamespace { old_namespace: namespace::Namespace, mountpoint: PathBuf, temp_mountpoint: PathBuf, } const MOUNTS: &[&str] = &[ "etc/passwd", "etc/group", "etc/resolv.conf", "etc/hosts", "etc/hostname", "etc/localtime", "etc/zoneinfo", "dev", "sys", "proc", ]; const CNTR_MOUNT_POINT: &str = "var/lib/cntr"; impl MountNamespace { fn new(old_namespace: namespace::Namespace) -> Result { let mountpoint = try_with!(tmp::tempdir(), "failed to create temporary mountpoint"); try_with!( set_permissions(mountpoint.path(), Permissions::from_mode(0o755)), "cannot change permissions of '{}'", mountpoint.path().display() ); let temp_mountpoint = try_with!(tmp::tempdir(), "failed to create temporary mountpoint"); try_with!( set_permissions(temp_mountpoint.path(), Permissions::from_mode(0o755)), "cannot change permissions of '{}'", temp_mountpoint.path().display() ); try_with!( sched::unshare(CloneFlags::CLONE_NEWNS), "failed to create mount namespace" ); Ok(MountNamespace { old_namespace, mountpoint: mountpoint.into_path(), temp_mountpoint: temp_mountpoint.into_path(), }) } fn send(self, sock: &ipc::Socket) -> Result { let res = { let message = &[ self.mountpoint.as_os_str().as_bytes(), b"\0", self.temp_mountpoint.as_os_str().as_bytes(), ]; sock.send(message, &[self.old_namespace.file()]) }; match res { Ok(_) => Ok(self), Err(e) => { self.cleanup(); Err(e) } } } pub fn receive(sock: &ipc::Socket) -> Result { let mut cmsgspace = cmsg_space!([RawFd; 2]); let (paths, mut fds) = try_with!( sock.receive((libc::PATH_MAX * 2) as usize, &mut cmsgspace), "failed to receive mount namespace" ); let paths: Vec<&[u8]> = paths.splitn(2, |c| *c == b'\0').collect(); assert!(paths.len() == 2); let fd = fds.pop().unwrap(); Ok(MountNamespace { old_namespace: namespace::MOUNT.namespace_from_file(fd), mountpoint: PathBuf::from(OsStr::from_bytes(paths[0])), temp_mountpoint: PathBuf::from(OsStr::from_bytes(paths[1])), }) } pub fn cleanup(self) { if let Err(err) = self.old_namespace.apply() { warn!("failed to switch back to old mount namespace: {}", err); return; } if let Err(err) = fs::remove_dir(&self.mountpoint) { warn!( "failed to cleanup mountpoint {:?}: {}", self.mountpoint, err ); } if let Err(err) = fs::remove_dir(&self.temp_mountpoint) { warn!( "failed to cleanup temporary mountpoint {:?}: {}", self.mountpoint, err ); } } } const NONE: Option<&'static [u8]> = None; pub fn setup_bindmounts(mounts: &[&str]) -> Result<()> { for m in mounts { let mountpoint_buf = PathBuf::from("/").join(m); let mountpoint = mountpoint_buf.as_path(); let source_buf = PathBuf::from("/var/lib/cntr").join(m); let source = source_buf.as_path(); let mountpoint_stat = match fs::metadata(mountpoint) { Err(e) => { if e.kind() == io::ErrorKind::NotFound { continue; } return try_with!( Err(e), "failed to get metadata of path {}", mountpoint.display() ); } Ok(data) => data, }; let source_stat = match fs::metadata(source) { Err(e) => { if e.kind() == io::ErrorKind::NotFound { continue; } return try_with!( Err(e), "failed to get metadata of path {}", source.display() ); } Ok(data) => data, }; #[allow(clippy::suspicious_operation_groupings)] if !((source_stat.is_file() && !mountpoint_stat.is_dir()) || (source_stat.is_dir() && mountpoint_stat.is_dir())) { continue; } let res = mount::mount( Some(source), mountpoint, NONE, MsFlags::MS_REC | MsFlags::MS_BIND, NONE, ); if res.is_err() { warn!("could not bind mount {:?}", mountpoint); } } Ok(()) } pub fn setup( fs: &CntrFs, socket: &ipc::Socket, container_namespace: namespace::Namespace, mount_label: &Option, ) -> Result<()> { try_with!( mkdir_p(&CNTR_MOUNT_POINT), "cannot create container mountpoint /{}", CNTR_MOUNT_POINT ); let ns = MountNamespace::new(container_namespace)?; try_with!( mount::mount( Some("none"), "/", NONE, MsFlags::MS_REC | MsFlags::MS_PRIVATE, NONE, ), "unable to bind mount /" ); // prepare bind mounts try_with!( mount::mount( Some("/"), &ns.temp_mountpoint, NONE, MsFlags::MS_REC | MsFlags::MS_BIND, NONE, ), "unable to move container mounts to new mountpoint" ); try_with!(fs.mount(ns.mountpoint.as_path(), mount_label), "mount()"); let ns = try_with!(ns.send(socket), "parent failed"); try_with!( mount::mount( Some(&ns.temp_mountpoint), &ns.mountpoint.join(CNTR_MOUNT_POINT), NONE, MsFlags::MS_REC | MsFlags::MS_MOVE, NONE, ), "unable to move container mounts to new mountpoint" ); try_with!( unistd::chdir(&ns.mountpoint), "failed to chdir to new mountpoint" ); try_with!( unistd::chroot(&ns.mountpoint), "failed to chroot to new mountpoint" ); try_with!(setup_bindmounts(MOUNTS), "failed to setup bind mounts"); Ok(()) } cntr-1.5.3/src/namespace.rs000064400000000000000000000046671046102023000137060ustar 00000000000000use nix::sched; use nix::unistd; use simple_error::try_with; use std::collections::HashSet; use std::fs::{self, File}; use std::os::unix::prelude::*; use std::path::PathBuf; use crate::procfs; use crate::result::Result; pub const MOUNT: Kind = Kind { name: "mnt" }; pub const UTS: Kind = Kind { name: "uts" }; pub const USER: Kind = Kind { name: "user" }; pub const PID: Kind = Kind { name: "pid" }; pub const NET: Kind = Kind { name: "net" }; pub const CGROUP: Kind = Kind { name: "cgroup" }; pub const IPC: Kind = Kind { name: "ipc" }; pub static ALL: &[Kind] = &[UTS, CGROUP, PID, NET, IPC, MOUNT, USER]; pub struct Kind { pub name: &'static str, } pub fn supported_namespaces() -> Result> { let mut namespaces = HashSet::new(); let entries = try_with!( fs::read_dir(PathBuf::from("/proc/self/ns")), "failed to open directory /proc/self/ns" ); for entry in entries { let entry = try_with!(entry, "failed to read directory /proc/self/ns"); if let Ok(name) = entry.file_name().into_string() { namespaces.insert(name); } } Ok(namespaces) } impl Kind { pub fn open(&'static self, pid: unistd::Pid) -> Result { let buf = self.path(pid); let path = buf.to_str().unwrap(); let file = try_with!(File::open(path), "failed to open namespace file '{}'", path); Ok(Namespace { kind: self, file }) } pub fn namespace_from_file(&'static self, file: File) -> Namespace { Namespace { kind: self, file } } pub fn is_same(&self, pid: unistd::Pid) -> bool { let path = self.path(pid); match fs::read_link(path) { Ok(dest) => match fs::read_link(self.own_path()) { Ok(dest2) => dest == dest2, _ => false, }, _ => false, } } fn path(&self, pid: unistd::Pid) -> PathBuf { procfs::get_path() .join(pid.to_string()) .join("ns") .join(self.name) } fn own_path(&self) -> PathBuf { PathBuf::from("/proc/self/ns").join(self.name) } } pub struct Namespace { pub kind: &'static Kind, file: File, } impl Namespace { pub fn apply(&self) -> Result<()> { try_with!( sched::setns(self.file.as_raw_fd(), sched::CloneFlags::empty()), "setns" ); Ok(()) } pub fn file(&self) -> &File { &self.file } } cntr-1.5.3/src/num_cpus.rs000064400000000000000000000001341046102023000135640ustar 00000000000000pub fn get() -> usize { unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) as usize } } cntr-1.5.3/src/procfs/mod.rs000064400000000000000000000055751046102023000140240ustar 00000000000000use libc::{c_ulong, pid_t}; use nix::unistd::Pid; use simple_error::{try_with, SimpleError}; use std::env; use std::ffi::OsString; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use std::path::PathBuf; use crate::result::Result; mod unix; pub fn get_path() -> PathBuf { PathBuf::from(&env::var_os("CNTR_PROC").unwrap_or_else(|| OsString::from("/proc"))) } pub struct ProcStatus { pub global_pid: Pid, pub local_pid: Pid, pub inherited_capabilities: c_ulong, pub effective_capabilities: c_ulong, } pub fn status(target_pid: Pid) -> Result { let path = get_path().join(target_pid.to_string()).join("status"); let file = try_with!(File::open(&path), "failed to open {}", path.display()); let mut ns_pid: Option = None; let mut inherited_caps: Option = None; let mut effective_caps: Option = None; let reader = BufReader::new(file); for line in reader.lines() { let line = try_with!(line, "could not read {}", path.display()); let columns: Vec<&str> = line.split('\t').collect(); assert!(columns.len() >= 2); if columns[0] == "NSpid:" { if let Some(pid_string) = columns.last() { let pid = try_with!( pid_string.parse::(), "read invalid pid from proc: '{}'", columns[1] ); ns_pid = Some(Pid::from_raw(pid)); } } else if columns[0] == "CapInh:" { if let Some(cap_string) = columns.last() { let cap = try_with!( c_ulong::from_str_radix(cap_string, 16), "read invalid capability from proc: '{}'", columns[1] ); inherited_caps = Some(cap); } } else if columns[0] == "CapEff:" { if let Some(cap_string) = columns.last() { let cap = try_with!( c_ulong::from_str_radix(cap_string, 16), "read invalid capability from proc: '{}'", columns[1] ); effective_caps = Some(cap); } } } Ok(ProcStatus { global_pid: target_pid, local_pid: ns_pid.ok_or_else(|| { SimpleError::new(format!( "Could not find namespace pid in {}", path.display() )) })?, inherited_capabilities: inherited_caps.ok_or_else(|| { SimpleError::new(format!( "Could not find inherited capabilities in {}", path.display() )) })?, effective_capabilities: effective_caps.ok_or_else(|| { SimpleError::new(format!( "Could not find effective capabilities in {}", path.display() )) })?, }) } cntr-1.5.3/src/procfs/unix.rs000064400000000000000000000012571046102023000142210ustar 00000000000000use simple_error::try_with; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use std::path::PathBuf; use crate::result::Result; #[allow(dead_code)] pub fn read_open_sockets() -> Result> { let file = try_with!(File::open("/proc/net/unix"), "cannot open /proc/net/unix"); let mut paths = vec![]; for line in BufReader::new(file).lines().skip(1) { let line = try_with!(line, "failed to read /proc/net/unix"); let fields: Vec<&str> = line.splitn(7, ' ').collect(); if fields.len() != 8 || fields[7].starts_with('@') { continue; } paths.push(PathBuf::from(fields[7])); } Ok(paths) } cntr-1.5.3/src/pty.rs000064400000000000000000000166331046102023000125620ustar 00000000000000use libc::{self, winsize}; use nix::errno::Errno; use nix::fcntl::OFlag; use nix::pty::*; use nix::sys::select; use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, SIGWINCH}; use nix::sys::stat; use nix::sys::termios::SpecialCharacterIndices::*; use nix::sys::termios::{ tcgetattr, tcsetattr, ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, Termios, }; use nix::{self, fcntl, unistd}; use simple_error::try_with; use std::fs::File; use std::io::{Read, Write}; use std::os::unix::prelude::*; use crate::result::Result; enum FilePairState { Write, Read, } struct FilePair<'a> { from: &'a File, to: &'a File, buf: [u8; libc::BUFSIZ as usize], read_offset: usize, write_offset: usize, state: FilePairState, } impl<'a> FilePair<'a> { fn new(from: &'a File, to: &'a File) -> FilePair<'a> { FilePair { from, to, buf: [8; libc::BUFSIZ as usize], write_offset: 0, read_offset: 0, state: FilePairState::Read, } } fn read(&mut self) -> bool { match self.from.read(&mut self.buf) { Ok(read) => { self.read_offset = read; self.write() } Err(_) => false, } } fn write(&mut self) -> bool { match self .to .write(&self.buf[self.write_offset..self.read_offset]) { Ok(written) => { self.write_offset += written; if self.write_offset >= self.read_offset { self.read_offset = 0; self.write_offset = 0; self.state = FilePairState::Read; } else { self.state = FilePairState::Write; }; true } Err(_) => false, } } } struct RawTty { fd: RawFd, attr: Termios, } impl RawTty { fn new(stdin: RawFd) -> Result { let orig_attr = try_with!(tcgetattr(stdin), "failed to get termios attributes"); let mut attr = orig_attr.clone(); attr.input_flags.remove( InputFlags::IGNBRK | InputFlags::BRKINT | InputFlags::PARMRK | InputFlags::ISTRIP | InputFlags::INLCR | InputFlags::IGNCR | InputFlags::ICRNL | InputFlags::IXON, ); attr.output_flags.remove(OutputFlags::OPOST); attr.local_flags.remove( LocalFlags::ECHO | LocalFlags::ECHONL | LocalFlags::ICANON | LocalFlags::ISIG | LocalFlags::IEXTEN, ); attr.control_flags .remove(ControlFlags::CSIZE | ControlFlags::PARENB); attr.control_flags.insert(ControlFlags::CS8); attr.control_chars[VMIN as usize] = 1; // One character-at-a-time input attr.control_chars[VTIME as usize] = 0; // with blocking read try_with!( tcsetattr(stdin, SetArg::TCSAFLUSH, &attr), "failed to set termios attributes" ); Ok(RawTty { fd: stdin, attr: orig_attr, }) } } impl Drop for RawTty { fn drop(&mut self) { let _ = tcsetattr(self.fd, SetArg::TCSANOW, &self.attr); } } fn shovel(pairs: &mut [FilePair]) { let mut read_set = select::FdSet::new(); let mut write_set = select::FdSet::new(); loop { read_set.clear(); write_set.clear(); let mut highest = 0; for pair in pairs.iter_mut() { let fd = match pair.state { FilePairState::Read => { let raw_fd = pair.from.as_raw_fd(); read_set.insert(raw_fd); raw_fd } FilePairState::Write => { let raw_fd = pair.to.as_raw_fd(); write_set.insert(raw_fd); raw_fd } }; if highest < fd { highest = fd; } } match select::select( highest + 1, Some(&mut read_set), Some(&mut write_set), None, None, ) { Err(Errno::EINTR) => { continue; } Err(_) => { return; } _ => {} } for pair in pairs.iter_mut() { match pair.state { FilePairState::Read => { if read_set.contains(pair.from.as_raw_fd()) && !pair.read() { return; } } FilePairState::Write => { if write_set.contains(pair.to.as_raw_fd()) && !pair.write() { return; } } } } } } extern "C" fn handle_sigwinch(_: i32) { let fd = unsafe { PTY_MASTER_FD }; if fd != -1 { resize_pty(fd); } } static mut PTY_MASTER_FD: i32 = -1; pub fn forward(pty: &File) -> Result<()> { let mut raw_tty = None; if unsafe { libc::isatty(libc::STDIN_FILENO as i32) } != 0 { resize_pty(pty.as_raw_fd()); raw_tty = Some(try_with!( RawTty::new(libc::STDIN_FILENO), "failed to set stdin tty into raw mode" )) }; unsafe { PTY_MASTER_FD = pty.as_raw_fd() }; let sig_action = SigAction::new( SigHandler::Handler(handle_sigwinch), SaFlags::empty(), SigSet::empty(), ); try_with!( unsafe { sigaction(SIGWINCH, &sig_action) }, "failed to install SIGWINCH handler" ); let stdin: File = unsafe { File::from_raw_fd(libc::STDIN_FILENO) }; let stdout: File = unsafe { File::from_raw_fd(libc::STDOUT_FILENO) }; let pty_file: File = unsafe { File::from_raw_fd(pty.as_raw_fd()) }; shovel(&mut [ FilePair::new(&stdin, &pty_file), FilePair::new(&pty_file, &stdout), ]); stdin.into_raw_fd(); stdout.into_raw_fd(); pty_file.into_raw_fd(); unsafe { PTY_MASTER_FD = -1 }; if let Some(_raw_tty) = raw_tty { drop(_raw_tty) } Ok(()) } fn get_winsize(term_fd: RawFd) -> winsize { use std::mem::zeroed; unsafe { let mut ws: winsize = zeroed(); match libc::ioctl(term_fd, libc::TIOCGWINSZ, &mut ws) { 0 => ws, _ => winsize { ws_row: 80, ws_col: 25, ws_xpixel: 0, ws_ypixel: 0, }, } } } fn resize_pty(pty_master: RawFd) { unsafe { libc::ioctl( pty_master, libc::TIOCSWINSZ, &mut get_winsize(libc::STDOUT_FILENO), ); } } pub fn open_ptm() -> Result { let pty_master = try_with!(posix_openpt(OFlag::O_RDWR), "posix_openpt()"); try_with!(grantpt(&pty_master), "grantpt()"); try_with!(unlockpt(&pty_master), "unlockpt()"); Ok(pty_master) } pub fn attach_pts(pty_master: &PtyMaster) -> nix::Result<()> { let pts_name = ptsname_r(pty_master)?; unistd::setsid()?; let pty_slave = fcntl::open(pts_name.as_str(), OFlag::O_RDWR, stat::Mode::empty())?; unistd::dup2(pty_slave, libc::STDIN_FILENO)?; unistd::dup2(pty_slave, libc::STDOUT_FILENO)?; unistd::dup2(pty_slave, libc::STDERR_FILENO)?; unistd::close(pty_slave)?; Ok(()) } cntr-1.5.3/src/result.rs000064400000000000000000000001461046102023000132540ustar 00000000000000use simple_error::SimpleError; use std::result; pub type Result = result::Result; cntr-1.5.3/src/sys_ext/internal/fchownat.rs000064400000000000000000000021421046102023000170570ustar 00000000000000use nix::errno::Errno; use nix::fcntl::AtFlags; use nix::unistd; use std::os::unix::prelude::RawFd; // According to the POSIX, -1 is used to indicate that // owner and group, respectively, are not to be changed. Since uid_t and // gid_t are unsigned types, we use wrapping_sub to get '-1'. fn optional_user(val: Option) -> u32 { val.map(Into::into) .unwrap_or_else(|| (0_u32).wrapping_sub(1)) } fn optional_group(val: Option) -> u32 { val.map(Into::into) .unwrap_or_else(|| (0_u32).wrapping_sub(1)) } /// Change ownership of a file /// (see [fchownat(2)](http://man7.org/linux/man-pages/man2/fchownat.2.html)). pub fn fchownat( dirfd: RawFd, pathname: &P, owner: Option, group: Option, flags: AtFlags, ) -> nix::Result<()> { let res = pathname.with_nix_path(|cstr| unsafe { libc::fchownat( dirfd, cstr.as_ptr(), optional_user(owner), optional_group(group), flags.bits(), ) })?; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/fstatvfs.rs000064400000000000000000000004221046102023000171050ustar 00000000000000use nix::errno::Errno; use nix::Result; use std::mem; use std::os::unix::io::RawFd; pub fn fstatvfs(fd: RawFd) -> Result { let mut s = unsafe { mem::zeroed() }; let res = unsafe { libc::fstatvfs64(fd, &mut s) }; Errno::result(res).map(|_| s) } cntr-1.5.3/src/sys_ext/internal/ioctl.rs000064400000000000000000000023531046102023000163640ustar 00000000000000use libc::{self, c_ulong}; use nix::errno::Errno; use nix::Result; use std::os::unix::io::RawFd; #[cfg(not(any(target_env = "musl")))] pub fn ioctl_read(fd: RawFd, cmd: c_ulong, data: &mut [u8]) -> Result<()> { let res = unsafe { libc::ioctl(fd, cmd, data) }; Errno::result(res).map(drop) } #[cfg(any(target_env = "musl"))] pub fn ioctl_read(fd: RawFd, cmd: c_ulong, data: &mut [u8]) -> Result<()> { let res = unsafe { libc::ioctl(fd, cmd as i32, data) }; Errno::result(res).map(drop) } #[cfg(not(any(target_env = "musl")))] pub fn ioctl_write(fd: RawFd, cmd: c_ulong, data: &[u8]) -> Result<()> { let res = unsafe { libc::ioctl(fd, cmd, data) }; Errno::result(res).map(drop) } #[cfg(any(target_env = "musl"))] pub fn ioctl_write(fd: RawFd, cmd: c_ulong, data: &[u8]) -> Result<()> { let res = unsafe { libc::ioctl(fd, cmd as i32, data) }; Errno::result(res).map(drop) } #[cfg(not(any(target_env = "musl")))] pub fn ioctl(fd: RawFd, cmd: c_ulong) -> Result<()> { let res = unsafe { libc::ioctl(fd, cmd) }; Errno::result(res).map(drop) } #[cfg(any(target_env = "musl"))] pub fn ioctl(fd: RawFd, cmd: c_ulong) -> Result<()> { let res = unsafe { libc::ioctl(fd, cmd as i32) }; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/linkat.rs000064400000000000000000000014421046102023000165320ustar 00000000000000use nix::errno::Errno; use nix::fcntl::AtFlags; use std::os::unix::prelude::RawFd; /// Call the link function to create a link to a file /// ([posix specification](http://pubs.opengroup.org/onlinepubs/9699919799/functions/linkat.html)). pub fn linkat( olddirfd: RawFd, oldpath: &P1, newdirfd: RawFd, newpath: &P2, flags: AtFlags, ) -> nix::Result<()> { let res = oldpath.with_nix_path(|old| { newpath.with_nix_path(|new| unsafe { libc::linkat( olddirfd, old.as_ptr() as *const libc::c_char, newdirfd, new.as_ptr() as *const libc::c_char, flags.bits(), ) }) })??; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/mknodat.rs000064400000000000000000000012541046102023000167060ustar 00000000000000use nix::errno::Errno; use nix::sys::stat; use std::os::unix::prelude::RawFd; /// Create a special or ordinary file /// ([posix specification](http://pubs.opengroup.org/onlinepubs/9699919799/functions/mknod.html)). #[cfg(not(any(target_os = "ios", target_os = "macos")))] pub fn mknodat( dirfd: &RawFd, path: &P, kind: stat::SFlag, perm: stat::Mode, dev: libc::dev_t, ) -> nix::Result<()> { let res = path.with_nix_path(|cstr| unsafe { libc::mknodat( *dirfd, cstr.as_ptr(), kind.bits() | perm.bits() as libc::mode_t, dev, ) })?; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/mod.rs000064400000000000000000000002731046102023000160300ustar 00000000000000pub mod fchownat; pub mod fstatvfs; pub mod ioctl; pub mod linkat; pub mod mknodat; pub mod prctl; pub mod readlinkat; pub mod renameat2; pub mod setrlimit; pub mod utime; pub mod xattr; cntr-1.5.3/src/sys_ext/internal/prctl.rs000064400000000000000000000010461046102023000163740ustar 00000000000000use libc::{self, c_int, c_ulong}; use nix::errno::Errno; use nix::Result; /// Apply an operation on a process /// [prctl(2)](http://man7.org/linux/man-pages/man2/prctl.2.html) /// /// prctl is called with a first argument describing what to do, /// further arguments with a significance depending on the first one. pub fn prctl( option: c_int, arg2: c_ulong, arg3: c_ulong, arg4: c_ulong, arg5: c_ulong, ) -> Result<()> { let res = unsafe { libc::prctl(option, arg2, arg3, arg4, arg5) }; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/readlinkat.rs000064400000000000000000000025361046102023000173730ustar 00000000000000use nix::errno::Errno; use nix::NixPath; use std::ffi::{OsStr, OsString}; use std::os::unix::prelude::*; fn readlinkat<'a, P: ?Sized + NixPath>( dirfd: RawFd, path: &P, buffer: &'a mut [u8], ) -> nix::Result<&'a OsStr> { let res = path.with_nix_path(|cstr| unsafe { libc::readlinkat( dirfd, cstr.as_ptr(), buffer.as_mut_ptr() as *mut libc::c_char, buffer.len() as libc::size_t, ) })?; match Errno::result(res) { Err(err) => Err(err), Ok(len) => { if (len as usize) >= buffer.len() { Err(Errno::ENAMETOOLONG) } else { Ok(OsStr::from_bytes(&buffer[..(len as usize)])) } } } } pub fn fuse_readlinkat(fd: RawFd) -> nix::Result { let mut buf = vec![0; (libc::PATH_MAX + 1) as usize]; loop { match readlinkat(fd, "", &mut buf) { Ok(target) => { return Ok(OsString::from(target)); } Err(Errno::ENAMETOOLONG) => {} Err(e) => return Err(e), }; // Trigger the internal buffer resizing logic of `Vec` by requiring // more space than the current capacity. The length is guaranteed to be // the same as the capacity due to the if statement above. buf.reserve(1) } } cntr-1.5.3/src/sys_ext/internal/renameat2.rs000064400000000000000000000012401046102023000171220ustar 00000000000000use nix::errno::Errno; use std::os::unix::prelude::RawFd; #[cfg(any(target_os = "android", target_os = "linux"))] pub fn renameat2( olddirfd: RawFd, oldpath: &P1, newdirfd: RawFd, newpath: &P2, flags: libc::c_uint, ) -> nix::Result<()> { let res = oldpath.with_nix_path(|old| { newpath.with_nix_path(|new| unsafe { libc::syscall( libc::SYS_renameat2, olddirfd, old.as_ptr(), newdirfd, new.as_ptr(), flags, ) }) })??; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/setrlimit.rs000064400000000000000000000010021046102023000172540ustar 00000000000000pub use libc::rlimit64 as Rlimit; use nix::errno::Errno; #[cfg(any(target_env = "gnu"))] pub fn setrlimit(resource: libc::c_uint, rlimit: &Rlimit) -> nix::Result<()> { let res = unsafe { libc::setrlimit64(resource, rlimit as *const Rlimit) }; Errno::result(res).map(drop) } #[cfg(not(any(target_env = "gnu")))] pub fn setrlimit(resource: libc::c_int, rlimit: &Rlimit) -> nix::Result<()> { let res = unsafe { libc::setrlimit64(resource, rlimit as *const Rlimit) }; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/utime.rs000064400000000000000000000034401046102023000163730ustar 00000000000000use nix::errno::Errno; use nix::fcntl::AtFlags; use nix::sys::time::TimeSpec; use std::os::unix::io::RawFd; /// A file timestamp. #[derive(Clone, Copy, Debug)] pub enum UtimeSpec { /// File timestamp is set to the current time. Now, /// The corresponding file timestamp is left unchanged. Omit, /// File timestamp is set to value Time(TimeSpec), } impl<'a> From<&'a UtimeSpec> for libc::timespec { fn from(time: &'a UtimeSpec) -> libc::timespec { match time { UtimeSpec::Now => libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_NOW, }, UtimeSpec::Omit => libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_OMIT, }, UtimeSpec::Time(spec) => *spec.as_ref(), } } } /// Change file timestamps with nanosecond precision /// (see [utimensat(2)](http://man7.org/linux/man-pages/man2/utimensat.2.html)). pub fn utimensat( dirfd: RawFd, pathname: &P, atime: &UtimeSpec, mtime: &UtimeSpec, flags: AtFlags, ) -> nix::Result<()> { let time = [atime.into(), mtime.into()]; let res = pathname.with_nix_path(|cstr| unsafe { libc::utimensat( dirfd, cstr.as_ptr(), time.as_ptr() as *const libc::timespec, flags.bits(), ) })?; Errno::result(res).map(drop) } /// Change file timestamps with nanosecond precision /// (see [futimens(2)](http://man7.org/linux/man-pages/man2/futimens.2.html)). pub fn futimens(fd: RawFd, atime: &UtimeSpec, mtime: &UtimeSpec) -> nix::Result<()> { let time = [atime.into(), mtime.into()]; let res = unsafe { libc::futimens(fd, time.as_ptr() as *const libc::timespec) }; Errno::result(res).map(drop) } cntr-1.5.3/src/sys_ext/internal/xattr.rs000064400000000000000000000112101046102023000164040ustar 00000000000000use super::readlinkat::fuse_readlinkat; use cntr_fuse::FileType; use libc::c_int; use nix::errno::Errno; use nix::NixPath; use nix::Result; use std::ffi::OsStr; use crate::files::Fd; fn getxattr( path: &P1, name: &P2, buf: &mut [u8], ) -> Result { let res = unsafe { path.with_nix_path(|p| { name.with_nix_path(|n| { libc::getxattr( p.as_ptr(), n.as_ptr(), buf.as_mut_ptr() as *mut libc::c_void, buf.len(), ) }) }) }??; Errno::result(res).map(|size| size as usize) } fn lgetxattr( path: &P1, name: &P2, buf: &mut [u8], ) -> Result { let res = unsafe { path.with_nix_path(|p| { name.with_nix_path(|n| { libc::lgetxattr( p.as_ptr(), n.as_ptr(), buf.as_mut_ptr() as *mut libc::c_void, buf.len(), ) }) }) }??; Errno::result(res).map(|size| size as usize) } fn listxattr(path: &P, list: &mut [u8]) -> Result { let res = unsafe { path.with_nix_path(|cstr| { libc::listxattr( cstr.as_ptr(), list.as_mut_ptr() as *mut libc::c_char, list.len(), ) }) }?; Errno::result(res).map(|size| size as usize) } fn llistxattr(path: &P, list: &mut [u8]) -> Result { let res = unsafe { path.with_nix_path(|cstr| { libc::llistxattr( cstr.as_ptr(), list.as_mut_ptr() as *mut libc::c_char, list.len(), ) }) }?; Errno::result(res).map(|size| size as usize) } fn lsetxattr( path: &P1, name: &P2, buf: &[u8], flags: c_int, ) -> Result<()> { let res = unsafe { path.with_nix_path(|p| { name.with_nix_path(|n| { libc::lsetxattr( p.as_ptr(), n.as_ptr(), buf.as_ptr() as *const libc::c_void, buf.len(), flags, ) }) }) }??; Errno::result(res).map(drop) } pub fn setxattr( path: &P1, name: &P2, buf: &[u8], flags: c_int, ) -> Result<()> { let res = unsafe { path.with_nix_path(|p| { name.with_nix_path(|n| { libc::setxattr( p.as_ptr(), n.as_ptr(), buf.as_ptr() as *const libc::c_void, buf.len(), flags, ) }) }) }??; Errno::result(res).map(drop) } fn removexattr(path: &P1, name: &P2) -> Result<()> { let res = unsafe { path.with_nix_path(|p| name.with_nix_path(|n| libc::removexattr(p.as_ptr(), n.as_ptr()))) }??; Errno::result(res).map(drop) } fn lremovexattr(path: &P1, name: &P2) -> Result<()> { let res = unsafe { path.with_nix_path(|p| name.with_nix_path(|n| libc::lremovexattr(p.as_ptr(), n.as_ptr()))) }??; Errno::result(res).map(drop) } pub fn fuse_setxattr( fd: &Fd, kind: FileType, name: &OsStr, value: &[u8], flags: u32, ) -> Result<()> { if kind == FileType::Symlink { let path = fuse_readlinkat(fd.raw())?; lsetxattr(path.as_os_str(), name, value, flags as i32) } else { setxattr(fd.path().as_str(), name, value, flags as i32) } } pub fn fuse_removexattr(fd: &Fd, kind: FileType, name: &OsStr) -> Result<()> { if kind == FileType::Symlink { let path = fuse_readlinkat(fd.raw())?; lremovexattr(path.as_os_str(), name) } else { removexattr(fd.path().as_str(), name) } } pub fn fuse_listxattr(fd: &Fd, kind: FileType, name: &mut [u8]) -> Result { if kind == FileType::Symlink { let path = fuse_readlinkat(fd.raw())?; llistxattr(path.as_os_str(), name) } else { listxattr(fd.path().as_str(), name) } } pub fn fuse_getxattr(fd: &Fd, kind: FileType, name: &OsStr, buf: &mut [u8]) -> Result { if kind == FileType::Symlink { let path = fuse_readlinkat(fd.raw())?; lgetxattr(path.as_os_str(), name, buf) } else { getxattr(fd.path().as_str(), name, buf) } } cntr-1.5.3/src/sys_ext/mod.rs000064400000000000000000000011561046102023000142150ustar 00000000000000mod internal; pub use self::internal::fchownat::fchownat; pub use self::internal::fstatvfs::fstatvfs; pub use self::internal::ioctl::{ioctl, ioctl_read, ioctl_write}; pub use self::internal::linkat::linkat; pub use self::internal::mknodat::mknodat; pub use self::internal::prctl::prctl; pub use self::internal::readlinkat::fuse_readlinkat; pub use self::internal::renameat2::renameat2; pub use self::internal::setrlimit::{setrlimit, Rlimit}; pub use self::internal::utime::{futimens, utimensat, UtimeSpec}; pub use self::internal::xattr::{ fuse_getxattr, fuse_listxattr, fuse_removexattr, fuse_setxattr, setxattr, }; cntr-1.5.3/src/tests.rs000064400000000000000000000016031046102023000130770ustar 00000000000000use simple_error::{bail,try_with}; use fs::Cntr; use fuse; use libc; use namespace; use std::io::{Write, Read}; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; const user: &'static namespace::Kind = &namespace::USER; #[test] fn test_mount() { let mut child = Command::new("unshare") .args(&["--user", "--mount", "--", "sh", "-c", "cat"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .unwrap(); let pid = child.id() as libc::pid_t; // synchronize with cat let mut buf = [b't']; let mut stdin = child.stdin.unwrap(); { stdin.write(&buf).unwrap(); child.stdout.unwrap().read_exact(&mut buf).unwrap(); let ns = user.open(pid).unwrap(); ns.apply().unwrap(); } let buf = PathBuf::from("mnt"); unsafe { fuse::spawn_mount(Cntr::new(), &buf, &[]); } } cntr-1.5.3/src/tmp.rs000064400000000000000000000026141046102023000125400ustar 00000000000000use nix::errno::Errno; use simple_error::{bail, try_with}; use std::env; use std::ffi::OsString; use std::fs; use std::os::unix::ffi::OsStringExt; use std::path::{Path, PathBuf}; use crate::files::mkdir_p; use crate::result::Result; pub struct TempDir { name: Option, } impl TempDir { pub fn path(&self) -> &Path { self.name.as_ref().unwrap() } pub fn into_path(mut self) -> PathBuf { self.name.take().unwrap() } } pub fn tempdir() -> Result { let mut template = env::temp_dir(); if !template.exists() { template = PathBuf::from("/dev/shm"); if !template.exists() { template = PathBuf::from("/tmp"); if !template.exists() { try_with!(mkdir_p(&template), "mkdir failed"); } } } template.push("cntr.XXXXXX"); let mut bytes = template.into_os_string().into_vec(); // null byte bytes.push(0); let res = unsafe { libc::mkdtemp(bytes.as_mut_ptr().cast()) }; if res.is_null() { bail!("mkdtemp failed with {}", Errno::last()); } else { // remove null byte bytes.pop(); let name = PathBuf::from(OsString::from_vec(bytes)); Ok(TempDir { name: Some(name) }) } } impl Drop for TempDir { fn drop(&mut self) { if let Some(ref p) = self.name { let _ = fs::remove_dir_all(p); } } } cntr-1.5.3/src/trace.rs000064400000000000000000000033471046102023000130420ustar 00000000000000use simple_error::{bail,try_with}; use libc; use nix; use nix::errno; use nix::sys::ptrace::*; use nix::sys::ptrace::ptrace::*; use nix::sys::wait::{WaitStatus, wait, waitpid}; use sigstr; use std::ptr; use types::{Result}; pub fn install(pid: libc::pid_t) -> Result<()> { let status = try_with!(waitpid(pid, None), "process died prematurely"); match status { WaitStatus::Exited(_, rc) => { bail!("process exited prematurely with {}", rc); } WaitStatus::Signaled(_, signal, _) => { bail!( "process was terminated with signal {:}", sigstr::Signal { n: signal } ); } WaitStatus::Continued(_) => { bail!("BUG: process was continued by someone"); } WaitStatus::StillAlive => { bail!("process should be stopped"); } WaitStatus::Stopped(_, _) => {} } let opts = PTRACE_O_TRACESECCOMP | PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXEC | PTRACE_O_TRACEVFORKDONE | PTRACE_O_TRACEEXIT; try_with!(ptrace_setoptions(pid, opts), "failed to ptrace process"); try_with!( ptrace(PTRACE_CONT, pid, ptr::null_mut(), ptr::null_mut()), "failed to resume tracee" ); Ok(()) } pub fn me() -> nix::Result { ptrace(PTRACE_TRACEME, 0, ptr::null_mut(), ptr::null_mut()) } pub fn dispatch() -> Result<()> { loop { match wait() { Err(nix::Error::Sys(errno::ECHILD)) => return Ok(()), Ok(WaitStatus::Stopped(pid, _)) => { ptrace(PTRACE_CONT, pid, ptr::null_mut(), ptr::null_mut()); } _ => {} }; } } cntr-1.5.3/src/user_namespace.rs000064400000000000000000000071421046102023000147330ustar 00000000000000use nix::unistd::Pid; use simple_error::try_with; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use crate::procfs; use crate::result::Result; #[derive(Clone, Copy, Debug)] struct Extent { first: u32, lower_first: u32, count: u32, } #[derive(Clone, Copy, Debug)] enum Kind { UidMap, GidMap, } #[derive(Clone, Copy)] pub struct IdMap { nr_extents: usize, extent: [Extent; 5], // 5 == UID_GID_MAP_MAX_EXTENTS } impl From for &'static str { fn from(k: Kind) -> &'static str { match k { Kind::UidMap => "uid_map", Kind::GidMap => "gid_map", } } } impl From<&'static str> for Kind { fn from(s: &'static str) -> Kind { match s { "uid_map" => Kind::UidMap, _ => Kind::GidMap, } } } const DEFAULT_EXTENT: Extent = Extent { first: 0, lower_first: 0, count: 4_294_967_295, }; pub const DEFAULT_ID_MAP: IdMap = IdMap { nr_extents: 1, extent: [DEFAULT_EXTENT; 5], }; impl IdMap { fn _new_from_pid(pid: Pid, kind: Kind) -> Result { let what: &str = kind.into(); let path = procfs::get_path().join(pid.to_string()).join(what); let f = try_with!(File::open(&path), "failed to open {}", path.display()); let buf_reader = BufReader::new(f); let mut id_map = IdMap { nr_extents: 0, extent: [DEFAULT_EXTENT; 5], }; for line in buf_reader.lines() { let line = try_with!(line, "failed to read {}", path.display()); let cols: Vec<&str> = line.split_whitespace().collect(); assert!(cols.len() == 3); assert!(id_map.nr_extents < id_map.extent.len()); id_map.extent[id_map.nr_extents] = Extent { first: try_with!( cols[0].parse::(), "invalid id value in {}: {}", what, line ), lower_first: try_with!( cols[1].parse::(), "invalid id value in {}: {}", what, line ), count: try_with!( cols[2].parse::(), "invalid id value in {}: {}", what, line ), }; id_map.nr_extents += 1; } Ok(id_map) } pub fn new_from_pid(pid: Pid) -> Result<(IdMap, IdMap)> { let uid_map = try_with!( IdMap::_new_from_pid(pid, Kind::UidMap), "failed to read uid_map" ); let gid_map = try_with!( IdMap::_new_from_pid(pid, Kind::GidMap), "failed to read uid_map" ); Ok((uid_map, gid_map)) } pub fn map_id_down(&self, id: u32) -> u32 { for idx in 0..self.nr_extents { let first = self.extent[idx].first; let last = first + self.extent[idx].count - 1; if id >= first && id <= last { return id - first + self.extent[idx].lower_first; } } // FIXME: should be replaced by overflowgid/overflowuid 65_534 } pub fn map_id_up(&self, id: u32) -> u32 { for idx in 0..self.nr_extents { let first = self.extent[idx].lower_first; let last = first + self.extent[idx].count - 1; if id >= first && id <= last { return id - first + self.extent[idx].first; } } // FIXME: should be replaced by overflowgid/overflowuid 65_534 } } cntr-1.5.3/vm-test.nix000064400000000000000000000024631046102023000127240ustar 00000000000000{ flake ? builtins.getFlake (toString ./.) , pkgs ? flake.inputs.nixpkgs.legacyPackages.${builtins.currentSystem} , makeTest ? pkgs.callPackage (flake.inputs.nixpkgs + "/nixos/tests/make-test-python.nix") , cntr ? flake.defaultPackage.${builtins.currentSystem} }: let makeTest' = test: makeTest test { inherit pkgs; inherit (pkgs) system; }; ociTest = { pkgs, ... }: { virtualisation.oci-containers.containers.nginx = { image = "nginx-container"; imageFile = pkgs.dockerTools.examples.nginx; ports = ["8181:80"]; }; environment.systemPackages = [ cntr ]; }; in { docker = makeTest' { name = "docker"; nodes.server = { ... }: { imports = [ ociTest ]; virtualisation.oci-containers.backend = "docker"; }; testScript = '' start_all() server.wait_for_unit("docker-nginx.service") server.wait_for_open_port(8181) server.succeed("cntr attach nginx true") ''; }; podman = makeTest' { name = "podman"; nodes.server = { ... }: { imports = [ ociTest ]; virtualisation.oci-containers.backend = "podman"; }; testScript = '' start_all() server.wait_for_unit("podman-nginx.service") server.wait_for_open_port(8181) server.succeed("cntr attach nginx true") ''; }; }