Commit 2fe29ff8 authored by jan.koester's avatar jan.koester
Browse files

test

parent d08c1ab7
Loading
Loading
Loading
Loading
+13 −1
Changes for commands/auth/CMakeLists.txt: 13 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -16,4 +16,16 @@ if(HAVE_LIBCRYPT)
  target_link_libraries(fresh crypt)
endif()

set(FRESH_APPLETS ${FRESH_APPLETS} login su passwd getty PARENT_SCOPE)
# su: its own small setuid-root program instead of an applet symlink (see
# suimpl.h); the fresh builtin hands unprivileged callers over to it.
string(REGEX REPLACE "/+$" "" FRESH_PREFIX_NOSLASH "${CMAKE_INSTALL_PREFIX}")
target_compile_definitions(fresh PRIVATE FRESH_SU_PATH="${FRESH_PREFIX_NOSLASH}/bin/su")
add_executable(su ${CMAKE_CURRENT_SOURCE_DIR}/su_main.cpp)
if(HAVE_LIBCRYPT)
  target_link_libraries(su crypt)
endif()
install(TARGETS su DESTINATION bin
        PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE
                    WORLD_READ WORLD_EXECUTE SETUID)

set(FRESH_APPLETS ${FRESH_APPLETS} login passwd getty PARENT_SCOPE)
+20 −85
Changes for commands/auth/su.cpp: 20 added lines, 85 removed lines.
Original line number Diff line number Diff line
@@ -26,109 +26,44 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>

#include <unistd.h>
#include <cstdlib>
#include <grp.h>
#include <sys/wait.h>

#include "shell.h"
#include "authutil.h"
#include "suimpl.h"

namespace fresh { namespace cmd {

    // Whether the calling user may use su at all: only members of group
    // "wheel" (primary gid, supplementary groups, or listed in
    // /etc/group.yaml's members -- login doesn't initgroups(), so the
    // member list is the authoritative one). No wheel group: nobody but
    // root.
    static bool callerInWheel(){
        struct group *gr=getgrnam("wheel");
        if(!gr) return false;
        gid_t wheel=gr->gr_gid;
        auth::PasswdEntry self;
        bool haveSelf=auth::lookupPasswdByUid(getuid(),self);
        if(haveSelf){
            for(char **m=gr->gr_mem;m && *m;++m)
                if(self.name==*m) return true;
            if(self.gid==wheel) return true;
        }
        if(getgid()==wheel || getegid()==wheel) return true;
        gid_t groups[256];
        int n=getgroups(256,groups);
        for(int i=0;i<n;++i)
            if(groups[i]==wheel) return true;
        return false;
    }

    // "su [-|-l|--login] [USERNAME]" (default USERNAME: root). Root
    // switching to any user needs no password (matches real su); anyone
    // else must be in group wheel and is prompted, verified the same way
    // login verifies. "-"/"-l"
    // additionally chdir's to the target's home and resets HOME/USER/
    // LOGNAME/SHELL, i.e. a login shell rather than just a privilege swap
    // that keeps the caller's own environment/cwd.
    // The builtin: as root it switches right here (auth::suMain()); anyone
    // else can't read /etc/shadow.yaml or setuid(), so the setuid-root /bin/su
    // (FRESH_SU_PATH) does it, in a child the shell waits for -- leaving the
    // su'd shell returns to this one.
    class SuCommand : public ShellCommand {
    public:
        SuCommand():ShellCommand("su"){}

