Libseccomp: ОШИБКА: SCMP_CMP_GT / GE / LT / LE не работает должным образом для отрицательных аргументов системного вызова

Созданный на 18 янв. 2017  ·  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).

Основываясь на справочной странице, я ожидал, что что-то вроде этого будет работать для любого значения, заданного 3-му аргументу 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));

Вместо этого setpriority(PRIO_PROCESS, 0, -1) приводит к блокировке системного вызова, когда «-1» явно меньше «0». setpriority(PRIO_PROCESS, 0, 0) и setpriority(PRIO_PROCESS, 0, 1) работают должным образом. Происходит то, что «-1» преобразуется в scmp_datum_t (uint64_t из secomp.h.in), что, конечно, делает его положительным, но 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 dev, ядро ​​4.9) и master несколько минут назад (в Ubuntu 17.04 dev, ядро ​​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 Комментарий

Спасибо за сообщение о проблеме; этот подходит.

Вы случайно не пробовали написать репродуктор с использованием заголовков / макросов в каталоге ядра samples / 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 вместе с полезными комментариями

FWIW, сам BPF использует u32 в качестве аргументов. Подписывает ли libseccomp расширение для аргументов compat? (Вероятно, этого не должно быть, но тогда правила соответствия "-1" должны быть разными для 32 [-битных и 64-битных ...)

Все функции правил API libseccomp интерпретируют все немедленные значения как _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

... как мы видим, если вы используете соответствующее приведение, значение не расширяется по знаку. Однако я полагаю, что это не то, что делает большинство людей. Хорошая новость заключается в том, что я полагаю, что количество системных вызовов, принимающих отрицательные аргументы, относительно невелико, поэтому их влияние должно быть несколько ограниченным.

Двигаясь вперед, нам определенно нужно внести что-то в документацию по этому поводу и посмотреть, можем ли мы сделать что-то, чтобы облегчить жизнь разработчикам, возможно, реализовать 32-битные варианты макросов _SCMP_A * _.

@pcmoore - спасибо за подробный ответ и извините, что не вернулись раньше. Нет, я еще не пробовал писать репродуктор на основе https://github.com/torvalds/linux/tree/master/samples/seccomp , но, судя по вашим отзывам, мне кажется, что в этом нет необходимости. Позвольте мне знать, если вам нужно что-нибудь еще. На данный момент я буду придерживаться подхода «будьте осторожны» и сообщу, если у меня возникнут какие-либо проблемы, и с нетерпением жду, как вы можете упростить это в будущем.

@jdstrand Я думаю, что на данный момент мы все настроены. Еще раз спасибо за отчет, извините, у меня не было лучшего ответа для вас, но, надеюсь, у нас что-то будет в будущем.

Между тем, если вы однажды столкнетесь с какими-либо проблемами с правильным значением приведения типа, пожалуйста, не стесняйтесь обновить эту проблему.

Хорошая новость заключается в том, что я полагаю, что количество системных вызовов, принимающих отрицательные аргументы, относительно невелико, поэтому их влияние должно быть несколько ограниченным.

Я столкнулся с этой проблемой при проверке (среди прочего), равен ли параметр fd openat () специальному значению 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+, похоже, использует исключительно системный вызов openat с AT_FDCWD для реализации 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) }},

К сожалению, вспомогательная функция не подходит в качестве инициализатора структуры:

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 и не должен быть совместим с интерфейсом ядра, может ли он содержать индикатор типа данных в виде отдельного поля и позволять пользовательским функциям разбираться с ним? И можно ли инициализировать это с помощью varargs?

В противном случае, вместо того, чтобы отмечать операцию в целом 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 и не должен быть совместим с интерфейсом ядра, может ли он содержать индикатор типа данных в виде отдельного поля и позволять пользовательским функциям разбираться с ним? И можно ли инициализировать это с помощью varargs?

У нас есть проблема в том, что структура 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 -E и clang-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, чтобы мы могли легче рассмотреть / прокомментировать изменения? Если нет, то ничего страшного, я брошу один и позабочусь о том, чтобы у вас было много кредитов :)

Я сделаю PR-проект сегодня вечером. Поверх https://github.com/pcmoore/misc-libseccomp/commit/b9ce39d776ed5a984c7e9e6db3b87463edce82a7 или с нуля?
Как мы можем похвалить блоггера Романа за его решение по перегрузке? Нашел то, что кажется текущим домом его блога по адресу https://kecher.net/overloading-macros/. Его пост выглядит вполне оригинальным. Комментарий со ссылкой на пост над логикой macro_dispatcher ?

Я сделаю PR-проект сегодня вечером. Поверх pcmoore @ b9ce39d или с нуля?

Отлично, спасибо! Идите вперед и основывайте его на главной ветке, я никогда не объединял вещи в своем дереве misc-libseccomp и не планирую на этом этапе, поскольку ваш подход намного лучше.

Как мы можем похвалить блоггера Романа за его решение по перегрузке? Нашел то, что кажется текущим домом его блога по адресу https://kecher.net/overloading-macros/. Его пост выглядит вполне оригинальным. Комментарий со ссылкой на пост над логикой macro_dispatcher ?

Обычно мы не указываем людей прямо в источнике, если нет каких-либо лицензионных требований; Я бы порекомендовал добавить комментарий в описании патча, в котором Роман зачисляет основную идею и дает ссылку на его сообщение в блоге. Я не вижу каких-либо лицензий или ограничений на его примеры, поэтому я не считаю, что в этом отношении есть какие-либо проблемы, и, основываясь на выборке из его блога, я считаю, что он намерен поделиться этими идеями с другими (такими как мы ), чтобы помочь им решить свои проблемы. Если у вас есть адрес электронной почты для Романа, вы всегда можете попробовать отправить ему письмо; Если мы не можем связаться с ним по какой-либо причине, думаю, можно продолжить.

Разрешено через 80a987d6f8d0152def07fa90ace6417d56eea741.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги