Loading commands/auth/authutil.h +17 −33 Changes for commands/auth/authutil.h: 17 added lines, 33 removed lines. Original line number Diff line number Diff line Loading @@ -32,7 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // difference. // // Goes through the real libc account database (getpwnam()/getpwuid(), // backed by /etc/user.yaml; getspnam()/shadow_update_hash(), backed by // backed by /etc/user.yaml; getspnam()/shadow_set_password(), backed by // /etc/shadow.yaml) instead of parsing /etc/passwd and /etc/shadow itself -- // one real account database for every consumer (whoami, id, ls -l's owner // names, ...), not a second, independently-parsed copy of it just for these Loading Loading @@ -105,30 +105,6 @@ namespace fresh { namespace auth { return result && hash==result; } // $6$ (SHA-512) salt: 16 chars from crypt's own base64-like alphabet, // read from /dev/urandom. Falls back to a fixed (still functional, just // not cryptographically random) salt if /dev/urandom can't be opened, // which should only ever happen if the caller isn't actually on Linux. inline std::string makeSalt(){ static const char alphabet[]= "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; unsigned char raw[16]; std::FILE *f=std::fopen("/dev/urandom","rb"); if(f){ if(std::fread(raw,1,sizeof(raw),f)!=sizeof(raw)) { /* best-effort */ } std::fclose(f); } else std::memset(raw,0x5a,sizeof(raw)); std::string salt="$6$"; for(unsigned char b: raw) salt+=alphabet[b % 64]; salt+="$"; return salt; } inline std::string hashPassword(const std::string &plaintext){ std::string salt=makeSalt(); char *result=crypt(plaintext.c_str(),salt.c_str()); return result ? std::string(result) : std::string(); } // Prompts on stdout, reads one line from stdin with terminal echo // disabled (restored afterward regardless of how reading ends). inline std::string readPassword(const std::string &prompt){ Loading @@ -153,14 +129,22 @@ namespace fresh { namespace auth { return pass; } // Real, atomic (temp file + rename, done inside shadow_update_hash() // itself) update of one user's /etc/shadow.yaml passwd field. Caller // must already have permission to write /etc/shadow.yaml (this system // has no setuid bit machinery -- passwd is expected to run as root // already, e.g. from an install/provisioning step or a root shell, not // as an unprivileged self-service tool). inline bool rewriteShadowHash(const std::string &username,const std::string &newHash){ return shadow_update_hash(username.c_str(),newHash.c_str())!=0; // Sets a user's password in /etc/shadow.yaml through the libc // (shadow_set_password(): atomic temp file + rename, under lckpwdf()). // A user with SMB access keeps it -- the NT hash next to the password // follows the new one (see <shadow.h>). Caller must already have // permission to write /etc/shadow.yaml (this system has no setuid bit // machinery -- passwd is expected to run as root already, e.g. from an // install/provisioning step or a root shell, not as an unprivileged // self-service tool). inline bool setPassword(const std::string &username,const std::string &password){ if(lckpwdf()!=0) return false; bool ok=shadow_set_password(username.c_str(),password.c_str(),0)==0; int e=errno; ulckpwdf(); errno=e; return ok; } }} commands/auth/passwd.cpp +4 −10 Changes for commands/auth/passwd.cpp: 4 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -39,9 +39,9 @@ namespace fresh { namespace cmd { // "passwd [USERNAME]" (default: the caller's own account). Non-root // callers may only change their own password, and must confirm the // current one first; root can change anyone's without that check. New // password hashed with a fresh random $6$ (SHA-512) salt and written // back into /etc/shadow via a temp-file rename (see authutil.h). // current one first; root can change anyone's without that check. The // libc hashes the new password ($6$, SHA-512, fresh salt) and keeps a // user's SMB access on it (see authutil.h, <shadow.h>). class PasswdCommand : public ShellCommand { public: PasswdCommand():ShellCommand("passwd"){} Loading Loading @@ -94,13 +94,7 @@ namespace fresh { namespace cmd { return 1; } std::string newHash=auth::hashPassword(p1); if(newHash.empty()){ std::cerr << "passwd: password hashing failed" << std::endl; return 1; } if(!auth::rewriteShadowHash(target,newHash)){ if(!auth::setPassword(target,p1)){ std::cerr << "passwd: failed to update /etc/shadow: " << strerror(errno) << std::endl; return 1; } Loading commands/network/ping.cpp +24 −12 Changes for commands/network/ping.cpp: 24 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -30,12 +30,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // Tries the unprivileged Linux "ping socket" (SOCK_DGRAM+IPPROTO_ICMP, // gated by net.ipv4.ping_group_range - the same mechanism /bin/ping itself // uses to work without setuid/CAP_NET_RAW) first, and falls back to a raw // ICMP socket - which does need CAP_NET_RAW - only if that fails. Matching // is done by comparing the reply's source address against the target // address rather than trying to track the kernel's identifier-rewriting // rules across both socket types, since with a single target and a single // sequential request-then-wait loop that is sufficient and avoids relying // on ping-socket kernel internals that aren't part of any stable contract. // ICMP socket - which does need CAP_NET_RAW - only if that fails. A reply // must come from the target and carry the sequence number of the request // waited for (so a late reply isn't counted for the next one); on a raw // socket, which sees every ICMP packet on the host, its identifier must be // ours as well - a ping socket's identifier is chosen by the kernel, which // only delivers that socket's replies anyway. #include <iostream> #include <iomanip> Loading @@ -48,6 +48,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <cerrno> #include <unistd.h> #include <sys/stat.h> #include <signal.h> #include <sys/socket.h> #include <sys/select.h> Loading Loading @@ -123,6 +124,9 @@ namespace fresh { namespace cmd { int gaiErr=getaddrinfo(host.c_str(),nullptr,&hints,&res); if(gaiErr!=0){ std::cerr << "ping: " << host << ": " << gai_strerror(gaiErr) << std::endl; struct stat st; if(gaiErr!=EAI_SYSTEM && stat("/etc/resolv.yaml",&st)!=0) std::cerr << "ping: no nameserver configured (/etc/resolv.yaml is missing)" << std::endl; return 1; } struct sockaddr_in target; Loading Loading @@ -162,8 +166,8 @@ namespace fresh { namespace cmd { struct icmphdr *icmp=(struct icmphdr*)packet.data(); icmp->type=ICMP_ECHO; icmp->code=0; icmp->un.echo.id=pid; icmp->un.echo.sequence=seq; icmp->un.echo.id=htons((unsigned short)pid); icmp->un.echo.sequence=htons((unsigned short)seq); for(int i=0;i<payloadSize;++i) packet[sizeof(struct icmphdr)+i]=(unsigned char)i; icmp->checksum=0; Loading Loading @@ -197,13 +201,17 @@ namespace fresh { namespace cmd { struct sockaddr_in from; socklen_t fromLen=sizeof(from); ssize_t n=recvfrom(sock,rbuf,sizeof(rbuf),0,(struct sockaddr*)&from,&fromLen); if(n<0) break; if(from.sin_addr.s_addr!=target.sin_addr.s_addr){ { struct timeval now; gettimeofday(&now,nullptr); remaining=timeoutSec-timevalDiffMs(sentTime,now)/1000.0; } if(n<0){ if(errno==EINTR) continue; break; } if(from.sin_addr.s_addr!=target.sin_addr.s_addr) continue; const unsigned char *icmpStart=rbuf; ssize_t icmpLen=n; Loading @@ -220,6 +228,10 @@ namespace fresh { namespace cmd { struct icmphdr *reply=(struct icmphdr*)icmpStart; if(reply->type!=ICMP_ECHOREPLY) continue; if(ntohs(reply->un.echo.sequence)!=(unsigned short)seq) continue; if(usingRawSocket && ntohs(reply->un.echo.id)!=(unsigned short)pid) continue; struct timeval now; gettimeofday(&now,nullptr); Loading @@ -227,7 +239,7 @@ namespace fresh { namespace cmd { if(!quiet) std::cout << icmpLen << " bytes from " << ipbuf << ": icmp_seq=" << reply->un.echo.sequence << ": icmp_seq=" << ntohs(reply->un.echo.sequence) << " time=" << std::fixed << std::setprecision(3) << rtt << " ms" << std::endl; Loading commands/system/CMakeLists.txt +2 −1 Changes for commands/system/CMakeLists.txt: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -30,9 +30,10 @@ target_sources(fresh PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/gzip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xz.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nproc.cpp ${CMAKE_CURRENT_SOURCE_DIR}/dmesg.cpp ${CMAKE_CURRENT_SOURCE_DIR}/sysutils.cpp ) set(FRESH_APPLETS ${FRESH_APPLETS} uname whoami hostname id date sleep env true false clear kill mount umount mknod switch_root cpio mkinitramfs losetup blkid halt reboot poweroff sysctl swapon swapoff pivot_root fsck mkswap hwclock ps tar gzip gunzip zcat xz nproc timeout tty logname PARENT_SCOPE) halt reboot poweroff sysctl swapon swapoff pivot_root fsck mkswap hwclock ps tar gzip gunzip zcat xz nproc dmesg timeout tty logname PARENT_SCOPE) src/shell.cpp +10 −0 Changes for src/shell.cpp: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1141,7 +1141,17 @@ std::vector<fresh::ShellWord> fresh::tokenizeShellWords(const std::string &input if(input[i]=='\n') sawNewline=true; ++i; } // Nor after a pipe or "&&"/"||": a newline there is just a line // break inside the same pipeline/list (POSIX "linebreak"). parse() // joins such lines with a space when it pulls them itself, but a // command substitution's text arrives with the newline still in it // -- e.g. cmake's bootstrap script's // cmake_copyright="`grep '^Copyright .* Kitware' ... | // sed 's/...//'`" // ran "grep ... |" and "sed" as two statements, sed reading the // shell's own stdin (a terminal: the build hung). if(sawNewline && !words.empty() && words.back().text!=";" && !fresh::endsWithBareOperator(words.back().text) && !(i<n && input[i]==';')){ ShellWord sep; sep.text=";"; sep.start=i; sep.end=i; Loading Loading
commands/auth/authutil.h +17 −33 Changes for commands/auth/authutil.h: 17 added lines, 33 removed lines. Original line number Diff line number Diff line Loading @@ -32,7 +32,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // difference. // // Goes through the real libc account database (getpwnam()/getpwuid(), // backed by /etc/user.yaml; getspnam()/shadow_update_hash(), backed by // backed by /etc/user.yaml; getspnam()/shadow_set_password(), backed by // /etc/shadow.yaml) instead of parsing /etc/passwd and /etc/shadow itself -- // one real account database for every consumer (whoami, id, ls -l's owner // names, ...), not a second, independently-parsed copy of it just for these Loading Loading @@ -105,30 +105,6 @@ namespace fresh { namespace auth { return result && hash==result; } // $6$ (SHA-512) salt: 16 chars from crypt's own base64-like alphabet, // read from /dev/urandom. Falls back to a fixed (still functional, just // not cryptographically random) salt if /dev/urandom can't be opened, // which should only ever happen if the caller isn't actually on Linux. inline std::string makeSalt(){ static const char alphabet[]= "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; unsigned char raw[16]; std::FILE *f=std::fopen("/dev/urandom","rb"); if(f){ if(std::fread(raw,1,sizeof(raw),f)!=sizeof(raw)) { /* best-effort */ } std::fclose(f); } else std::memset(raw,0x5a,sizeof(raw)); std::string salt="$6$"; for(unsigned char b: raw) salt+=alphabet[b % 64]; salt+="$"; return salt; } inline std::string hashPassword(const std::string &plaintext){ std::string salt=makeSalt(); char *result=crypt(plaintext.c_str(),salt.c_str()); return result ? std::string(result) : std::string(); } // Prompts on stdout, reads one line from stdin with terminal echo // disabled (restored afterward regardless of how reading ends). inline std::string readPassword(const std::string &prompt){ Loading @@ -153,14 +129,22 @@ namespace fresh { namespace auth { return pass; } // Real, atomic (temp file + rename, done inside shadow_update_hash() // itself) update of one user's /etc/shadow.yaml passwd field. Caller // must already have permission to write /etc/shadow.yaml (this system // has no setuid bit machinery -- passwd is expected to run as root // already, e.g. from an install/provisioning step or a root shell, not // as an unprivileged self-service tool). inline bool rewriteShadowHash(const std::string &username,const std::string &newHash){ return shadow_update_hash(username.c_str(),newHash.c_str())!=0; // Sets a user's password in /etc/shadow.yaml through the libc // (shadow_set_password(): atomic temp file + rename, under lckpwdf()). // A user with SMB access keeps it -- the NT hash next to the password // follows the new one (see <shadow.h>). Caller must already have // permission to write /etc/shadow.yaml (this system has no setuid bit // machinery -- passwd is expected to run as root already, e.g. from an // install/provisioning step or a root shell, not as an unprivileged // self-service tool). inline bool setPassword(const std::string &username,const std::string &password){ if(lckpwdf()!=0) return false; bool ok=shadow_set_password(username.c_str(),password.c_str(),0)==0; int e=errno; ulckpwdf(); errno=e; return ok; } }}
commands/auth/passwd.cpp +4 −10 Changes for commands/auth/passwd.cpp: 4 added lines, 10 removed lines. Original line number Diff line number Diff line Loading @@ -39,9 +39,9 @@ namespace fresh { namespace cmd { // "passwd [USERNAME]" (default: the caller's own account). Non-root // callers may only change their own password, and must confirm the // current one first; root can change anyone's without that check. New // password hashed with a fresh random $6$ (SHA-512) salt and written // back into /etc/shadow via a temp-file rename (see authutil.h). // current one first; root can change anyone's without that check. The // libc hashes the new password ($6$, SHA-512, fresh salt) and keeps a // user's SMB access on it (see authutil.h, <shadow.h>). class PasswdCommand : public ShellCommand { public: PasswdCommand():ShellCommand("passwd"){} Loading Loading @@ -94,13 +94,7 @@ namespace fresh { namespace cmd { return 1; } std::string newHash=auth::hashPassword(p1); if(newHash.empty()){ std::cerr << "passwd: password hashing failed" << std::endl; return 1; } if(!auth::rewriteShadowHash(target,newHash)){ if(!auth::setPassword(target,p1)){ std::cerr << "passwd: failed to update /etc/shadow: " << strerror(errno) << std::endl; return 1; } Loading
commands/network/ping.cpp +24 −12 Changes for commands/network/ping.cpp: 24 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -30,12 +30,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // Tries the unprivileged Linux "ping socket" (SOCK_DGRAM+IPPROTO_ICMP, // gated by net.ipv4.ping_group_range - the same mechanism /bin/ping itself // uses to work without setuid/CAP_NET_RAW) first, and falls back to a raw // ICMP socket - which does need CAP_NET_RAW - only if that fails. Matching // is done by comparing the reply's source address against the target // address rather than trying to track the kernel's identifier-rewriting // rules across both socket types, since with a single target and a single // sequential request-then-wait loop that is sufficient and avoids relying // on ping-socket kernel internals that aren't part of any stable contract. // ICMP socket - which does need CAP_NET_RAW - only if that fails. A reply // must come from the target and carry the sequence number of the request // waited for (so a late reply isn't counted for the next one); on a raw // socket, which sees every ICMP packet on the host, its identifier must be // ours as well - a ping socket's identifier is chosen by the kernel, which // only delivers that socket's replies anyway. #include <iostream> #include <iomanip> Loading @@ -48,6 +48,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <cerrno> #include <unistd.h> #include <sys/stat.h> #include <signal.h> #include <sys/socket.h> #include <sys/select.h> Loading Loading @@ -123,6 +124,9 @@ namespace fresh { namespace cmd { int gaiErr=getaddrinfo(host.c_str(),nullptr,&hints,&res); if(gaiErr!=0){ std::cerr << "ping: " << host << ": " << gai_strerror(gaiErr) << std::endl; struct stat st; if(gaiErr!=EAI_SYSTEM && stat("/etc/resolv.yaml",&st)!=0) std::cerr << "ping: no nameserver configured (/etc/resolv.yaml is missing)" << std::endl; return 1; } struct sockaddr_in target; Loading Loading @@ -162,8 +166,8 @@ namespace fresh { namespace cmd { struct icmphdr *icmp=(struct icmphdr*)packet.data(); icmp->type=ICMP_ECHO; icmp->code=0; icmp->un.echo.id=pid; icmp->un.echo.sequence=seq; icmp->un.echo.id=htons((unsigned short)pid); icmp->un.echo.sequence=htons((unsigned short)seq); for(int i=0;i<payloadSize;++i) packet[sizeof(struct icmphdr)+i]=(unsigned char)i; icmp->checksum=0; Loading Loading @@ -197,13 +201,17 @@ namespace fresh { namespace cmd { struct sockaddr_in from; socklen_t fromLen=sizeof(from); ssize_t n=recvfrom(sock,rbuf,sizeof(rbuf),0,(struct sockaddr*)&from,&fromLen); if(n<0) break; if(from.sin_addr.s_addr!=target.sin_addr.s_addr){ { struct timeval now; gettimeofday(&now,nullptr); remaining=timeoutSec-timevalDiffMs(sentTime,now)/1000.0; } if(n<0){ if(errno==EINTR) continue; break; } if(from.sin_addr.s_addr!=target.sin_addr.s_addr) continue; const unsigned char *icmpStart=rbuf; ssize_t icmpLen=n; Loading @@ -220,6 +228,10 @@ namespace fresh { namespace cmd { struct icmphdr *reply=(struct icmphdr*)icmpStart; if(reply->type!=ICMP_ECHOREPLY) continue; if(ntohs(reply->un.echo.sequence)!=(unsigned short)seq) continue; if(usingRawSocket && ntohs(reply->un.echo.id)!=(unsigned short)pid) continue; struct timeval now; gettimeofday(&now,nullptr); Loading @@ -227,7 +239,7 @@ namespace fresh { namespace cmd { if(!quiet) std::cout << icmpLen << " bytes from " << ipbuf << ": icmp_seq=" << reply->un.echo.sequence << ": icmp_seq=" << ntohs(reply->un.echo.sequence) << " time=" << std::fixed << std::setprecision(3) << rtt << " ms" << std::endl; Loading
commands/system/CMakeLists.txt +2 −1 Changes for commands/system/CMakeLists.txt: 2 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -30,9 +30,10 @@ target_sources(fresh PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/gzip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xz.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nproc.cpp ${CMAKE_CURRENT_SOURCE_DIR}/dmesg.cpp ${CMAKE_CURRENT_SOURCE_DIR}/sysutils.cpp ) set(FRESH_APPLETS ${FRESH_APPLETS} uname whoami hostname id date sleep env true false clear kill mount umount mknod switch_root cpio mkinitramfs losetup blkid halt reboot poweroff sysctl swapon swapoff pivot_root fsck mkswap hwclock ps tar gzip gunzip zcat xz nproc timeout tty logname PARENT_SCOPE) halt reboot poweroff sysctl swapon swapoff pivot_root fsck mkswap hwclock ps tar gzip gunzip zcat xz nproc dmesg timeout tty logname PARENT_SCOPE)
src/shell.cpp +10 −0 Changes for src/shell.cpp: 10 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1141,7 +1141,17 @@ std::vector<fresh::ShellWord> fresh::tokenizeShellWords(const std::string &input if(input[i]=='\n') sawNewline=true; ++i; } // Nor after a pipe or "&&"/"||": a newline there is just a line // break inside the same pipeline/list (POSIX "linebreak"). parse() // joins such lines with a space when it pulls them itself, but a // command substitution's text arrives with the newline still in it // -- e.g. cmake's bootstrap script's // cmake_copyright="`grep '^Copyright .* Kitware' ... | // sed 's/...//'`" // ran "grep ... |" and "sed" as two statements, sed reading the // shell's own stdin (a terminal: the build hung). if(sawNewline && !words.empty() && words.back().text!=";" && !fresh::endsWithBareOperator(words.back().text) && !(i<n && input[i]==';')){ ShellWord sep; sep.text=";"; sep.start=i; sep.end=i; Loading