第9章 进程凭证
每个进程都有一组与之相关的数值型用户标识符(UIDs)和组标识符(GIDs)。有时,把这些标识符称之为 进程凭证(process credentials) 。这些标识符有:
- 实际
【真实】(real)用户ID和实际组ID; - 有效(effective)用户ID和有效组ID;
- 保存的set-user-ID(saved set-user-ID)和保存的set-group-ID;
- 文件系统用户ID和文件系统组ID(Linux特有);
- 辅助组
【补充组】(supplementary group)IDs。
本章中,将 【we,我们】 详细探究上述进程标识符的用途 【the purpose of,作用】 ,并且介绍用于获取或修改上述标识符的系统调用和库函数。还将讨论 【we also discuss, 我们还会讨论】 特权进程和非特权进程的概念,并阐述 【and,以及】 set-user-ID和set-group-ID的机制,采用该机制所创建的程序可以以特定用户或组的权限运行。【which allow the creation of programs that run with the privileges of a specified user or group,用于程序创建,这些程序以指定用户或组的特权进行运行】 。
9.1 Real User ID and Real Group ID
实际用户ID(Real User ID) 和 实际组ID(Real Group ID) 用于标识用户属于哪个用户和组。作为登录过程的一部分 【As part of the login process,这里process的意思不是“进程”而是“过程”】 ,login shell 【作为login进程的部分,】 会从/etc/passwd文件(8.1节)读取相应用户密码记录的第三个(UID)和第四个字段(GID),从而得到实际用户ID和实际组ID。当新的进程被创建时(例如使用shell创建程序时),它将 【会】 从父进程中继承这些标识符。
9.2 Effective User ID and Effective Group ID
在大部分UNIX实现中(Linux系统略有不同,9.5节介绍),当进程尝试执行各种操作(即 【i.e., 也就是,】 系统调用)时,将结合 有效用户ID(Effective User ID)、有效组ID(Effective Group ID) 和辅助组ID(supplementary group IDs)一起确认进程是否拥有权限。举例来说,当进程访问资源(例如文件和System V进程间通信对象)时,上述IDs会根据这些资源所属的用户ID和组ID,决定是否授予进程访问权限。如20.5节所述~~【 As we’ll see
in Section 20.5,在20.5节中】~~ ,内核还会使用有效用户ID决定进程是否可以向其他进程发送信号。
具有有效用户ID为0(root的用户ID)的进程具有超级用户的所有权限。此类进程被称之为 特权进程(privileged process)。某些系统调用只有特权进程才有权限执行。
在39章中,将阐述Linux capability的实现,capability机制将授予超级用户的权限划分成很多不同的单元,这些单元可以独立地启用
【enabled,开启】和禁用【disabled,关闭】。
通常,有效用户ID和有效组ID与对应的实际用户ID和实际组ID的值相同。但是有两种方式可以将effective IDs设置成不同的值,一种方式是使用9.7节的系统调用,第二种方式是执行 【通过】 set-user-ID 和 set-group-ID 程序 【的执行】 。
9.3 Set-User-ID and Set-Group-ID programs
set-user-ID程序将进程的有效用户ID设置成与可执行文件的用户ID(文件属主)相同的值,从而使进程获取它平时所没有的权限。set-group-ID程序为进程的有效组ID执行类似的任务。(专业术语 set-user-ID程序 和 set-group-ID程序 有时简写成 set-UID程序 和 set-GID程序 。译者注:甚至可以简写为 SUID程序 和 SGID程序)
像其他文件一样,采用用户ID和组ID来定义可执行程序的所有权。此外,可执行文件具有两个特别的权限位(permission bits):set-user-ID位和set-group-ID位。(实际上 【In fact,事实上】 ,每个文件都有这两个权限位,但此处只 关注 可执行文件的权限位 【but it is their use with executable files that interests us here,但是这里我们只对可执行文件的 感兴趣 】 。)使用 chmod 命令对权限位进行设置。非特权用户只能 对其拥有 的文件进行设置 【An unprivileged user can set these bits for files that they own,非特权用户只能设置 它们自己所拥有文件的权限】 。特权用户可以设置任何文件的权限位。下面是一个例子:
正如上例所示,程序可以同时对这两个位进行设置,尽管这并不常见。当使用 ls -l 查看程序(文件)的权限时,如果程序设置了set-user-ID或set-group-ID权限位,那么通常用于表示 可执行(execute) 权限的位(x)会被s替代:
当 set-user-ID程序 运行时(即使用exec()将程序加载到进程的内存中),内核将进程的有效用户ID设置成可执行文件的用户ID。运行set-group-ID程序时,进程的有效组ID具有类似的效果。采用这种方法(换句话说,用户执行程序)改变进程的有效用户ID和有效组ID,可以使进程获得通常所没有的权限。例如,如果一个可执行文件的属主是root(超级用户),并且开启了set-user-ID权限,那么当程序运行时进程将获得超级用户权限。
set-user-ID程序和set-group-ID程序还可以将进程的有效IDs(有效用户ID和有效组ID)改成除了root之外的其他用户IDs。例如,为了访问某个受保护的文件(或者其他系统资源),专门为此创建一个拥有访问这些文件权限的用户(或组)以及set-user-ID(set-group-ID)程序,这样进程的有效用户(组)ID就可以改为这些IDs。这就使得程序可以访问这些文件,但是不具有超级用户的所有权限。
有时,使用术语 set-user-ID-root程序 来区分set-user-ID程序,前者的属主是root,后者的属主是其他用户,只给予进程相应用户的权限。
从现在开始,使用的术语“特权(privileged)”有两层不同意思。其一是之前定义的:有效用户ID为0的进程,它具有root用户的所有特权。其二是当我们讨论到set-user-ID程序的所属用户(非root)时,指进程获取了该set-user-ID程序的用户ID的特权。术语“特权”是指哪种意思,我们可以通过上下文来判别。
Linux中set-user-ID程序的常用例子有:用于更改用户ID的 passwd命令;用于挂载(mount)和卸载(unmount)文件系统的 mount 和 unmount命令;用于在不同用户ID下运行shell的 su命令。set-group-ID程序的一个例子是:wall,用于将消息写入到所属组是tty组的所有终端中(通常,所有终端都属于这个组)。
在8.5节中,我们注意到Listing 8-2中的程序需要使用root账号登录后才能访问/etc/shadow文件。我们可以通过将该程序设置为set-user-ID-root程序,这样任意的用户都能运行。如下:
set-user-ID/set-group-ID技术是一种有用和强大的工具。但是如果应用没有进行良好的设计,会导致 安全隐患【 security breaches,安全问题】 。38章中我们列出一套在编写set-user-ID和set-groupID程序时应当遵循的 良好编程习惯【 good practices,良好做法】 。
摘自网上的一个对set-user-ID的解释:
set-user-ID 会创建s与t权限,是为了让一般用户在执行某些程序的时候,能够暂时具有该程序拥有者的权限。举例来说,我们知道,账号与密码的存放文件其实是 /etc/passwd与 /etc/shadow。而 /etc/shadow文件的权限是“----------”。它的拥有者是root。在这个权限中,仅有root可以“强制”存储,其他人是连看都不行的。但是,偏偏笔者使用dmtsai这个一般身份用户去更新自己的密码时,使用的就是 /usr/bin/passwd程序,却可以更新自己的密码。也就是说,dmtsai这个一般身份用户可以存取 /etc/shadow密码文件。这怎么可能?明明 /etc/shadow就是没有dmtsai可存取的权限。这就是因为有s权限的帮助。当s权限在user的x时,也就是类似 -r-s–x--x,称为set-user-ID,这个user-ID表示用户的ID,而user表示这个程序(/usr/bin/passwd)的拥有者(root)。那么,我们就可以知道,当dmtsai用户执行 /usr/bin/passwd时,它就会“暂时”得到文件拥有者root的权限。
set-user-ID仅可用在“set-user-ID”,set-user-ID因为是程序在执行过程中拥有文件拥有者的权限,因此,它仅可用于二进制文件,不能用在批处理文件(shell脚本)上。这是因为shell脚本只是将很多二进制执行文件调进来执行而已。所以set-user-ID的权限部分,还是要看shell脚本调用进来的程序设置,而不是shell脚本本身。当然,set-user-ID对目录是无效的。这点要特别注意。
9.4 Saved Set-User-ID and Saved Set-Group-ID
saved set-user-ID 和 saved set-group-ID 是为了set-user-ID程序和set-group-ID程序而设计的。当程序执行时,会有以下步骤:
-
若 开启了可执行文件的
【如果可执行文件开启了】set-user-ID(set-group-ID)权限位。那么进程的有效用户(组)ID会被设置成可执行文件的属主。若未 设置set-user-ID(set-group-ID)权限位【如果set-user-ID(set-group-ID)位没有设置】,那么进程的有效用户(组)ID保持不变。 - saved set-userID 和 saved set-group-ID的值从对应的有效用户ID和有效组ID中复制过来。不管set-user-ID或者set-group-ID权限位是否开启,这个步骤(复制行为)都会执行。
举例说明上述步骤的影响:假设进程的用户ID、有效用户ID和saved set-user-ID都是1000,执行了属主为root(用户ID为0)的set-user-ID程序。在执行后,进程的IDs将变为:
有不少 【various,各种】 系统调用允许set-user-ID程序将有效用户ID值在实际用户ID和saved set-user-ID之间随意切换。类似的系统调用允许set-group-ID程序修改它的有效组ID。以这种方式,程序可以临时将与执行文件的用户(组)ID相关的权限 放弃(drop) 和 重新获取(regain) 。(换句话说,程序可以有两种权限状态:自己的权限和set-user-ID程序属主的权限)正如38.2节所述,在set-user-ID程序和set-group-ID程序中,当程序实际不需要使用特权ID(即saved set-user-ID)执行操作,就切换到非特权ID(即实际用户ID),这是一个安全的编程习惯。
saved set-user-ID和save set-group-ID有时也称为 save user ID 和 saved group ID。
9.5 File-System User ID and File-System Group ID
在Linux中,执行文件操作(例如打开文件、改变文件所属权和修改文件权限)时,使用 文件系统用户ID(File-System User ID) 和 文件系统组ID(File-System Group ID)(与辅助组ID相结合) 决定操作权限,而不是有效用户ID和有效组ID。(像其他UNIX实现一样,有效用户ID和有效组ID仍在使用,用途如上一节所述。)
通常,文件系统用户ID和文件系统组ID与对应的有效用户ID和有效ID具有相同的值(一般与对应的实际用户ID和实际组ID的值也相同)。此外,每当使用系统调用或者set-userID(set-group-ID)程序对有效用户ID(有效组ID)进行修改时,对应的文件系统ID也会改为相同的值。因为文件系统IDs以这种方式跟随有效IDs进行变化。这意味着,检查特权和权限时,Linux的行为与其他UNIX实现的行为类似。只有当使用Linux 特有 的两个系统调用:setfsuid() 和 setfsgid() 时,文件系统IDs才会与有效IDs不同。
为什么Linux要提供文件系统IDs,在什么情况下我们才希望有效IDs与文件系统IDs不同?这原因主要跟历史有关。文件系统IDs首次出现在Linux1.2中。在这个内核版本中,如果某个进行发送信号给另一个进程,那么需要发送进程的 有效用户ID 与目标进程的实际用户ID或有效用户ID匹配。这就影响到了某些程序,如Linux NFS(网络文件系统)服务程序,这些程序需要能够访问文件,就像它拥有相应客户端进程的有效IDs。然而,如果NFS服务程序改变了它的有效用户ID,那么容易受到非特权用户进程的发来的信号攻击( it would be vulnerable to signals from unprivileged user processes)(译者注:客户端进程发过来的信号找不到服务端的服务进程,导致客户端不断往服务端发送信号?这块内容有点看不明白)。为了防止这种可能性,【在设计中增加了】 文件系统用户ID和文件系统组ID 应运而生(were devised)。NFS服务的有效IDs保持不变,只通过改变文件系统IDs从而伪装成另一个用户,这样即达到了访问文件的目的,又避免遭受信号攻击。
从内核2.0开始,Linux 在信号发送权限方面 开始采用SUSv3强制规定 【关于发送信号权限方面】 的规则。这些规则不涉及目标进程的有效用户ID(参考20.5节)。这样,就不再需要文件系统ID这个特性了,但是为了兼容已存在的软件,这个特色还是被保留了下来。
因为文件系统ID 实属异类【something of an oddity,有点奇怪】 ,并且通常与对应的有效IDs的值相同。在本书的剩下章节中,我们以进程的有效IDs来描述各种权限检查。虽然在Linux进程进行权限检查时,实际可能用到了文件系统ID。但实际上,它们的存在 并不会带来显著差别【their presence seldom makes an effective differenc】。
9.6 Supplementary Group IDs
辅助组ID(Supplementary Group ID) 是进程所属的额外组。进程从父进程中继承这些IDs。login shell从系统组文件(/etc/group)中获取辅助组IDs。正如上所述,这些IDs用于结合有效IDs和文件系统IDs,从而决定访问文件、System V IPC对象和其他系统资源的权限。
9.7 Retrieving and Modifying Process Credentials
Linux提供了一系列系统调用和库函数,用于获取和改变上面几节中的各种用户ID和组ID。其中只有部分APIs才在SUSv3中进行了定义。剩下的APIs,有些在其他UNIX实现中被广泛使用,有些是Linux特有的。在阐述各种接口时,我们需要注意可移植性的问题。在这节的末尾,Table 9-1对用于改变进程凭证的所有接口操作做了概括。
除了使用下面描述的各种系统调用,还可以使用Linux特有的 /proc/PID/status 文件中的 UID 、GID 和 Groups 来查看进程凭证。UID(GID)中的标识符分别表示:实际用户ID(实际组ID)、有效用户ID(有效组ID)、saved set-user-ID (saved set-group-ID)和文件系统用户ID(文件系统组ID)。
下面章节中,我们使用 有效用户ID为0的进程称为 特权进程 这一传统定义。然而,在Linux中将超级用户特权划分成不同的 能力(capability)(39章)。有两个 能力会在修改进程用户ID和组ID的这些系统调用用到:
- CAP_SETUID 能力允许进程对它们的用户IDs做任意修改。
- CAP_SETGID 能力允许进程对它们的组IDs做任意修改。
9.7.1 Retrieving and Modifying Real,Effective,and Saved Set IDs
接下来,将介绍用于获取和修改实际IDs、有效IDs和saved set IDs的这些系统调用。有若干系统调用是用于执行这些任务的,在某些情况下,它们的功能可能会重叠,反映出不少系统调用来源于不同的UNIX实现这一实际情况。
Retrieving real and effective IDs
getuid() 和 getgid() 系统调用分别返回调用进程的实际用户ID和实际组ID。geteuid() 和 getegid() 系统调用为有效IDs执行相应的任务。这些系统调用总是能调用成功。
#include <unistd.h>
// 返回调用进程的实际用户ID
uid_t getuid(void);
// 返回调用进程的有效用户ID
uid_t geteuid(void);
// 返回调用进程的实际组ID
gid_t getgid(void);
// 返回调用进程的有效组ID
gid_t getegid(void);
Modifying effective IDs
setuid() 系统调用将进程的有效用户ID(还有可能将实际用户ID和saved set-user-ID)设置成给定的uid参数的值。setgid() 系统调用为对应的组IDs执行类似的任务。
#include <unistd.h>
//执行成功时返回0,失败时返回-1
int setuid(uid_t uid);
int setgid(gid_t gid);
使用setuid()和setgid()可以对进程凭证做哪些改变呢?这主要取决于是否是特权进程(有效用户ID是0的进程)。setuid()有下列规则:
- 当非特权进程调用setuid()时,只有进程的有效用户ID会改变。此外,它只能被设置成实际用户ID和saved set-user-ID的值(如果违反这个约束,会产生EPERM错误)。这意味着,对于非特权用户,这个系统调用只有在执行set-user-ID程序时才有用。因为对于正常程序的执行,进程的实际用户ID、有效用户ID和saved set-user-ID都是相同的值。在一些派生自BSD的实现(系统)中,通过非特权进程调用setuid()或getgid()具有与其他UNIX实现(系统)不同的语义:系统调用会将实际IDs、有效IDs和saved set IDs修改成当前的实际IDs或有效IDs的值。
- 当特权进程执行setuid(),传入一个非0参数,那么 实际用户ID 、有效用户ID 和 saved set-user-ID 都会被设置成uid参数所指定的值。这是一波单向(one-way)操作,一旦特权进程以这种方式改变了它的标识符(IDs)后,它就失去了所有特权(由进程的有效用户ID决定),因此随后就不能使用setuid()将这些标识符重新设置回0了。如果这不是你想要的,那么可以使用随后介绍的 seteuid() 和 setreuid() 来替代setuid()。
使用 setgid() 对组ID进行设置的规则也是类似的,只是用 setgid() 代替 setuid(),用 组 代替 用户。setgid()的规则1与上面所述相同。在规则2中,因为改变组不会导致进程丧失特权(由进程的有效用户ID决定),特权程序可以使用setgid()自由地将组ID改成想要的值。
将set-user-ID-root程序(当前有效用户ID是0)以不可逆的方式放弃所有特权(通过将有效用户ID和saved set-user-ID设为与实际用户ID相同的值)的首先方式是使用下列调用:
if (setuid(getuid()) == -1)
errExit("setuid");
出于9.4节所述的安全原因,属主不是root的set-user-ID程序可以使用setuid()将有效用户ID在实际用户ID和saved set-user-ID的值之间切换。但是如果仅仅考虑这个原因,那么seteuid()会使更好的选择,因为它具有相同的效果,而不需要考虑set-user-ID程序的属主是否是root。
进程可以通过使用 seteuid() 将它的有效用户ID改成euid参数所指定的值。setegid() 将有效组ID改成egid参数所指定的值。
#include <unistd.h>
//执行成功时返回0,错误时返回-1
int seteuid(uid_t euid);
int setegid(gid_t egid);
进程在使用seteuid()和setegid()对进程的有效IDs进行改变时,会有以下规则:
- 非特权进程只能将有效ID设置成相应的实际ID和save set ID。(换句话说,对于非特权进程,seteuid()和seteuid()分别与setuid()和setgid()的效果相同吗,之前介绍的BSD移植性问题。)
- 特权进程可以将有效ID设置成任意的值。如果特权进程使用seteuid()将有效用户ID改成非0的值,那么它不再具有特权(但是有时可以通过第一条规则重新获取特权)
set-user-ID和set-group-ID程序临时放弃特权,随后又重新获取特权的首选方法是使用seteuid()。如下例:
/*
假设初始IDs: real=1000,effective=0,saved=0
保存刚开始的有效用户ID,它的值与saved set-user-ID相同语句。执行后变量euid=0
*/
euid=geteuid();
/*
有效用户ID被设为1000,去除特权
执行后的IDs:real=1000,effective=1000,saved=0
*/
if (seteuid(getuid()) == -1)
errExit("seteuid");
/*
根据上述规则1:非特权进程只能将有效ID设置成相应的实际ID和save set ID
因为save set ID是0,所以允许将有效用户ID重新设置为0
执行后的IDs:real=1000,effective=0,saved=0
*/
if (seteuid(euid) == -1)
errExit("seteuid");
seteuid()和setegid()来源于BSD,现在已成为SUSv3的规范,出现在大部分的UNIX实现中。
在GNU C库的旧版本中(glibc2.0及更早版本),seteuid(euid)被实现为setreuid(-1,euid)。在现代的glibc版本中,seteuid(euid)被实现为setresuid(-1, euid, -1)。两种实现都允许我们将euid指定为与当前有效用户ID相同的值(即保持不变)。但是SUSv3中没有规定seteuid()的这种行为,其他UNIX实现也不支持。
通常情况下,有效用户ID要么与实际用户ID相同,要么与saved set-user-ID相同。(Linux中要使有效用户ID既不同于实际用户ID,也不同于save set-user-ID的唯一方法是使用非标准的setresuid()系统调用。)
Modifying real and effective IDs
setreuid() 系统调用允许我们独立地改变实际用户ID和有效用户ID。setregid() 系统调用为实际组ID和有效组ID执行类似的任务。
#include <unistd.h>
// 成功时返回0,发生错误时返回-1
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
每个系统调用的第一个参数是新的实际ID。第二个参数是新的有效ID。如果我们只想改变其中的一个标识符,那么只需要将其他参数指定为-1。
setreuid()和setregid()起初来源于BSD,现在已是SUSv3的规范,并且已被大部分UNIX实现支持。
与这节中的其他系统调用一样,setreuid()和setregid()也有以下规则。我们只从setreuid()的角度阐述这些规则,setregid()也是类似的。
- 非特权进程只能将实际用户ID设置成当前的实际用户ID(即保持不变)或有效用户ID的值。有效用户ID只能设置成当前实际用户ID、有效用户ID(即保持不变)或者saved set-user-ID。
- 特权进程可以对这些IDs做任何改变。
- 只要下面条件成立,不管是特权进程还是非特权进程,saved set-user-ID都会被设置成新的有效用户ID:
a) ruid不是-1(即实际用户ID被设置,即使被设置成了与现在实际用户ID相同的值)。
b) 有效用户ID被设置成不同与系统调用之前的实际用户ID
反过来说,如果进程使用setreuid()只将有效用户ID改成与当前实际用户ID相同的值,那么saved set-user-ID保持不变。 随后可以通过调用setreuid()(或seteuid())将有效用户ID恢复成saved set-user-ID的值。
第3条规则提供一种set-user-ID程序永久放弃特权的方法,使用如下系统调用:
setreuid(getuid(), getuid());
set-user-ID-root进程想要对用户和组凭证都修改成任意的值,应该首先调用setregid(),然后调用setreuid()。如果调用的先后顺序相反,那么调用setregid()时将失败,因为调用了setreuid()之后,程序不再是具有特权的了。
Retrieving real,effective,and saved set IDs
大部分UNIX实现中,进程不能直接获取(或更改)saved set-user-ID和set-group-ID。但是Linux提供了两种(非标准的)系统调用,允许我们可以这么做:getresuid() 和 getresgid()。
#define _GNU_SOURCE
#include <unistd.h>
// 成功时返回0,失败时返回-1
int getresuid(uid_t *ruid, uid_t *euid, uid_t *suid);
int getresig(gid_t *rgid, gid_t *geid, gid_t *sgid);
getresuid()系统调用返回调用进程中三个参数指向的实际用户ID、有效用户ID和saved set-user-ID的值。getresgid()类似。
Modifying real,effective,and saved set IDs
setresuid() 系统调用允许调用进程独立地改变用户IDs的这三个值。新的用户IDs的值由系统调用中的三个参数指定。**setresgid()**为组ID执行类似的任务。
#define _GNU_SOURCE
#include <unistd.h>
//成功时返回0,失败时返回-1
int setresuid(uid_t ruid, uid_t euid, uid_t suid);
int setresgid(gid_t rgid, gid_t egid, gid_t sgid);
如果我们不想改变所有的标识符,那么将该参数设为-1,将保持该标识符不变。例如下面的调用等价于seteuid(x):
setresuid(-1, x, -1);
setresuid()(setresgid()类似)的规则如下:
- 非特权进程可以将实际用户ID、有效用户ID和saved set-user-ID的任意ID设置成当前的实际用户ID、有效用户ID或者saved set-user-ID中的任意值。
- 特权进行可以对实际用户ID、有效用户ID和saved set-user-ID做任何改变。
- 不管调用是否对其他IDs做了改动,文件系统用户ID总是被设置成与(可能是新的)有效用户ID相同的值。
调用setresuid()和setresgid()是原子性(all-or-nothing effect)的。要么请求的所有标识符都被修改成功,要么都失败。(这也适用于本章中其他修改多个ID的系统调用)
尽管setresuid()和setresgid()为修改进程凭证提供了更加简明直白的API。但是我们不能将它们用于可移植的应用中。因为它们不是SUSv3的规范,其他的一些UNIX系统并没有对它们进行支持。
9.7.2 Retrieving and Modifying File-System IDs
之前描述的所有系统调用在改变进程的有效用户ID或有效组ID时总会修改对应的文件系统ID。想要单独地修改文件系统IDs,必须采用两个Linux特有的系统调用:setfsuid() 和 setfsgid()。
#include <sys/fsuid.h>
// Always returns the previous file-system user ID
int setfsuid(uid_t fsuid);
// Always returns the previous file-system group ID
int setfsgid(gid_t fsgid);
setfsuid()系统调用将进程的文件系统用户ID修改成fsuid参数所指定的值。setfsgid()系统调用将进程的文件系统组ID修改成fsgid参数所指定的值。
setfsuid()的规则如下(setfsgid类似):
- 非特权进程可以将文件系统用户ID设置成当前的实际用户ID、有效用户ID、文件系统用户ID (即保持不变)或者saved set-user-ID的值。
- 特权进程可以将文件系统用户ID设置成任何值。
setfsuid()和setfsgid()系统调用的使用在Linux中已经不再是必须的了。如果应用需要移植到其他UNIX系统中,那么就应该避免使用这两个系统调用。
9.7.3 Retrieving and Modifying Supplementary Group IDs
getgroups() 系统调用返回调用进程的所属组的集合,保存在grouplist指向的数组中。
#include <unistd.h>
// 成功时返回grouplist中组ID的个数,失败时返回-1
int getgroups(int gidsetsize, gid_t grouplist[]);
像大部分UNIX实现一样,在Linux中,getgroups()仅仅返回调用进程的辅助组IDs。然而,SUSv3规定,在返回的grouplist中还可以包含调用进程的有效组ID。
调用程序必须为grouplist数组分配内存,并在gidsetsize参数中指定grouplist数组的长度。如果执行成功,getgroups()返回grouplist中存放的group IDs的个数。
如果进程的组的个数超过了gidsetsize,getgroups()将返回EINVLA错误。为了避免这种情况,可以将grouplist的长度设为1(为保证可移植性,将有效组ID算上)加上常量NGROUPS_MAX(在<limits.h>中定义)。NGROUPS_MAX中定义了进程的辅助组的最大个数。所以,我们可以这样声明grouplist:
gid_t grouplist[NGROUPS_MAX + 1];
在Linux内核2.6.4之前,NGROUPS_MAX的值是32,从内核2.6.4开始,NGROUPS_MAX的值是65535。
应用也可以在运行时,使用以下方式确定NGROUPS_MAX限制:
- 调用sysconf(_SC_NGROUPS_MAX)。(11.2节将解释sysconf()的用法)
- 从Linux特有的、只读的 /proc/sys/kernel/ngroups_max 文件中读取这个值。这个文件是从内核2.6.4才加入的。
除此之外,应用还可以在调用getgroups()时指定gidtsetsize为0。这种情况下,grouplist不会被修改,但是会返回调用进程所属组的个数。
使用上面技术获得的值都可用于为将来的getgroups()调用动态分配grouplist数组。
特权进程可使用 setgroups() 和 initgroups() 来改变辅助组ID的集合。
#define _BSD_SOURCE
#include <grp.h>
//成功时返回0,失败时返回-1
int setgroups(size_t gidsetsize, const gid_t *grouplist);
int initgroups(const char *user, gid_t group);
setgroups()系统调用使用给定的grouplist数组替换进程的辅助组IDs。gidsetsiz参数指定了grouplist数组中组IDs的个数。
initgroups()函数通过扫描/etc/groups,并构建一个user参数所属组的列表,从而初始化调用进程的辅助组IDs。此外,group参数中指定的group ID也会加到进程的辅助组IDs中。
initgroups()主要用于创建登录会话的程序,例如 login程序,它会在运行用户的login shell时设置各种进程属性。这类程序一般从 密码文件中 读取相应用户的 组ID 字段,将获取的值作为group参数。这里稍微有点令人费解 【confusing,混乱】 ,因为因为密码文件中的组ID并不是真正的辅助组,而是为login shell定义了初始的实际用户ID、有效用户ID和saved set-user-ID。尽管如此,这就是initgroups()函数经常使用的方式。
尽管不是SUSv3中定义的规范,但是所有UNIX实现支持setgroups()和initgroups()。
9.7.4 summary of Calls for Modifying Process Credentials
Table 9-1 对修改进程凭证的各种系统调用和库函数的效果进行了概括。
Figure 9-1 提供了与Table 9-1中相同信息的图形概括。本图内容是从修改用户ID的角度加以展示的,修改组ID与之类似。
9.7.5 Example: Displaying Process Credentials
Listing 9-1的程序使用上述的系统调用和库函数来获取进程的所有用户ID和组ID,然后进行展示:
// Listing 9-1 Dispaly all process user and group IDs
// proccred/idshow.c
#define _GNU_SOURCE
#include <unistd.h>
#include <limits.h>
#include "ugid_functions.h" /* userNameFromId() & groupNameFromId() */
#include "tlpi_hdr.h"
#define SG_SIZE(NGROUPS_MAX + 1)
int main(int argc, char *argv[])
{
uid_t ruid, euid, suid, fsuid;
gid_t rgid, egid, sgid, fsgid;
gid_t suppGroups[SG_SIZE];
int numGroups, j;
char *p;
if (getresuid(&ruid, &euid, &suid) == -1)
errExit("getresuid");
if (getresgid(&rgid, &egid, &sgid) == -1)
errExit("getresgid");
/* Attempts to change the file-system IDs are always ignored
for unprivileged processes, but even so, the following
calls return the current file-system IDs */
fsuid = setfsuid(0);
fsgid = setfsgid(0);
printf("UID: ");
p = userNameFromId(ruid);
printf("real=%s (%ld); ", (p == NULL) ? "???" : p, (long) ruid);
p = userNameFromId(euid);
printf("eff=%s (%ld); ", (p == NULL) ? "???" : p, (long) euid);
p = userNameFromId(suid);
printf("saved=%s (%ld); ", (p == NULL) ? "???" : p, (long) suid);
p = userNameFromId(fsuid);
printf("fs=%s (%ld); ", (p == NULL) ? "???" : p, (long) fsuid);
printf("\n");
printf("GID: ");
p = groupNameFromId(rgid);
printf("real=%s (%ld); ", (p == NULL) ? "???" : p, (long) rgid);
p = groupNameFromId(egid);
printf("eff=%s (%ld); ", (p == NULL) ? "???" : p, (long) egid);
p = groupNameFromId(sgid);
printf("saved=%s (%ld); ", (p == NULL) ? "???" : p, (long) sgid);
p = groupNameFromId(fsgid);
printf("fs=%s (%ld); ", (p == NULL) ? "???" : p, (long) fsgid);
printf("\n");
numGroups = getgroups(SG_SIZE, suppGroups);
if (numGroups == -1)
errExit("getgroups");
printf("Supplementary groups (%d): ", numGroups);
for (j = 0; j < numGroups; j++) {
p = groupNameFromId(suppGroups[j]);
printf("%s (%ld) ", (p == NULL) ? "???" : p, (long) suppGroups[j]);
}
printf("\n");
exit(EXIT_SUCCESS);
}
9.8 Summary
每个进程都有一些相关的用户IDs和组IDs(凭证,credentials)。实际IDs定义了进程的所属权。在大部分UNIX实现中,当访问像文件这样的资源时,有效IDs用于决定进程的权限。然而,在Linux中,使用文件系统IDs决定访问文件的权限,而使用有效IDs进行其他权限检查。(因为文件系统IDs通常与对应的有效IDs具有相同的值,所以Linux对文件权限的检查方式与其他UNIX相同 【Linux behaves in the same way as other UNIX implementations when checking file permissions,所以当检查文件权限时,Linux表现出与其他UNIXs实现相同的行为】 )进程的辅助组IDs进程所属的额外组,用于权限检查。不少系统调用和库函数允许进程获取和改变它的用户IDs和组IDs。
当运行set-user-ID程序时,进程的有效用户ID被设置成文件的所属者。该机制允许用户“假借”其他用户的特权来运行特定的程序。
相应的,set-group-ID程序会改变运行该程序的进程的有效组ID。saved set-user-ID和saved set-group-ID允许set-user-ID程序和set-group-ID程序临时放弃特权,随后又可重新获取。
用户ID 0是特别的。通常,名为root的用户帐号具有这个用户ID。拥有有效用户ID为0的进程是具有特权的——也就是说,当进程执行各种系统调用时,可以免去很多权限检查。