Libseccomp: BUG:SCMP_CMP_GT/GE/LT/LE 对于负系统调用参数没有按预期工作

创建于 2017-01-18  ·  20评论  ·  资料来源: seccomp/libseccomp

你好!

我不确定 SCMP_CMP_GT/GE/LT/LE 的当前行为是否按预期工作,或者其实现中是否存在错误。 seccomp_rule_add的手册页仅对 SCMP_CMP_GT 进行了说明:

SCMP_CMP_GT:
        Matches when the argument value is greater than the datum value,
        example:

        SCMP_CMP( arg , SCMP_CMP_GT , datum )

手册页没有指定数据的类型,并且有各种(隐含)类型的示例(以及一个转换为 scmp_datum_t 的示例)。

根据手册页,我希望这样的事情适用于给 setpriority 的第三个参数的任何值(为此假设默认策略为 SCMP_ACT_ALLOW):

rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM),
        SCMP_SYS(setpriority),
        3,
        SCMP_A0(SCMP_CMP_EQ, PRIO_PROCESS),
        SCMP_A1(SCMP_CMP_EQ, 0),
        SCMP_A2(SCMP_CMP_GT, 0));

相反,当“-1”明显小于“0”时, setpriority(PRIO_PROCESS, 0, -1)导致系统调用被阻塞。 setpriority(PRIO_PROCESS, 0, 0)setpriority(PRIO_PROCESS, 0, 1)按预期工作。 正在发生的事情是,'-1' 被转换为 scmp_datum_t(来自 secomp.h.in 的 uint64_t),这当然使它成为正数,但 SCMP_CMP_GT 和朋友们没有处理这个转换。 SCMP_CMP_EQ 与负数据一起工作得很好(猜测数据仍然是正数(我没有验证),但比较是在转换后的 scmp_datum_t 之间)。

2.1.0+dfsg-1(Ubuntu 14.04 LTS,3.13 内核)、2.2.3-3ubuntu3(Ubuntu 16.04 LTS,4.9 内核)、2.3.1-2ubuntu2(Ubuntu 17.04 开发版,4.9 内核)和几分钟前的大师(在 Ubuntu 17.04 开发版,4.9 内核上),全部在 amd64 上。

AFAICT,没有针对 SCMP_CMP_GT 和 SCMP_CMP_LE 的测试。 SCMP_CMP_LT 的少数测试似乎没有考虑负值,SCMP_CMP_GE 的测试也没有(如果我错了,请纠正我)。

那么问题来了:这种行为是故意的吗? 如果是这样,虽然我承认可能会争辩说手册页是准确的,因为在理解 scmp_datum_t 是数据类型时这些工作完全正确,但这种情况并不是很清楚,手册页可能应该说应用程序需要考虑这。 否则,这似乎是 SCMP_CMP_GT/GE/LT/LE 实现中的错误。

下面是一个用 SCMP_CMP_GT 演示这个问题的小程序,尽管可以观察到 GE、LT 和 LE 都具有相同的行为:

