Mini Shell
# coding=utf-8
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2018 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
# This module provides functions for secure I/O and filesystem operations
import grp
import sys
import tempfile
import contextlib
import os
from functools import lru_cache
from typing import TYPE_CHECKING
from ctypes import (cdll, c_long, Structure, c_ushort, c_ubyte, c_char, POINTER,
c_int, c_void_p, c_char_p)
from clcommon import ClPwd
from clcommon.clpwd import drop_user_privileges
def __getattr__(name):
# NOTE(vlebedev): Delay shared libraries loading until they are actually used.
# It makes module loadable even in case those shared libraries are not available
# and that is useful for e.g. unit testing on local non-CL system.
if name == 'libc':
return _load_libc()
elif name == 'liblve':
return _load_liblve()
raise AttributeError(f"module {__name__} has no attribute {name}")
def __dir__():
return ['libc', 'liblve', *globals().keys()]
# --- libc functions -----------------------------------------------
@lru_cache(maxsize=None)
def _load_libc():
libc = cdll.LoadLibrary("libc.so.6")
libc.fchown.argtypes = [c_int, c_int, c_int]
libc.fchown.restype = c_int
libc.fchmod.argtypes = [c_int, c_int]
libc.fchmod.restype = c_int
# accepts file/dir descriptor (integer)
libc.fdopendir.argtypes = [c_int]
# returns pointer to DIR structure
libc.fdopendir.restype = c_void_p
# accepts pointer to DIR structure
libc.readdir.argtypes = [c_void_p]
# returns pointer to DIRENTRY structure
libc.readdir.restype = DIRENTRY_P
# accepts pointer to DIR structure
libc.rewinddir.argtypes = [c_void_p]
# returns void
libc.rewinddir.restype = None
# accepts pointer to DIR structure
libc.closedir.argtypes = [c_void_p]
libc.closedir.restype = c_int
return libc
if TYPE_CHECKING:
libc = _load_libc()
ino_t = c_long
off_t = c_long
class DIRENTRY(Structure):
_fields_ = [
('d_ino', ino_t), # inode number
('d_off', off_t), # offset to the next dirent
('d_reclen', c_ushort), # length of this record
('d_type', c_ubyte), # type of file; not supported by all file system types
('d_name', c_char * 256), # filename
]
DIRENTRY_P = POINTER(DIRENTRY)
def fchown(fd, uid, gid):
return _load_libc().fchown(fd, uid, gid)
def fchmod(fd, mode):
return _load_libc().fchmod(fd, mode)
def fdopen(fd):
return _load_libc().fdopen(fd)
def readdir(dirp):
return _load_libc().readdir(dirp)
def rewinddir(dirp):
return _load_libc().rewinddir(dirp)
def closedir(dirp):
return _load_libc().closedir(dirp)
# --- liblve functions -----------------------------------------------
@lru_cache(maxsize=None)
def _load_liblve():
try:
liblve = cdll.LoadLibrary("libsecureio.so.0")
except OSError:
liblve = cdll.LoadLibrary("liblve.so.0")
# Opens path for reading not following symlinks and verifies that opened path is inside parent_path
# Returns:
# descriptor if successful
# -1 if path does not exist or is a symlink
# -2 if opened path is NOT inside parent_path or cannot be determined
# accepts path, parent_path
liblve.open_not_symlink.argtypes = [c_char_p, c_char_p]
liblve.open_not_symlink.restype = c_int
# Closes descriptor (if it is not equal -1)
# accepts file/dir descriptor (integer)
liblve.closefd.argtypes = [c_int]
# returns void
liblve.closefd.restype = None
# Tries to read first directory entry in order to ensure that descriptor is valid
# Returns 0 if reading succeeded or -1 if error has occured
# accepts descriptor
liblve.check_dir.argtypes = [c_int]
liblve.check_dir.restype = c_int
# Checks if path is a directory (in secure manner)
# Also opens path (if descriptor fd == -1) and then checks that opened path is inside parent_path
# Returns descriptor if path refers to directory
# Returns -1 if path does not exist or is not a directory
# Returns -2 if opened path is NOT inside parent_path or cannot be determined
# accepts path, descriptor, parent_path
liblve.isdir.argtypes = [c_char_p, c_int, c_char_p]
liblve.isdir.restype = c_int
# Sets permissions to directory (in secure manner)
# Returns descriptor if successful
# Returns -1 if error has occured
# Returns -2 if opened path is NOT inside parent_path or cannot be determined
# accepts: const char *path, mode_t perm, int fd, const char *parent_path
liblve.set_perm_dir_secure.argtypes = [c_char_p, c_int, c_int, c_char_p]
liblve.set_perm_dir_secure.restype = c_int
# Sets owner and group of directory (in secure manner)
# Returns descriptor if successful
# Returns -1 if error has occured
# Returns -2 if opened path is NOT inside parent_path or cannot be determined
# accepts: const char *path, uid_t uid, gid_t gid, int fd, const char *parent_path
liblve.set_owner_dir_secure.argtypes = [c_char_p, c_int, c_int, c_int, c_char_p]
liblve.set_owner_dir_secure.restype = c_int
# Creates directory if it does not exist, sets permissions/owner otherwise
# Returns descriptor if successful
# Returns -1 if error has occured
# accepts: const char *path, mode_t perm, uid_t uid, gid_t gid, int fd, const char *parent_path
liblve.create_dir_secure.argtypes = [c_char_p, c_int, c_int, c_int, c_int, c_char_p]
liblve.create_dir_secure.restype = c_int
# Recursive directory creation function
# Returns 0 if successful
# Returns -1 if error has occured
# accepts: const char *path, mode_t perm, uid_t uid, gid_t gid, const char *parent_path
liblve.makedirs_secure.argtypes = [c_char_p, c_int, c_int, c_int, c_char_p]
liblve.makedirs_secure.restype = c_int
# Writes absolute path pointed by descriptor fd to buffer *buf
# Returns buf if successful
# Returns NULL if error has occured
liblve.get_path_from_descriptor.argtypes = [c_int, c_char_p]
liblve.get_path_from_descriptor.restype = c_char_p
# Returns 1 if subdir is subdirectory of dir, 0 otherwise
liblve.is_subdir.argtypes = [c_char_p, c_char_p]
liblve.is_subdir.restype = c_int
return liblve
if TYPE_CHECKING:
liblve = _load_liblve()
def _open_not_symlink(path, parent_path):
return _load_liblve().open_not_symlink(path, parent_path)
def check_dir(fd):
return _load_liblve().check_dir(fd)
def isdir(path, descriptor, parent_path):
return _load_liblve().isdir(path, descriptor, parent_path)
def get_path_from_descriptor(fd, buf):
return _load_liblve().get_path_from_descriptor(fd, buf)
def is_subdir(dir, subdir):
return _load_liblve().is_subdir(dir, subdir)
# True : euid/egid == 0/0
# False : euid/egid == user/user
# set by set_user_perm() and set_root_perm() functions
root_flag = True
LOGFILE = "/var/log/cagefs-update.log"
MIN_UID = 500
SILENT_FLAG = False
def open_not_symlink(path):
return os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
def open_file_not_symlink(path):
return os.fdopen(open_not_symlink(path), 'r')
def flistdir(fd):
"""Returns list of entries of directory pointed by descriptor"""
# Duplicate descriptor, because closedir() closes descriptor associated with directory stream
fd2 = os.dup(fd)
# Open directory stream
dirp = fdopendir(fd2) # NOQA
if not dirp:
raise RuntimeError("fdopendir error")
# Reset position of directory stream
# (so it will be possible to read content of directory multiple times
# via other descriptors that refer to the directory)
rewinddir(dirp)
dirlist = []
while True:
entryp = readdir(dirp)
if not entryp:
break
entry = entryp.contents
dirlist.append(entry.d_name)
rewinddir(dirp)
closedir(dirp)
return dirlist
def closefd(fd):
if fd is not None:
try:
os.close(fd)
except OSError:
pass
def set_perm_dir_secure(path, perm, parent_path, fd=None, logger=None):
"""Sets permissions to directory (in secure manner)
Returns descriptor if successful
Returns None if error has occured"""
if fd is None:
fd = -1
fd = _load_liblve().set_perm_dir_secure(path.encode(), perm, fd, parent_path.encode())
if fd > 0:
return fd
if logger is not None:
logger('Error: failed to set permissions of directory ' + path, False, True)
return None
def set_owner_dir_secure(path, uid, gid, parent_path, fd=None, logger=None):
"""Sets owner and group of directory (in secure manner)
Returns descriptor if successful
Returns None if error has occured"""
if fd is None:
fd = -1
fd = _load_liblve().set_owner_dir_secure(path.encode(), uid, gid, fd, parent_path.encode())
if fd > 0:
return fd
if logger is not None:
logger('Error: failed to set owner of directory ' + path, False, True)
return None
def create_dir_secure(path, perm, uid, gid, parent_path, fd=None, logger=None):
"""Creates directory if it does not exist, sets permissions/owner otherwise
Returns descriptor if successful
Returns None if error has occured"""
if fd is None:
fd = -1
fd = _load_liblve().create_dir_secure(path.encode(), perm, uid, gid, fd, parent_path.encode())
if fd > 0:
return fd
if logger is not None:
logger('Error : failed to create directory ' + path, False, True)
return None
def makedirs_secure(path, perm, uid, gid, parent_path, logger=None):
"""Recursive directory creation function
Returns 0 if successful
Returns -1 if error has occured"""
res = _load_liblve().makedirs_secure(path.encode(), perm, uid, gid, parent_path.encode())
if res and logger:
logger('Error : failed to create directory ' + path, False, True)
return res
def read_file_secure(filename, uid=None, gid=None, exit_on_error=True, write_log=True):
"""read file not following symlinks"""
if (uid is None and gid is not None) or (uid is not None and gid is None):
raise RuntimeError("read_file_secure: uid and gid should be both null or be both not null")
drop_perm = (uid is not None) and (gid is not None)
if drop_perm:
set_user_perm(uid, gid)
try:
file_object = open_file_not_symlink(filename)
content = file_object.readlines()
file_object.close()
if drop_perm:
set_root_perm()
return content
except (OSError, IOError) as e:
if drop_perm:
set_root_perm()
logging('Error: failed to read ' + filename + ' : ' + str(e), SILENT_FLAG, 1, write_log)
if not exit_on_error:
raise
sys.exit(1)
def write_file_secure(content, ini_path, uid, gid, drop_perm=True, perm=0o644, write_log=True):
"""Returns True if error has occured"""
dirpath = os.path.dirname(ini_path)
if drop_perm:
set_user_perm(uid, gid)
fd = None
temp_path = None
try:
fd, temp_path = tempfile.mkstemp(prefix='cagefs_', dir=dirpath)
file_object = os.fdopen(fd, 'w')
file_object.write(''.join(content))
if not drop_perm and uid is not None and gid is not None:
if fchown(fd, uid, gid):
raise OSError('fchown failed')
if fchmod(fd, perm):
raise OSError('fchmod failed')
file_object.close()
except (IOError, OSError) as e:
try:
file_object.close()
except Exception:
pass
try:
os.close(fd)
except Exception:
pass
try:
os.unlink(temp_path)
except Exception:
pass
if drop_perm:
set_root_perm()
logging(
f"Error: failed to write file {ini_path} : {str(e).replace('Errno', 'Err code')}",
SILENT_FLAG,
1,
write_log,
)
return True
except Exception as e:
logging(f'Error: {str(e)}', SILENT_FLAG, 1)
sys.exit(1)
error = False
try:
os.rename(temp_path, ini_path)
except OSError as e:
error = True
logging('Error: failed to rename tempfile to ' + ini_path + ' : ' + str(e), SILENT_FLAG, 1, write_log)
try:
os.unlink(temp_path)
except OSError:
pass
if drop_perm:
set_root_perm()
return error
def write_file_via_tempfile(
content, dest_path, perm, prefix='', suffix='', as_user=None):
"""
Safely write string content to a file
:param content: str
:param dest_path: str -> path to a file
:param perm: int -> permissions for the file
:param prefix: str -> add to temporary file name
:param suffix: str -> add to temporary file name
:param as_user: str -> name of the user to drop privileges to
"""
if as_user is not None:
old_groups = os.getgroups()
drop_user_privileges(as_user, effective_or_real=True, set_env=False)
dirpath = os.path.dirname(dest_path)
fd, temp_path = None, None
try:
fd, temp_path = tempfile.mkstemp(
prefix=prefix, suffix=suffix, dir=dirpath)
with os.fdopen(fd, 'w', errors='surrogateescape') as f_temp:
f_temp.write(content)
except (IOError, OSError):
if fd is None or temp_path is None:
raise
try:
os.close(fd)
except (IOError, OSError):
pass
try:
os.unlink(temp_path)
except (IOError, OSError):
pass
raise
try:
os.chmod(temp_path, perm)
os.rename(temp_path, dest_path)
except (OSError, IOError, TypeError):
try:
os.unlink(temp_path)
except (OSError, IOError):
pass
raise
if as_user is not None:
ruid = os.getuid()
os.seteuid(ruid)
os.setegid(os.getgid())
# All of the above can be called from user named as_user
if ruid == 0:
os.setgroups(old_groups)
def set_user_perm(uid, gid, exit=True):
global root_flag
try:
os.setegid(gid)
except (OSError,) as e:
if exit:
print_error('failed to set egid to ' + str(gid) + ': ' + str(e))
sys.exit(1)
else:
return -1
groups = get_groups(uid, gid)
try:
os.setgroups(groups)
except (OSError,) as e:
if exit:
print_error('failed to set supplementary groups to :', groups, str(e))
sys.exit(1)
else:
return -1
try:
os.seteuid(uid)
except (OSError,) as e:
if exit:
print_error('failed to set euid to ' + str(uid) + ': ' + str(e))
sys.exit(1)
else:
return -1
if uid == 0:
root_flag = True
else:
# If it's possible, switch on CAP_SYS_RESOURCE
_load_liblve().enable_quota_capability()
root_flag = False
def set_root_perm(exit=True):
global root_flag
try:
os.seteuid(0)
except (OSError,) as e:
if exit:
print_error('failed to set euid to 0 :', str(e))
sys.exit(1)
else:
return -1
try:
os.setegid(0)
except (OSError,) as e:
if exit:
print_error('Error: failed to set egid to 0 :', str(e))
sys.exit(1)
else:
return -1
groups = get_groups(0, 0)
try:
os.setgroups(groups)
except (OSError,) as e:
if exit:
print_error('Error: failed to set supplementary groups to :', groups, str(e))
sys.exit(1)
else:
return -1
root_flag = True
def print_error(*args):
print("Error:", end=' ', file=sys.stderr)
for a in args:
print(a, end=' ', file=sys.stderr)
print(file=sys.stderr)
def get_groups(uid, gid):
"""Returns supplementary groups for uid"""
gr = get_grp_dict()
pw = get_pwd_dict()
groups = set()
for group in gr:
members = gr[group].gr_mem
for user in members:
try:
member_uid = pw[user].pw_uid
except KeyError:
continue
if member_uid == uid:
groups.add(gr[group].gr_gid)
groups.add(gid)
return list(groups)
grp_dict = None
def get_grp_dict():
global grp_dict
if grp_dict is None:
grp_dict = {}
gr = grp.getgrall()
for line in gr:
grp_dict[line.gr_name] = line
return grp_dict
clpwd = ClPwd(min_uid=MIN_UID)
def get_pwd_dict():
return clpwd.get_user_dict()
log_file = None
def logging(msg, silent=False, verbose=True, write_log=True):
global log_file
if not silent:
if verbose:
print(msg)
if write_log:
root_flag_saved = root_flag
if not root_flag:
uid, gid = get_perm()
set_root_perm()
try:
if log_file is None:
umask_saved = os.umask(0o22)
# log_file is opened in "line buffered" mode
log_file = open(LOGFILE, 'w', 1) # NOQA
os.umask(umask_saved)
log_file.write(msg)
log_file.write("\n")
except (OSError, IOError) as e:
print_error("writing to ", LOGFILE, str(e))
sys.exit(1)
if not root_flag_saved:
set_user_perm(uid, gid)
def get_perm():
try:
uid = os.geteuid()
gid = os.getegid()
except (OSError,) as e:
print_error('failed to get (euid,egid)', str(e))
sys.exit(1)
return uid, gid
def set_capability(clear=False):
"""
Set CAP_SYS_RESOURCE capability
:param bool clear: Set on if it's true, set off otherwise
:return: 0 for success, -1 otherwise
:rtype: int
"""
return _load_liblve().disable_quota_capability() if clear \
else _load_liblve().enable_quota_capability()
def change_uid(uid):
"""
Change effective uid of current process and set CAP_SYS_RESOURCE capbality
to prevent "Disk quota exceeded" error
:param int euid: User ID to set it as current effective UID
:return: 0 if capability was set successfuly, -1 otherwise
:rtype: int
"""
os.seteuid(uid)
return set_capability()
def _set_quota_checks_status(enabled):
"""
Disable quota kernel check to allow us to write
more than user can by quota.
"""
if not enabled:
_load_liblve().enable_quota_capability()
else:
_load_liblve().disable_quota_capability()
@contextlib.contextmanager
def disable_quota():
_set_quota_checks_status(enabled=False)
try:
yield
finally:
_set_quota_checks_status(enabled=True)
@contextlib.contextmanager
def set_umask(umask_value):
saved_umask = os.umask(umask_value)
try:
yield
finally:
os.umask(saved_umask)
Zerion Mini Shell 1.0