mallocのパラメータが環境変数から設定される流れ

# はじめに

malloc関連のパラメータは`mallopt(3)`で変更することができます。
例えば、下記を実行することで、malloc内部でmmapを使うしきい値を1MBに変更することができます。

```
mallopt(M_MMAP_THRESHOLD, 1024*1024);
```

malloptで設定可能なパラメータのうちいくつかは、環境変数でも変更できます。
例えば、先述の`M_MMAP_THRESHOLD`は、`MALLOC_MMAP_THRESHOLD_`環境変数でも設定できます。

```
$ export MALLOC_MMAP_THRESHOLD_=1048576
$ ./a.out
```


環境変数であればプログラムの変更なしに適用できるので、システムのデフォルトを変更するときなどに有用です。
この環境変数によるmalloptの変更は、glibcの[Tunables](https://www.gnu.org/software/libc/manual/html_mono/libc.html#Tunables)という仕組みを使っています。

本記事ではglibcのtunablesが設定される流れを
`MALLOC_MMAP_THRESHOLD_`環境変数を例に備忘メモとして残しておきます。
誤りなどありましたら、ご指摘いただければ幸いです。

環境は以下のとおりです。

```
$ arch
x86_64
$ uname -r
5.4.0-42-generic
$ lsb_release -d
Description:	Ubuntu 18.04.4 LTS
$ dpkg -l | grep libc-bin
ii  libc-bin             2.27-3ubuntu1.2 amd64           GNU C Library: Binaries
```


# `MALLOC_MMAP_THRESHOLD_`環境変数が反映される流れ

## 1. ローダ(ld-linux.so)でauxiliary vectorを確認する

auxiliary vectorとは、`getauxval(3)`で取得することができる、
kernelのELF loaderから渡される実行ファイルの補助情報です。

ローダで、auxiliary vectorから`AT_SECURE`情報を取得し、
`AT_SECURE`が真の場合、global変数の`__libc_enable_secure`が立ちます。

ここで`AT_SECURE`は、少し勘違いしやすい名前ですが、
今のプロセスがセキュアに実行される必要があるということを意味します。
より具体的に言えば、下記のいずれかの場合に`AT_SECURE`が立ちます。

- set-user-ID もしくは set-group-ID が有効なプログラム
- capabilitiesを付与されたプログラム


```glibc-2.27/elf/dl-sysdep.c
ElfW(Addr)
_dl_sysdep_start (void **start_argptr,
		  void (*dl_main) (const ElfW(Phdr) *phdr, ElfW(Word) phnum,
				   ElfW(Addr) *user_entry, ElfW(auxv_t) *auxv))
{
...

  for (av = GLRO(dl_auxv); av->a_type != AT_NULL; set_seen (av++))
    switch (av->a_type)
      {
...
      case AT_SECURE:
        __libc_enable_secure = av->a_un.a_val;
        break;
...
      }

  __tunables_init (_environ);

...
  (*dl_main) (phdr, phnum, &user_entry, GLRO(dl_auxv));
  return user_entry;
}
```

## 2. ローダで、環境変数の値から`tunable_list[]`を更新

`__tunables_init`では、`tunable_list[]`の中にある項目が環境変数に含まれている場合、
`tunable_initialize()`で環境変数の値が反映されます。

ただし、`__libc_enable_secure`が立っている場合(=`AT_SECURE`が立っている場合)は、
以下の３種類の`security_level`に応じた処理が行われます。

- `SXID_ERASE`: `AT_SECURE`の場合、子プロセスが読まないように環境変数から削除する
- `SXID_IGNORE`: `AT_SECURE`の場合、無視する（環境変数には残る）
- `NONE`: 常に反映する


```glibc-2.27/elf/dl-tunables.c
void
__tunables_init (char **envp)
{
...
  while ((envp = get_next_env (envp, &envname, &len, &envval,
                               &prev_envp)) != NULL)
    {
...
      for (int i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
        {
          tunable_t *cur = &tunable_list[i];
...
              if (__libc_enable_secure)
                {
                  if (cur->security_level == TUNABLE_SECLEVEL_SXID_ERASE)
                    {
                      /* Erase the environment variable.  */
                      ...
                    }

                  if (cur->security_level != TUNABLE_SECLEVEL_NONE)
                    continue;
                }

              tunable_initialize (cur, envval);
              break;
            }
        }
    }
}
```


tunableの各項目の`security_level`は、環境変数名や型と併せて、`.list`ファイルで定義されています。


```glibc-2.27/elf/dl-tunables.list
glibc {
  malloc {
    ...
    mmap_threshold {
      type: SIZE_T
      env_alias: MALLOC_MMAP_THRESHOLD_
      security_level: SXID_IGNORE
    }
    ...
  }
}
```

`scripts/gen-tunables.awk`によって`dl-tunables.list`から`dl-tunable-list.h`を自動生成し、
そこに`tunable_list[]`が定義されます。

```dl-tunable-list.h
static tunable_t tunable_list[] attribute_relro = {
  ...
  {TUNABLE_NAME_S(glibc, malloc, mmap_threshold), {TUNABLE_TYPE_SIZE_T, 0, SIZE_MAX}, {}, NULL, TUNABLE_SECLEVEL_SXID_IGNORE, "MALLOC_MMAP_THRESHOLD_"},
  ...
};
```



## 3. 初回malloc時に、`tunable_list[]`から`mp_`に反映

ここからは、tunableの話ではなく、mallocパラメータ固有の話になります。
また、実行タイミングもローダではなく、普通のmain関数に入ってから初めてmallocが呼び出されたときです。

プログラムで初めてメモリ確保関数を呼び出したときには、`malloc_hook_ini`が呼び出され、
そこから`ptmalloc_init`を呼び出します。

```glibc-2.27/malloc/hooks.c
static void *
malloc_hook_ini (size_t sz, const void *caller)
{
  __malloc_hook = NULL;
  ptmalloc_init ();
  return __libc_malloc (sz);
}
```

`ptmalloc_init`では、2.で設定したtunable_listから、`mmap_threshold`を取得し、
mallocパラメータをまとめて保持する構造体`mp_`に設定します。


```glibc-2.27/malloc/arena.c
static void
ptmalloc_init (void)
{
...
  TUNABLE_GET (mmap_threshold, size_t, TUNABLE_CALLBACK (set_mmap_threshold));
...
}
```


```glibc-2.27/elf/dl-tunables.c
void
__tunable_get_val (tunable_id_t id, void *valp, tunable_callback_t callback)
{
  tunable_t *cur = &tunable_list[id];

  switch (cur->type.type_code)
    {
...
    case TUNABLE_TYPE_SIZE_T:
	{
	  *((size_t *) valp) = (size_t) cur->val.numval;
	  break;
	}
...

  if (cur->initialized && callback != NULL)
    callback (&cur->val);
}

```

実際に設定するのは`do_set_mmap_threshold`関数です。
ここを見ると実は設定値の上限があったり、
`mmap_threshold`を設定するときには動的なしきい値制御を無効にしていることがわかります。

```glibc-2.27/malloc/malloc.c
static inline int
__always_inline
do_set_mmap_threshold (size_t value)
{
  /* Forbid setting the threshold too high.  */
  if (value <= HEAP_MAX_SIZE / 2)
    {
      LIBC_PROBE (memory_mallopt_mmap_threshold, 3, value, mp_.mmap_threshold,
		  mp_.no_dyn_threshold);
      mp_.mmap_threshold = value;
      mp_.no_dyn_threshold = 1;
      return 1;
    }
  return 0;
}

```

これで晴れて、`mmap`するかどうかの判断に、`MALLOC_MMAP_THRESHOLD_`環境変数の値が使われるようになります。
めでたしめでたし。

```glibc-2.27/malloc/malloc.c
static void *
sysmalloc (INTERNAL_SIZE_T nb, mstate av)
{
...
  if (av == NULL
      || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
	  && (mp_.n_mmaps < mp_.n_mmaps_max)))
    {
...
          mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0));
    }
}
```


# おわりに

mallocパラメータを環境変数で設定する場合、デバッガなりでちゃんと設定が反映されているか確認したほうが良いですね。


# 参考
[mallocの動作を追いかける(環境変数編) - Qiita](https://qiita.com/kaityo256/items/fc9a27d0df1371246951)
[mallopt(3) - Linux manual page](https://man7.org/linux/man-pages/man3/mallopt.3.html)
[The GNU C Library](https://www.gnu.org/software/libc/manual/html_mono/libc.html)
[getauxval(3) - Linux manual page](https://man7.org/linux/man-pages/man3/getauxval.3.html)