/*
 * gcc -o test-nice test-nice.c -lseccomp
 * sudo ./test-nice 0 1  # should be denied
 * sudo ./test-nice 0 0  # should be allowed
 * sudo ./test-nice 0 -1 # should be allowed?
 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <fcntl.h>
#include <stdarg.h>
#include <seccomp.h>
#include <sys/resource.h>

int main(int argc, char **argv)
{
    if (argc < 3) {
        fprintf(stderr, "test-nice N N\n");
        return 1;
    }

    int rc = 0;
    scmp_filter_ctx ctx = NULL;
    int filter_n = atoi(argv[1]);
    int n = atoi(argv[2]);

    // Allow everything by default for this test
    ctx = seccomp_init(SCMP_ACT_ALLOW);
    if (ctx == NULL)
        return ENOMEM;

    printf("set EPERM for nice(>%d)\n", filter_n);
    rc = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM),
            SCMP_SYS(setpriority),
            3,
            SCMP_A0(SCMP_CMP_EQ, PRIO_PROCESS),
            SCMP_A1(SCMP_CMP_EQ, 0),
            SCMP_A2(SCMP_CMP_GT, filter_n));

    if (rc != 0) {
        perror("seccomp_rule_add failed");
        goto out;
    }

    rc = seccomp_load(ctx);
    if (rc != 0) {
        perror("seccomp_load failed");
        goto out;
    }

    // try to use the filtered syscall
    errno = 0;
    printf("Attempting nice(%d)\n", n);
    nice(n);
    if (errno != 0) {
        perror("could not nice");
        if (filter_n > n)
            fprintf(stderr, "nice(%d) unsuccessful. bug?\n", n);
        rc = 1;
        goto out;
    } else
        printf("nice(%d) successful\n", n);

out:
    seccomp_release(ctx);

    return rc;
}
bug prioritmedium

所有20条评论

感谢问题报告; 这是一个很好的。

无论如何,您是否尝试过使用内核样本/seccomp目录中的头文件/宏来编写复制器?

我的印象是内核中的 BPF 代码将立即值视为有符号的; 情况可能并非如此,或者我可能在 libseccomp 代码中搞砸了一些东西。

FWIW,BPF 本身使用 u32 作为参数。 libseccomp 是否对 compat 参数进行签名扩展? (它可能不应该,但是匹配“-1”的规则在 32 位和 64 位之间必须不同......)

现在让我担心的问题是跳转运算符中的 BPF GT/GE 比较,特别是因为我怀疑大多数人都将 BPF 立即数视为这些比较的有符号值。

@kees使用内核的 seccomp-bpf 机器对系统调用参数进行签名比较的推荐方法是什么? 我希望它不是“首先检查高位,然后在比较负数之前进行必要的二进制补码转换”。 虽然很烦人,但我们总是可以更改 libseccomp 以生成必要的 BPF(尽管在某些情况下生成的过滤器现在会更大),但我担心创建自己的 BPF 过滤器的应用程序; 他们正确处理这个问题的几率可能不是很好。

不幸的是,由于系统调用参数是“无符号长”(参见 syscall_get_arguments() 和 struct seccomp_data),系统调用如何处理符号转换没有任何常见的情况。 一些系统调用在跨越兼容障碍时会进行符号扩展,而其他系统调用 (prctl) 不会。 是否有很多负面但不减一的系统调用参数?

今天回到这个问题,今天早上玩了更多的东西,我认为这最终会成为一个文档/“小心!” 问题,因为没有好的解决方案,尤其是当我们谈论现有用户时。 让我尝试提供一些 libseccomp 背景/解释,以配合@kees来自内核方面的有用评论。

FWIW,BPF 本身使用 u32 作为参数。 libseccomp 是否对 compat 参数进行签名扩展? (它可能不应该,但是匹配“-1”的规则必须在 32[-bit 和 64-bit 之间有所不同......)

libseccomp API 规则函数将所有立即值解释为 _uint64_t_,因此如果您对类型/转换不小心,您可能会遇到问题。 例子:

$ cat 00-test.c
    /* ... */
    seccomp_rule_add_exact(ctx, SCMP_ACT_KILL, 1000, 1,
                           SCMP_A0(SCMP_CMP_GT, -1));
    seccomp_rule_add_exact(ctx, SCMP_ACT_KILL, 1001, 1,
                           SCMP_A0(SCMP_CMP_GT, (uint32_t)-1));
    seccomp_rule_add_exact(ctx, SCMP_ACT_KILL, 1002, 1,
                           SCMP_A0(SCMP_CMP_GT, 0xffffffff));
    /* ... */
$ make 00-test
  CC       00-test.o
  CCLD     00-test
$ ./00-test -p
  #
  # pseudo filter code start
  #
  # filter for arch x86_64 (3221225534)
  if ($arch == 3221225534)
    # filter for syscall "UNKNOWN" (1002) [priority: 65533]
    if ($syscall == 1002)
      if ($a0.hi32 >= 0)
        if ($a0.lo32 > 4294967295)
          action KILL;
    # filter for syscall "UNKNOWN" (1001) [priority: 65533]
    if ($syscall == 1001)
      if ($a0.hi32 >= 0)
        if ($a0.lo32 > 4294967295)
          action KILL;
    # filter for syscall "UNKNOWN" (1000) [priority: 65533]
    if ($syscall == 1000)
      if ($a0.hi32 >= 4294967295)
        if ($a0.lo32 > 4294967295)
          action KILL;
    # default action
    action ALLOW;
  # invalid architecture action
  action KILL;
  #
  # pseudo filter code end
  # 
$ ./00-test -b | ../tools/scmp_bpf_disasm 
   line  OP   JT   JF   K
  =================================
   0000: 0x20 0x00 0x00 0x00000004   ld  $data[4]
   0001: 0x15 0x00 0x0c 0xc000003e   jeq 3221225534 true:0002 false:0014
   0002: 0x20 0x00 0x00 0x00000000   ld  $data[0]
   0003: 0x35 0x0a 0x00 0x40000000   jge 1073741824 true:0014 false:0004
   0004: 0x15 0x00 0x02 0x000003e8   jeq 1000 true:0005 false:0007
   0005: 0x20 0x00 0x00 0x00000014   ld  $data[20]
   0006: 0x35 0x04 0x06 0xffffffff   jge 4294967295 true:0011 false:0013
   0007: 0x15 0x01 0x00 0x000003e9   jeq 1001 true:0009 false:0008
   0008: 0x15 0x00 0x04 0x000003ea   jeq 1002 true:0009 false:0013
   0009: 0x20 0x00 0x00 0x00000014   ld  $data[20]
   0010: 0x35 0x00 0x02 0x00000000   jge 0    true:0011 false:0013
   0011: 0x20 0x00 0x00 0x00000010   ld  $data[16]
   0012: 0x25 0x01 0x00 0xffffffff   jgt 4294967295 true:0014 false:0013
   0013: 0x06 0x00 0x00 0x7fff0000   ret ALLOW
   0014: 0x06 0x00 0x00 0x00000000   ret KILL

...正如我们所见,如果您使用适当的强制转换,则该值不会进行符号扩展。 但是,我希望这不是大多数人正在做的事情。 好消息是,我认为采用否定参数的系统调用数量相对较少,因此影响应该是有限的。

展望未来,我们肯定需要在文档中添加一些关于此的内容,看看我们是否可以做一些事情来让开发人员的生活更轻松,也许可以实现 _SCMP_A*_ 宏的 32 位变体。

@pcmoore - 感谢您的详细回复,很抱歉没有早点回来。 不,我还没有尝试编写基于https://github.com/torvalds/linux/tree/master/samples/seccomp的复制器,但根据您的反馈,听起来我不需要。 需要帮助请叫我。 现在,我将采取“小心”的方法,如果我有任何问题,我会向您报告,并期待您将来如何更轻松地解决问题。

@jdstrand我想我们暂时都准备好了。 再次感谢您的报告,很抱歉我没有更好的答案给您,但希望我们将来会有一些东西。

同时,如果您在使用正确的类型转换值时遇到任何问题,请随时更新此问题。

好消息是,我认为采用否定参数的系统调用数量相对较少,因此影响应该是有限的。

我只是在检查(除其他外)openat() 的 fd 参数是否等于特殊值 AT_FDCWD 时遇到了这个问题,即 -100。 这导致:

  # filter for syscall "openat" (257) [priority: 131067]
  if ($syscall == 257)
    if ($a0.hi32 == 4294967295)
      if ($a0.lo32 == 4294967196)
        if ($a2.hi32 & 0x00000000 == 0)
          if ($a2.lo32 & 0x00000003 == 0)
            action ERRNO(2);

应该在哪里:

  # filter for syscall "openat" (257) [priority: 131067]
  if ($syscall == 257)
    if ($a0.hi32 == 0)
      if ($a0.lo32 == 4294967196)
        if ($a2.hi32 & 0x00000000 == 0)
          if ($a2.lo32 & 0x00000003 == 0)
            action ERRNO(2);