        int runCommand(int argc,const char **argv){
            bool loginShell=false;
            std::string target="root";
            for(int i=1;i<argc;++i){
                std::string a=argv[i];
                if(a=="-" || a=="-l" || a=="--login") loginShell=true;
                else target=a;
            }

            if(getuid()!=0 && !callerInWheel()){
                std::cerr << "su: permission denied (not in group wheel)" << std::endl;
                return 1;
            }

            auth::PasswdEntry pe;
            if(!auth::lookupPasswd(target,pe)){
                std::cerr << "su: user " << target << " does not exist" << std::endl;
                return 1;
            }
            if(geteuid()==0)
                return auth::suMain(argc,argv);

            if(geteuid()!=0){
                std::string hash;
                if(!auth::lookupShadowHash(target,hash))
                    hash=pe.passwd;
                if(!hash.empty()){
                    std::string given=auth::readPassword("Password: ");
                    if(!auth::verifyPassword(hash,given)){
                        std::cerr << "su: Authentication failure" << std::endl;
            pid_t pid=fork();
            if(pid<0){
                std::cerr << "su: fork: " << strerror(errno) << std::endl;
                return 1;
            }
            if(pid==0){
                execv(FRESH_SU_PATH,const_cast<char **>(argv));
                std::cerr << "su: exec " << FRESH_SU_PATH << ": " << strerror(errno) << std::endl;
                _exit(127);
            }
            }

            if(setgid(pe.gid)!=0 || setuid(pe.uid)!=0){
                std::cerr << "su: cannot switch to " << target << ": " << strerror(errno) << std::endl;
                return 1;
            }

            std::string shell=pe.shell.empty() ? "/bin/sh" : pe.shell;
            if(loginShell){
                if(!pe.home.empty()) chdir(pe.home.c_str());
                setenv("HOME",pe.home.c_str(),1);
                setenv("USER",pe.name.c_str(),1);
                setenv("LOGNAME",pe.name.c_str(),1);
                setenv("SHELL",shell.c_str(),1);
            }

            size_t slash=shell.find_last_of('/');
            std::string base=(slash==std::string::npos) ? shell : shell.substr(slash+1);
            std::string argv0=(loginShell ? "-" : "")+base;
            char *newArgv[2]={const_cast<char *>(argv0.c_str()),nullptr};

            execv(shell.c_str(),newArgv);
            std::cerr << "su: exec " << shell << ": " << strerror(errno) << std::endl;
            return 1;
            int status=0;
            while(waitpid(pid,&status,0)<0)
                if(errno!=EINTR) return 1;
            if(WIFEXITED(status)) return WEXITSTATUS(status);
            return 128+WTERMSIG(status);
        }
    };

+36 −0
Changes for commands/auth/su_main.cpp: 36 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

// /bin/su: the setuid-root standalone su (see suimpl.h). Not a symlink to
// fresh like the other applets -- only this small program is setuid, never
// the whole shell.

#include "suimpl.h"

int main(int argc,char **argv){
    return fresh::auth::suMain(argc,(const char **)argv);
}

commands/auth/suimpl.h

0 → 100644
+139 −0
Changes for commands/auth/suimpl.h: 139 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

// su itself, shared by the fresh builtin and the standalone setuid-root
// /bin/su (su_main.cpp). Only the latter can actually switch users for an
// unprivileged caller: reading /etc/shadow.yaml and setuid() need root, so
// the builtin hands non-root callers over to it (see su.cpp). Decisions are
// made on the real uid -- in the setuid binary the effective one is always
// 0 -- and systempp ignores its SYSTEMPP_*_YAML overrides there
// (secure_getenv()), so a caller can't point it at a shadow file of their own.

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>

#include <unistd.h>
#include <grp.h>

#include "authutil.h"

namespace fresh { namespace auth {

    // Whether the calling user may use su at all: only members of group
    // "wheel" (primary gid, supplementary groups, or listed in
    // /etc/group.yaml's members -- login doesn't initgroups(), so the
    // member list is the authoritative one). No wheel group: nobody but
    // root.
    inline bool callerInWheel(){
        struct group *gr=getgrnam("wheel");
        if(!gr) return false;
        gid_t wheel=gr->gr_gid;
        PasswdEntry self;
        bool haveSelf=lookupPasswdByUid(getuid(),self);
        if(haveSelf){
            for(char **m=gr->gr_mem;m && *m;++m)
                if(self.name==*m) return true;
            if(self.gid==wheel) return true;
        }
        if(getgid()==wheel || getegid()==wheel) return true;
        gid_t groups[256];
        int n=getgroups(256,groups);
        for(int i=0;i<n;++i)
            if(groups[i]==wheel) return true;
        return false;
    }

    // "su [-|-l|--login] [USERNAME]" (default USERNAME: root). Root
    // switching to any user needs no password (matches real su); anyone
    // else must be in group wheel and is prompted, verified the same way
    // login verifies. "-"/"-l" additionally chdir's to the target's home
    // and resets HOME/USER/LOGNAME/SHELL, i.e. a login shell rather than
    // just a privilege swap that keeps the caller's own environment/cwd.
    inline int suMain(int argc,const char **argv){
        bool loginShell=false;
        std::string target="root";
        for(int i=1;i<argc;++i){
            std::string a=argv[i];
            if(a=="-" || a=="-l" || a=="--login") loginShell=true;
            else target=a;
        }

        if(getuid()!=0 && !callerInWheel()){
            std::cerr << "su: permission denied (not in group wheel)" << std::endl;
            return 1;
        }

        PasswdEntry pe;
        if(!lookupPasswd(target,pe)){
            std::cerr << "su: user " << target << " does not exist" << std::endl;
            return 1;
        }

        if(getuid()!=0){
            std::string hash;
            if(!lookupShadowHash(target,hash))
                hash=pe.passwd;
            if(!hash.empty()){
                std::string given=readPassword("Password: ");
                if(!verifyPassword(hash,given)){
                    std::cerr << "su: Authentication failure" << std::endl;
                    return 1;
                }
            }
        }

        // the target's own groups, not the caller's supplementary ones
        if(initgroups(pe.name.c_str(),pe.gid)!=0 || setgid(pe.gid)!=0 || setuid(pe.uid)!=0){
            std::cerr << "su: cannot switch to " << target << ": " << strerror(errno) << std::endl;
            return 1;
        }

        std::string shell=pe.shell.empty() ? "/bin/sh" : pe.shell;
        if(loginShell){
            if(!pe.home.empty()) chdir(pe.home.c_str());
            setenv("HOME",pe.home.c_str(),1);
            setenv("USER",pe.name.c_str(),1);
            setenv("LOGNAME",pe.name.c_str(),1);
            setenv("SHELL",shell.c_str(),1);
        }

        size_t slash=shell.find_last_of('/');
        std::string base=(slash==std::string::npos) ? shell : shell.substr(slash+1);
        std::string argv0=(loginShell ? "-" : "")+base;
        char *newArgv[2]={const_cast<char *>(argv0.c_str()),nullptr};

        execv(shell.c_str(),newArgv);
        std::cerr << "su: exec " << shell << ": " << strerror(errno) << std::endl;
        return 1;
    }

}}