diff --git a/.dockerignore b/.dockerignore index ce1b641..fc71d7d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ * -!CMakeLists.txt -!setcap.c -!config.h.in +!src +!Cargo.toml +!Cargo.lock \ No newline at end of file diff --git a/.gitignore b/.gitignore index 00a6291..b6a8ccb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,208 @@ -/.vscode -/build +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,windows,linux,macos,rust,rust-analyzer +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,intellij+all,windows,linux,macos,rust,rust-analyzer + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### rust-analyzer ### +# Can be generated by other build systems other than cargo (ex: bazelbuild/rust_rules) +rust-project.json + + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,windows,linux,macos,rust,rust-analyzer + +/target \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0144999 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "setcap-static" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libc = { version = "0.2", default-features = false } +libc-print = "0.1.22" + +[profile.dev] +# This isn't required for development builds, but makes development +# build behavior match release builds. To enable unwinding panics +# during development, simply remove this line. +panic = "abort" + +[profile.release] +#opt-level = 'z' # Optimize for size. +lto = true # Enable Link Time Optimization +codegen-units = 1 # Reduce number of codegen units to increase optimizations. +panic = 'abort' # Abort on panic +strip = true # Strip symbols from binary* +debug-assertions = false +debug = false +rpath = false +incremental = false diff --git a/Dockerfile b/Dockerfile index 0defbac..2976b95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,29 @@ -FROM harbor.repository.lb.home.dc.internal.amuz.es/infrastructure/alpine-base:3.19-latest AS build -RUN apk add --no-cache cmake make musl-dev gcc libcap-static libcap-dev -WORKDIR /build -COPY . . -RUN \ - cmake -S . -B build -DCMAKE_BUILD_TYPE=MinSizeRel && \ - cmake --build build --config MinSizeRel && \ - strip build/setcap-static +#syntax=docker/dockerfile:1 + +## +## Build +## +FROM rust:1-alpine3.19 AS build +LABEL org.opencontainers.image.authors="Sangbum Kim " + +# set the workdir and copy the source into it +WORKDIR /app +COPY . /app + +ENV RUSTFLAGS='-C link-arg=-s -C link-arg=-fuse-ld=lld' + +RUN set -x && \ + apk add --no-cache \ + libcap-static \ + libcap-dev \ + lld \ + musl-dev &&\ + cargo build --release && \ + ldd target/release/setcap-static + +# RUN --mount=type=bind,rw,source=.,target=/host \ +# cp -avf target/release/setcap-static /host/setcap-static + FROM scratch -COPY --from=build /build/build/setcap-static /setcap-static +COPY --from=build /app/target/release/setcap-static /setcap-static diff --git a/Dockerfile.debian b/Dockerfile.debian new file mode 100644 index 0000000..e7903e7 --- /dev/null +++ b/Dockerfile.debian @@ -0,0 +1,38 @@ +#syntax=docker/dockerfile:1 + +## +## Build +## +FROM rust:1-slim AS build +LABEL org.opencontainers.image.authors="Sangbum Kim " + +# set the workdir and copy the source into it +WORKDIR /app +COPY . /app + +# ENV RUSTFLAGS='-C link-arg=-s -C linker=rust-lld -C link-arg=-fuse-ld=lld' +# ENV RUSTFLAGS='-C link-arg=-s -C link-arg=-fuse-ld=lld' +# ENV RUSTFLAGS='-C target-feature=+crt-static -C link-arg=-s -C link-args=-nostartfiles -C link-arg=-nostdlib' +# ENV RUSTFLAGS='-C target-feature=+crt-static -C link-arg=-s' +# ENV RUSTFLAGS='-C target-feature=+crt-static -C link-arg=-s' +# ENV RUSTFLAGS='-C target-feature=+crt-static -C link-arg=-static -C link-arg=-s' +# ENV RUSTFLAGS='-C target-feature=+crt-static -C link-arg=-static -C link-arg=-s -C link-arg=-fuse-ld=lld' +ENV RUSTFLAGS='-C target-feature=+crt-static -C link-arg=-s -C link-arg=-fuse-ld=lld' + +# do a release build +RUN set -x && \ + apt update && \ + apt install -y \ + # libcap2 \ + lld \ + libcap-dev \ + &&\ + cargo build --release && \ + ldd target/release/rsetcap + +RUN --mount=type=bind,rw,source=.,target=/host \ + cp -avf target/release/rsetcap /host/rsetcap + + +# FROM scratch +# COPY --from=build /build/build/setcap-static /setcap-static diff --git a/c/.dockerignore b/c/.dockerignore new file mode 100644 index 0000000..cfa9dff --- /dev/null +++ b/c/.dockerignore @@ -0,0 +1,2 @@ +* +!src \ No newline at end of file diff --git a/c/.gitignore b/c/.gitignore new file mode 100644 index 0000000..00a6291 --- /dev/null +++ b/c/.gitignore @@ -0,0 +1,2 @@ +/.vscode +/build diff --git a/c/Dockerfile b/c/Dockerfile new file mode 100644 index 0000000..79c3a98 --- /dev/null +++ b/c/Dockerfile @@ -0,0 +1,16 @@ +FROM harbor.repository.lb.home.dc.internal.amuz.es/infrastructure/alpine-base:3.19-latest AS build +RUN apk add --no-cache cmake make musl-dev gcc libcap-static libcap-dev +WORKDIR /build + +COPY src/ ./ + +RUN \ + cmake -S . -B build -DCMAKE_BUILD_TYPE=MinSizeRel && \ + cmake --build build --config MinSizeRel && \ + strip build/setcap-static + +RUN --mount=type=bind,rw,source=.,target=/host \ + cp -avf build/setcap-static /host/setcap-static + +FROM scratch +COPY --from=build /build/build/setcap-static /setcap-static diff --git a/CMakeLists.txt b/c/src/CMakeLists.txt similarity index 100% rename from CMakeLists.txt rename to c/src/CMakeLists.txt diff --git a/LICENSE b/c/src/LICENSE similarity index 100% rename from LICENSE rename to c/src/LICENSE diff --git a/README.md b/c/src/README.md similarity index 100% rename from README.md rename to c/src/README.md diff --git a/config.h.in b/c/src/config.h.in similarity index 100% rename from config.h.in rename to c/src/config.h.in diff --git a/lgtm.yml b/c/src/lgtm.yml similarity index 100% rename from lgtm.yml rename to c/src/lgtm.yml diff --git a/setcap.c b/c/src/setcap.c similarity index 100% rename from setcap.c rename to c/src/setcap.c diff --git a/setcap-static b/setcap-static new file mode 100755 index 0000000..d1c7ddf Binary files /dev/null and b/setcap-static differ diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..0611624 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,133 @@ +use core::ffi::CStr; +use libc::c_char; + +#[derive(Copy, Clone, Debug)] +pub struct Args { + argc: usize, + argv: *const *const u8, +} + +impl Args { + ///Creates new instance, but verifies that each string inside are UTF-8. + /// + ///On error returns pair: `(string index, Utf8Error)` + /// + ///The function is safe as long as you pass C style main function arguments. + pub unsafe fn new( + argc: isize, + argv: *const *const u8, + ) -> Result { + assert!(argc > 0); + assert!(!argv.is_null()); + + let this = Args { + argc: argc as usize, + argv, + }; + + let args = this.as_slice(); + for idx in 0..this.argc { + let arg = *args.get_unchecked(idx); + if let Err(error) = c_str_to_rust(arg) { + return Err((idx, error)); + } + } + + Ok(this) + } + + #[inline(always)] + ///Unchecked version of `Args::new` + /// + ///Do it on your own risk + pub unsafe fn new_unchecked(argc: isize, argv: *const *const u8) -> Self { + Args { + argc: argc as usize, + argv, + } + } + + #[inline(always)] + ///Returns slice of raw C strings + pub fn as_slice(&self) -> &[*const u8] { + unsafe { core::slice::from_raw_parts(self.argv, self.argc) } + } + + #[inline(always)] + ///Retrieves string by index. + /// + ///No checks, 100% unsafe. + pub unsafe fn get_str_by_index(&self, index: usize) -> &str { + let elem = *self.as_slice().get_unchecked(index); + c_str_to_rust_unchecked(elem) + } + + pub unsafe fn get_cstr_by_index(&self, index: usize) -> &CStr { + let elem = *self.as_slice().get_unchecked(index); + CStr::from_ptr(elem as *const c_char) + } +} + +impl<'a> IntoIterator for &'a Args { + type Item = &'a str; + type IntoIter = IntoIter<'a>; + + #[inline(always)] + fn into_iter(self) -> Self::IntoIter { + Self::IntoIter { + inner: self, + index: 0, + } + } +} + +///Iterator over [Args](struct.Args.html) +/// +///Comparing to normal iterators can be iterated back and forth. +pub struct IntoIter<'a> { + inner: &'a Args, + index: usize, +} + +impl<'a> Iterator for IntoIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + if self.index >= self.inner.argc { + return None; + } + + let elem = unsafe { self.inner.get_str_by_index(self.index) }; + self.index += 1; + Some(elem) + } + + #[inline(always)] + fn size_hint(&self) -> (usize, Option) { + let count = self.inner.argc - self.index; + (count, Some(count)) + } + + #[inline(always)] + fn count(self) -> usize { + self.inner.argc - self.index + } +} + +///Converts C string to Rust's, verifying it is UTF-8 +/// +///It is UB to pass non-C string as it requires \0 +unsafe fn c_str_to_rust(ptr: *const u8) -> Result<&'static str, core::str::Utf8Error> { + let len = libc::strlen(ptr as *const i8); + let parts = core::slice::from_raw_parts(ptr, len); + core::str::from_utf8(parts) +} + +///Converts C string to Rust's one assuming it is UTF-8 +/// +///It is UB to pass non-C string as it requires \0 +unsafe fn c_str_to_rust_unchecked(ptr: *const u8) -> &'static str { + let len = libc::strlen(ptr as *const i8); + let parts = core::slice::from_raw_parts(ptr, len); + core::str::from_utf8_unchecked(parts) +} diff --git a/src/caps.rs b/src/caps.rs new file mode 100644 index 0000000..1702bd1 --- /dev/null +++ b/src/caps.rs @@ -0,0 +1,129 @@ +#![allow( + dead_code, + non_snake_case, + non_camel_case_types, + non_upper_case_globals +)] + +use libc::{c_char, c_int, c_uint, c_void, uid_t}; + +#[repr(i32)] +pub(crate) enum Value { + CAP_CHOWN, + CAP_DAC_OVERRIDE, + CAP_DAC_READ_SEARCH, + CAP_FOWNER, + CAP_FSETID, + CAP_KILL, + CAP_SETGID, + CAP_SETUID, + CAP_SETPCAP, + CAP_LINUX_IMMUTABLE, + CAP_NET_BIND_SERVICE, + CAP_NET_BROADCAST, + CAP_NET_ADMIN, + CAP_NET_RAW, + CAP_IPC_LOCK, + CAP_IPC_OWNER, + CAP_SYS_MODULE, + CAP_SYS_RAWIO, + CAP_SYS_CHROOT, + CAP_SYS_PTRACE, + CAP_SYS_PACCT, + CAP_SYS_ADMIN, + CAP_SYS_BOOT, + CAP_SYS_NICE, + CAP_SYS_RESOURCE, + CAP_SYS_TIME, + CAP_SYS_TTY_CONFIG, + CAP_MKNOD, + CAP_LEASE, + CAP_AUDIT_WRITE, + CAP_AUDIT_CONTROL, + CAP_SETFCAP, + CAP_MAC_OVERRIDE, + CAP_MAC_ADMIN, + CAP_SYSLOG, + CAP_WAKE_ALARM, + CAP_BLOCK_SUSPEND, + CAP_AUDIT_READ, + CAP_PERFMON, + CAP_BPF, + CAP_CHECKPOINT_RESTORE, +} + +impl Into for Value { + fn into(self) -> cap_value_t { + self as cap_value_t + } +} + +#[repr(u32)] +pub(crate) enum Flag { + CAP_EFFECTIVE, + CAP_PERMITTED, + CAP_INHERITABLE, +} + +impl Into for Flag { + fn into(self) -> cap_flag_t { + self as cap_flag_t + } +} + +#[repr(u32)] +pub(crate) enum FlagValue { + CAP_CLEAR, + CAP_SET, +} + +impl Into for FlagValue { + fn into(self) -> cap_flag_value_t { + self as cap_flag_value_t + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct cap_t { + pub __user_cap_header_struct: cap_user_header_t, + pub __user_cap_data_struct: [cap_user_data_t; 2usize], +} + +#[repr(C)] +#[derive(Copy, Clone)] + +pub(crate) struct cap_user_header_t { + pub version: u32, + pub pid: ::libc::c_int, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub(crate) struct cap_user_data_t { + pub effective: c_uint, + pub permitted: c_uint, + pub inheritable: c_uint, +} + +pub(crate) type cap_flag_t = c_uint; +pub(crate) type cap_value_t = c_int; +pub(crate) type cap_flag_value_t = c_uint; + +// #[link(name = "cap", kind = "dylib")] +#[link(name = "cap", kind = "static")] +extern "C" { + pub(crate) fn cap_get_proc() -> *mut cap_t; + pub(crate) fn cap_from_text(arg1: *const c_char) -> *mut cap_t; + pub(crate) fn cap_set_nsowner(target_caps: *mut cap_t, uid: uid_t) -> c_int; + pub(crate) fn cap_set_flag( + arg1: *mut cap_t, + arg2: cap_flag_t, + arg3: ::libc::c_int, + arg4: *const cap_value_t, + arg5: cap_flag_value_t, + ) -> ::libc::c_int; + pub(crate) fn cap_set_proc(arg1: *const cap_t) -> c_int; + pub(crate) fn cap_set_file(arg1: *const c_char, arg2: *const cap_t) -> c_int; + pub(crate) fn cap_free(arg1: *mut c_void) -> c_int; +} diff --git a/src/link.rs b/src/link.rs new file mode 100644 index 0000000..864bcd5 --- /dev/null +++ b/src/link.rs @@ -0,0 +1,17 @@ +#![allow(non_camel_case_types, dead_code)] + +use libc::FILE; + +#[link(name = "c")] +extern "C" { + static stdin: *mut FILE; + static stdout: *mut FILE; + static stderr: *mut FILE; +} + +#[panic_handler] +#[inline(never)] +#[cfg(not(test))] +unsafe fn panic_handler(_: &core::panic::PanicInfo) -> ! { + libc::exit(2) +} diff --git a/src/link_gcc.rs b/src/link_gcc.rs new file mode 100644 index 0000000..ab7fa5b --- /dev/null +++ b/src/link_gcc.rs @@ -0,0 +1,26 @@ +#![allow(non_camel_case_types, dead_code)] + +use libc::{c_int, exit, FILE}; + +#[repr(C, align(16))] +struct f128 { + a: [u8; 16], +} + +#[no_mangle] +extern "C" fn __letf2(_a: f128, _b: f128) -> c_int { + 0 +} + +#[no_mangle] +extern "C" fn __unordtf2(_a: f128, _b: f128) -> c_int { + 0 +} + +#[no_mangle] +unsafe extern "C" fn _Unwind_Resume() -> ! { + exit(2) +} + +#[no_mangle] +extern "C" fn __gcc_personality_v0() {} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ca96d08 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,104 @@ +#![no_std] +#![no_main] + +mod args; +mod caps; +mod link; + +use args::Args; +use libc::{c_int, c_void, unlink, EXIT_FAILURE, EXIT_SUCCESS}; +use libc_print::std_name::eprintln; + +#[no_mangle] +unsafe extern "C" fn main(argc: c_int, argv: *const *const u8) -> c_int { + match argc { + 3 => (), + 1 => return EXIT_SUCCESS, + _ => { + eprintln!("Usage: setcap capabilities filename"); + return EXIT_FAILURE; + } + } + + let arg = match Args::new(argc as isize, argv) { + Ok(arg) => arg, + Err((valid_up_to, err)) => { + match err.error_len() { + Some(err_len) => eprintln!( + "invalid utf-8 sequence of {} bytes from index {}", + err_len, valid_up_to + ), + None => eprintln!("incomplete utf-8 byte sequence from index {}", valid_up_to), + }; + return EXIT_FAILURE; + } + }; + + let my_caps = caps::cap_get_proc(); + + if my_caps.is_null() { + eprintln!("cap_get_proc"); + return EXIT_FAILURE; + } + + let target_caps = caps::cap_from_text(arg.get_cstr_by_index(1).as_ptr()); + + if target_caps.is_null() { + eprintln!( + "cap_from_text: failed to parse \"{}\"", + arg.get_str_by_index(1), + ); + caps::cap_free(my_caps as *mut c_void); + return EXIT_FAILURE; + } + + let ret = caps::cap_set_nsowner(target_caps, 0); + if ret != 0 { + eprintln!("cap_set_nsowner: {}", ret); + caps::cap_free(my_caps as *mut c_void); + caps::cap_free(target_caps as *mut c_void); + return EXIT_FAILURE; + } + + let flag: caps::cap_value_t = caps::Value::CAP_SETFCAP.into(); + let ret = caps::cap_set_flag( + my_caps, + caps::Flag::CAP_EFFECTIVE.into(), + 1, + &flag, + caps::FlagValue::CAP_SET.into(), + ); + + if ret != 0 { + eprintln!("cap_set_flag(CAP_SETFCAP): {}", ret); + caps::cap_free(my_caps as *mut c_void); + caps::cap_free(target_caps as *mut c_void); + return EXIT_FAILURE; + } + + let ret = caps::cap_set_proc(my_caps); + if ret != 0 { + eprintln!("cap_set_proc: {}", ret); + caps::cap_free(my_caps as *mut c_void); + caps::cap_free(target_caps as *mut c_void); + return EXIT_FAILURE; + } + + caps::cap_free(my_caps as *mut c_void); + + let ret = caps::cap_set_file(arg.get_cstr_by_index(2).as_ptr(), target_caps); + if ret != 0 { + eprintln!("cap_set_file: {}", ret); + caps::cap_free(target_caps as *mut c_void); + return EXIT_FAILURE; + } + + caps::cap_free(target_caps as *mut c_void); + + _ = match arg.get_cstr_by_index(0).to_bytes() { + [b'/', b'!', ..] => unlink(arg.get_cstr_by_index(0).as_ptr()), + _ => 0, + }; + + EXIT_SUCCESS +}