由于 glibc 2.26+ 似乎专门使用带有 AT_FDCWD 的 openat 系统调用来实现 open() 这可能会绊倒很多人。 按照上面的建议对 uint32_t 应用强制转换为我解决了这个问题:

        // selector, action, syscall, no of args, args
        { SEL, SCMP_ACT_ERRNO(ENOENT), "openat", 2,
-               { SCMP_A0(SCMP_CMP_EQ, AT_FDCWD), /* glibc 2.26+ */
+               { SCMP_A0(SCMP_CMP_EQ, (uint32_t)AT_FDCWD), /* glibc 2.26+ */
                  SCMP_A2(SCMP_CMP_MASKED_EQ, O_ACCMODE, O_RDONLY) }},

一个明确的 SCMP_A0_U32 肯定会很好。

@drakenclimber @jdstrand @michaelweiser你们怎么看https://github.com/pcmoore/misc-libseccomp/commit/b9ce39d776ed5a984c7e9e6db3b87463edce82a7作为解决这个问题?

@pcmoore :感谢您继续调查此事! 我只是试了一下,它在代码中看起来非常好:

static struct {
        const uint64_t promises;
        const uint32_t action;
        const char *syscall;
        const int arg_cnt;
        const struct scmp_arg_cmp args[3];
} scsb_calls[] = {
[...]
        { PLEDGE_WPATH, SCMP_ACT_ALLOW, "openat", 2, /* glibc 2.26+ */
                { SCMP_A0_32(SCMP_CMP_EQ, AT_FDCWD),
                  SCMP_A2_64(SCMP_CMP_MASKED_EQ, O_ACCMODE, O_WRONLY) }},

不幸的是,辅助函数似乎不适合作为 struct 初始值设定项:

In file included from pledge.c:42:
/include/seccomp.h:230:26: error: initializer element is not constant
 #define SCMP_CMP32(...)  (__scmp_arg_32(SCMP_CMP64(__VA_ARGS__)))
                          ^
/include/seccomp.h:241:26: note: in expansion of macro ‘SCMP_CMP32’
 #define SCMP_A0_32(...)  SCMP_CMP32(0, __VA_ARGS__)
                          ^~~~~~~~~~
pledge.c:188:5: note: in expansion of macro ‘SCMP_A0_32’
   { SCMP_A0_32(SCMP_CMP_EQ, AT_FDCWD),
     ^~~~~~~~~~
/include/seccomp.h:230:26: note: (near initialization for ‘scsb_calls[21].args[0]’)
 #define SCMP_CMP32(...)  (__scmp_arg_32(SCMP_CMP64(__VA_ARGS__)))
                          ^
/include/seccomp.h:241:26: note: in expansion of macro ‘SCMP_CMP32’
 #define SCMP_A0_32(...)  SCMP_CMP32(0, __VA_ARGS__)
                          ^~~~~~~~~~
pledge.c:188:5: note: in expansion of macro ‘SCMP_A0_32’
   { SCMP_A0_32(SCMP_CMP_EQ, AT_FDCWD),
     ^~~~~~~~~~

感谢@michaelweiser的评论,不幸的是,我不认为人们使用这个宏作为初始化器,但这是一个有效的用途,肯定有一些人以这种方式使用它。

我需要考虑一下这个问题……您对如何以优雅的方式解决这个问题有什么想法吗?

不知道,对不起,我已经用火柴撑开了眼睛。 :)

现在看看它,我想我看到了问题:由于变量参数列表,您无法注入必要的强制转换,对吗?

scmp_arg_cmp 是否可能包含一个联合,以正确的宽度、对齐方式(甚至可能是字节顺序)对数据提供不同的视图(IMO 有点与“优雅”冲突):? 如果它纯粹是 libseccomp 内部的并且不需要与内核接口兼容,它是否可以将数据类型指示符作为单独的字段携带并让用户函数对其进行排序? 甚至可以使用可变参数初始化吗?

否则,不是将操作标记为整个 32/64 位,而是可以对操作数进行注释,包装强制转换并向用户提供严厉的建议(就像您一样)始终使用这些注释来惩罚遇到难以调试的问题?

{ SCMP_A0(SCMP_CMP_EQ, SCMP_OP_32(AT_FDCWD)),
  SCMP_A2(SCMP_CMP_MASKED_EQ, SCMP_OP_64(O_ACCMODE), SCMP_OP_64(O_WRONLY)) }},

要么

{ SCMP_A0(SCMP_CMP_EQ, SCMP_OP1_32(AT_FDCWD)),
  SCMP_A2(SCMP_CMP_MASKED_EQ, SCMP_OP2_64(O_ACCMODE, O_WRONLY)) }},

我没有足够的预处理器破解来想出更多,对不起。

@pcmoore ,这些变化对我来说看起来不错。 我不是预处理器专家,但我会看看@michaelweiser上面提到的问题

现在看看它,我想我看到了问题:由于变量参数列表,您无法注入必要的强制转换,对吗?

是的,差不多就是这样。 也许有一种不太可怕的方法,但我还没有找到。

scmp_arg_cmp 是否可能包含一个联合,以正确的宽度、对齐方式(甚至可能是字节顺序)对数据提供不同的视图(IMO 有点与“优雅”冲突):? 如果它纯粹是 libseccomp 内部的并且不需要与内核接口兼容,它是否可以将数据类型指示符作为单独的字段携带并让用户函数对其进行排序? 甚至可以使用可变参数初始化吗?

我们有一个问题,即 scmp_arg_cmp 结构体是 libseccomp API 的一部分,所以除非我们想提升 libseccomp 主要版本,否则我们无法真正更改结构体的大小或任何成员字段的偏移量; 这样做会破坏现有应用程序的现有二进制接口。 将 64 位数据字段转换为包含 64 位或 32 位值的联合本身应该没问题,但您还需要向 scmp_arg_cmp 结构添加一些附加信息以指示要使用哪个联合成员; 正是这个额外的标志可能有问题。

可能会从“arg”或“op”字段中窃取一些位,两者都是 32 位值并且仅使用该空间的一小部分。 但是,我认为这是一个相当极端的选择,如果可能的话,我想避免这种情况。

否则,不是将操作标记为整个 32/64 位,而是可以对操作数进行注释,包装强制转换并向用户提供严厉的建议(就像您一样)始终使用这些注释来惩罚遇到难以调试的问题?

我不太明白用宏包装操作数会得到什么,你能详细说明一下吗? 我们可以提供一个宏来包装数据值,但这与仅仅要求调用者提供正确的转换并没有什么不同。

@pcmoore ,这些变化对我来说看起来不错。 我不是预处理器专家,但我会看看@michaelweiser上面提到的问题

万分感谢。 希望我们三个人之间可以提出一些有用的东西。

@pcmoore :查看http://efesx.com/2010/07/17/variadic-macro-to-count-number-of-arguments/http://efesx.com/2010/08/31/overloading-宏/我想出了以下内容:

#define VA_NUM_ARGS(...) VA_NUM_ARGS_IMPL(__VA_ARGS__, 5,4,3,2,1)
#define VA_NUM_ARGS_IMPL(_1,_2,_3,_4,_5,N,...) N
#define macro_dispatcher(func, ...) \
            macro_dispatcher_(func, VA_NUM_ARGS(__VA_ARGS__))
#define macro_dispatcher_(func, nargs) \
            macro_dispatcher__(func, nargs)
#define macro_dispatcher__(func, nargs) \
            func ## nargs

#define SCMP_CMP64(...)         ((struct scmp_arg_cmp){__VA_ARGS__})

#define SCMP_CMP32_1(x)                 SCMP_CMP64(x)
#define SCMP_CMP32_2(x, y)              SCMP_CMP64(x, y)
#define SCMP_CMP32_3(x, y, z)           SCMP_CMP64(x, y, (uint32_t)(z))
#define SCMP_CMP32_4(x, y, z, q)        SCMP_CMP64(x, y, (uint32_t)(z), (uint32_t)(q))
#define SCMP_CMP32(...) macro_dispatcher(SCMP_CMP32_, __VA_ARGS__)(__VA_ARGS__)

#define SCMP_A0_64(...)         SCMP_CMP64(0, __VA_ARGS__)
#define SCMP_A0_32(...)         SCMP_CMP32(0, __VA_ARGS__)

对于这个测试用例:

        struct scmp_arg_cmp f[] = {
                SCMP_A0_64(SCMP_CMP_EQ, 1, 20),
                SCMP_A0_32(SCMP_CMP_EQ, 2, 3),
                SCMP_A0_32(SCMP_CMP_LT, 2),
        };

它来自gcc-7.4.0 -Eclang-7 -E为:

 struct scmp_arg_cmp f[] = {
  ((struct scmp_arg_cmp){0, SCMP_CMP_EQ, 1, 20}),
  ((struct scmp_arg_cmp){0, SCMP_CMP_EQ, (uint32_t)(2), (uint32_t)(3)}),
  ((struct scmp_arg_cmp){0, SCMP_CMP_LT, (uint32_t)(2)}),
 };

假设SCMP_A[0-5]_43至少需要op才能工作并且SCMP_CMP32需要arg ,可以通过将这些参数设置为位置来节省两行:

#define SCMP_CMP32_1(x, y, z)           SCMP_CMP64(x, y, (uint32_t)(z))
#define SCMP_CMP32_2(x, y, z, q)        SCMP_CMP64(x, y, (uint32_t)(z), (uint32_t)(q))
#define SCMP_CMP32(x, y,...)            macro_dispatcher(SCMP_CMP32_, __VA_ARGS__)(x, y, __VA_ARGS__)

#define SCMP_A0_32(x,...)       SCMP_CMP32(0, x, __VA_ARGS__)

干得好@michaelweiser! 您是否想将 PR 放在一起,以便我们可以更轻松地审查/评论更改? 如果没有,那完全没问题,我会把一个放在一起,确保你得到足够的信任:)

我会做今晚的公关项目。 在https://github.com/pcmoore/misc-libseccomp/commit/b9ce39d776ed5a984c7e9e6db3b87463edce82a7之上还是从头开始?
我们如何赞扬 Blogger Roman 的重载解决方案? 在https://kecher.net/overloading-macros/ 上找到了似乎是他博客的当前主页macro_dispatcher逻辑上方帖子的链接?

我会做今晚的公关项目。 在pcmoore@b9ce39d 之上还是从头开始?

太好了谢谢! 继续并将其建立在 master 分支的基础上,我从未将这些内容合并到我的 misc-libseccomp 树中,并且此时不打算合并,因为您的方法要好得多。

我们如何赞扬 Blogger Roman 的重载解决方案? 在https://kecher.net/overloading-macros/ 上找到了似乎是他博客的当前主页macro_dispatcher逻辑上方帖子的链接?

我们通常不会直接在来源中信任人,除非有一些许可要求; 我建议在补丁描述中添加评论,将基本思想归功于 Roman 并提供指向他的博客文章的链接。 我没有看到他的例子有任何许可或限制,所以我认为在这方面没有任何问题,并且根据他博客的一个样本,我相信他的意图是与其他人(比如我们)分享这些想法) 来帮助他们解决问题。 如果您有 Roman 的电子邮件地址,您可以随时尝试向他发送电子邮件; 如果我们因任何原因无法联系到他,我认为可以继续。

通过 80a987d6f8d0152def07fa90ace6417d56eea741 解决。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

cgwalters picture cgwalters  ·  14评论

vasanthaganeshk picture vasanthaganeshk  ·  9评论

drakenclimber picture drakenclimber  ·  10评论

erdumbledore picture erdumbledore  ·  3评论

alban picture alban  ·  5评论