第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,非特权用户只能设置 它们自己所拥有文件的权限】 。特权用户可以设置任何文件的权限位。下面是一个例子:
第9章 进程凭证
正如上例所示,程序可以同时对这两个位进行设置,尽管这并不常见。当使用 ls -l 查看程序(文件)的权限时,如果程序设置了set-user-ID或set-group-ID权限位,那么通常用于表示 可执行(execute) 权限的位(x)会被s替代:
第9章 进程凭证
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)文件系统的 mountunmount命令;用于在不同用户ID下运行shell的 su命令。set-group-ID程序的一个例子是:wall,用于将消息写入到所属组是tty组的所有终端中(通常,所有终端都属于这个组)。
在8.5节中,我们注意到Listing 8-2中的程序需要使用root账号登录后才能访问/etc/shadow文件。我们可以通过将该程序设置为set-user-ID-root程序,这样任意的用户都能运行。如下:
第9章 进程凭证
第9章 进程凭证

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-IDsaved set-group-ID 是为了set-user-ID程序和set-group-ID程序而设计的。当程序执行时,会有以下步骤:

  1. 开启了可执行文件的 【如果可执行文件开启了】 set-user-ID(set-group-ID)权限位。那么进程的有效用户(组)ID会被设置成可执行文件的属主。若未 设置set-user-ID(set-group-ID)权限位 【如果set-user-ID(set-group-ID)位没有设置】 ,那么进程的有效用户(组)ID保持不变。
  2. 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将变为:
第9章 进程凭证

有不少 【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 IDsaved 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 文件中的 UIDGIDGroups 来查看进程凭证。UID(GID)中的标识符分别表示:实际用户ID(实际组ID)、有效用户ID(有效组ID)、saved set-user-ID (saved set-group-ID)和文件系统用户ID(文件系统组ID)。
第9章 进程凭证

下面章节中,我们使用 有效用户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()有下列规则:

  1. 当非特权进程调用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的值。
  2. 当特权进程执行setuid(),传入一个非0参数,那么 实际用户ID有效用户IDsaved 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进行改变时,会有以下规则:

  1. 非特权进程只能将有效ID设置成相应的实际ID和save set ID。(换句话说,对于非特权进程,seteuid()和seteuid()分别与setuid()和setgid()的效果相同吗,之前介绍的BSD移植性问题。)
  2. 特权进程可以将有效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()也是类似的。

  1. 非特权进程只能将实际用户ID设置成当前的实际用户ID(即保持不变)或有效用户ID的值。有效用户ID只能设置成当前实际用户ID、有效用户ID(即保持不变)或者saved set-user-ID。
  2. 特权进程可以对这些IDs做任何改变。
  3. 只要下面条件成立,不管是特权进程还是非特权进程,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()类似)的规则如下:

  1. 非特权进程可以将实际用户ID、有效用户ID和saved set-user-ID的任意ID设置成当前的实际用户ID、有效用户ID或者saved set-user-ID中的任意值。
  2. 特权进行可以对实际用户ID、有效用户ID和saved set-user-ID做任何改变。
  3. 不管调用是否对其他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类似):

  1. 非特权进程可以将文件系统用户ID设置成当前的实际用户ID、有效用户ID、文件系统用户ID (即保持不变)或者saved set-user-ID的值。
  2. 特权进程可以将文件系统用户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才加入的。
    第9章 进程凭证

除此之外,应用还可以在调用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章 进程凭证
第9章 进程凭证

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的进程是具有特权的——也就是说,当进程执行各种系统调用时,可以免去很多权限检查。