Closed30

AWS Lambda 上で Net::HTTP::Post による multipart Upload が失敗する件

ピン留めされたアイテム
snakasnaka

問題

AWS Lambda 上のRubyの関数経由で API を叩くプログラムが以下のエラーで落ちた

system temporary path is world-writable: /tmp
/tmp is world-writable: /tmp
. is not writable: /var/task

Error: could not find a temporary directory

Error の前のメッセージは warning っぽい

snakasnaka

コードはおよそ以下のようになっていた

require 'net/http'

def send_attachment(attachment)
  post = Net::HTTP::Post.new(uri)

  attachment_body = attachment.body.decoded.force_encoding('ASCII-8BIT')
  form_data = [
    ['file', StringIO.new(attachment_body), { filename: attachment.filename, content_type: attachment.mime_type }]
  ]
  post.set_form form_data, 'multipart/form-data'

  Net::HTTP.start('example.com', 80, use_ssl: true) do |http|
    http.request(post)
  end
end
ピン留めされたアイテム
snakasnaka

環境

  • Lambda Runtime
    • Ruby 3.3
  • Architecture
    • x86_64
ピン留めされたアイテム
snakasnaka

原因

  • Ruby 側で /tmp をテンポラリディレクトリとして利用しようとしているが、ディレクトリのパーミッションが world-writable [1] である

ワークアラウンド

方針

  • 独自にディレクトリを設定して、wold-writable ではない パーミッションを設定する

例えば以下のように /tmp 配下に private-tmp というディレクトリを作って chmod でパーミッションの設定を行う

# 専用の temp ディレクトリを作っておく
PRIVATE_TMP = '/tmp/private-tmp'
ENV['TMPDIR'] = PRIVATE_TMP

if Dir.exist?(PRIVATE_TMP)
    puts "#{PRIVATE_TMP} already exist"
else
    FileUtils.mkdir(PRIVATE_TMP, mode: 0700)
end

def lambda_handler(event:, context:)
    # `TMPDIR` 環境変数を利用するので問題なし
    p Dir.tmpdir
end
脚注
  1. 他のユーザ・他のグループから読み(r)/書き(w)/実行(x)可能で、かつ、sticky bit が立っていない状態を表しているらしい ↩︎

ピン留めされたアイテム
snakasnaka

以下は調査の経緯

snakasnaka

エラーメッセージについて調べる

以下によると、セキュリティ観点から temporary ファイルのパーミッションをチェックして、適切ではないパーミッションが設定された場所に temporary ファイルが作成できないようになったらしい。

https://bugs.ruby-lang.org/issues/9156

snakasnaka

コンテナについて調べる

AWS Lambda の Ruby ランタイムのコンテナがどのような設定になっているか調べる

https://github.com/aws/aws-lambda-base-images

上記リポジトリを clone して、以下で Ruby ランタイムのブランチに切り替え

git checkout ruby3.3

以下でコンテナイメージをビルド

docker build -t ruby3.3:local -f Dockerfile.ruby3.3 .
snakasnaka

Git LFS でファイルをローカルにダウンロードする

git lsf pull
snakasnaka

ダウンロードしたファイルの中身も確認しておく

tar -tf x86_64/0767a5530547b0a2f1305ecdf29d2238c9dba99bfcda97a7d589dfdef3c1bd16.tar.xz | less

Ruby 関連のコマンドやライブラリなどが含まれている

var/lang
var/lang/bin
var/lang/bin/bundle
var/lang/bin/bundle.lock
var/lang/bin/bundler
var/lang/bin/bundler.lock
var/lang/bin/erb
var/lang/bin/gem
var/lang/bin/irb
var/lang/bin/racc
var/lang/bin/racc.lock
var/lang/bin/rake
var/lang/bin/rake.lock
var/lang/bin/rbs
var/lang/bin/rbs.lock
var/lang/bin/rdbg
var/lang/bin/rdbg.lock
var/lang/bin/rdoc
var/lang/bin/ri
var/lang/bin/ruby
... (略) ...
snakasnaka
tar -tf x86_64/4a1a18ec974289442c220349379879e0be174492cd21be651168f2efd7f6da58.tar.xz | less

こっちは Gem

var/runtime/extensions
var/runtime/gems
var/runtime/gems/aws_lambda_ric-3.0.0
var/runtime/gems/aws_lambda_ric-3.0.0/Gemfile
var/runtime/gems/aws_lambda_ric-3.0.0/LICENSE
var/runtime/gems/aws_lambda_ric-3.0.0/NOTICE
var/runtime/gems/aws_lambda_ric-3.0.0/README.md
var/runtime/gems/aws_lambda_ric-3.0.0/aws_lambda_ric.gemspec
... (略) ...
snakasnaka
tar -tf x86_64/723857be1338657a062a5b40f529ea594c12dcfe3e50c2b10a00c611012f910b.tar.xz

entrypoint の shell script

tar: Archive entry has empty or unreadable filename ... skipping.
lambda-entrypoint.sh
tar: Error exit delayed from previous errors.

ワーニングらしきメッセージはよくわからない

snakasnaka
tar -tf x86_64/afa9570d8e88fff239a8db4b528a895f8485cc6868351b6d92df684fb821a2db.tar.xz

ライセンス関連?

tar: Archive entry has empty or unreadable filename ... skipping.
THIRD-PARTY-LICENSES.txt
tar: Error exit delayed from previous errors.
snakasnaka
tar -tf x86_64/e4795398be36b75eb725e1e5505fafe2fa7910af23f745f97b5b083fc9aa1838.tar.xz

usr/local/bin のなにか

tar: Archive entry has empty or unreadable filename ... skipping.
usr
usr/local
usr/local/bin
usr/local/bin/aws-lambda-rie
tar: Error exit delayed from previous errors.

途中で出力が止まってるのかも?

snakasnaka
tar -tf x86_64/e58e411439bf2f8971d86988b770e3611f50d9c7179720908bea0efafbce3688.tar.xz | less

/usr/bin 配下のLinux標準コマンド群 shls 普通にあるな

./usr/bin/ls

./usr/bin/sh
snakasnaka

コンテナの /tmp ディレクトリの状態を調べる

entrypoint を迂回して sh でコンテナの中を確認する

$ docker run --rm -it --entrypoint '' ruby3.3:local ls -l /
... (略) ...
drwxrwxrwt   2 root root   4096 Jun 28 01:01 tmp

sticky bit は問題なさげ

stat コマンドでも

$ docker run --rm -it --entrypoint '' ruby3.3:local stat -c %a /tmp
1777

問題なさそう

snakasnaka

だとすると... tmpdir で warn "#{name} is world-writable: #{dir}" このワーニングが出る理由がわからない... 🤔

snakasnaka

完全に想像だけど、コンテナランタイムによって tempfs の挙動が違うということがあるかもしれない。
と、以下の runc の Issue を見て思った。

https://github.com/opencontainers/runc/issues/3952

もしそうであれば、ローカル (Docker) では sticky bit 立っているように見えるが、AWS Lambda で実行している際には sticky bit が立っていないように振る舞う(まだ確認していないのでこのような表現になっている)という現象に説明はつく。

snakasnaka
snakasnaka

Firecracker はコンテナランタイムではなく、microVM と呼ばれている超軽量でVMを高速に起動することができる実行基盤で、Fargate や Lambda で利用されているらしい ( 記事が 2018年のものなので 2024年現在もそうなのかは不明 )

snakasnaka

AWS Lambda 実行環境を確認する

AWS Lambda で以下のような関数を実行してコンテナから見た /tmp の様子を確認する

def lambda_handler(event:, context:)
    puts "#### Running Lambda ####"
    puts `ls -l /`
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end

実行結果

total 76
-rw-r--r--   1 root root 14375 Jan  1  2000 THIRD-PARTY-LICENSES.txt
lrwxrwxrwx   1 root root     7 Jan 30  2023 bin -> usr/bin
dr-xr-xr-x   2 root root  4096 Jan 30  2023 boot
drwxr-xr-x   2 root root   200 Aug 16 06:07 dev
drwxr-xr-x  34 root root  4096 May 10 11:16 etc
drwxr-xr-x   2 root root  4096 Jan 30  2023 home
-rwxr-xr-x   1 root root   397 Jul 31 08:50 lambda-entrypoint.sh
lrwxrwxrwx   1 root root     7 Jan 30  2023 lib -> usr/lib
lrwxrwxrwx   1 root root     9 Jan 30  2023 lib64 -> usr/lib64
drwxr-xr-x   2 root root  4096 Apr 25 19:25 local
drwxr-xr-x   2 root root  4096 Jan 30  2023 media
drwxr-xr-x   2 root root  4096 Jan 30  2023 mnt
drwxr-xr-x   2 root root  4096 Jan 30  2023 opt
dr-xr-xr-x 124 root root     0 Aug 16 06:14 proc
dr-xr-x---   2 root root  4096 Jan 30  2023 root
drwxr-xr-x   5 root root  4096 Apr 25 19:25 run
lrwxrwxrwx   1 root root     8 Jan 30  2023 sbin -> usr/sbin
drwxr-xr-x   2 root root  4096 Jan 30  2023 srv
drwxr-xr-x   2 root root  4096 Apr 25 19:25 sys
drwxrwxrwx   2 root root  4096 Jun  4 16:07 tmp
drwxr-xr-x  12 root root  4096 Jul 31 08:47 usr
drwxr-xr-x  24 root root  4096 May  9 21:46 var

やはり tmp の sticky bit は立っていない。

Lambda Function の Runtime settings は以下

  • Runtime: Ruby 3.3
  • ArchitectureInfo: x86_64
snakasnaka

Dir.tmpdir を実行してみる

def lambda_handler(event:, context:)
    # TODO implement
    puts "#### Running Lambda ####"
    p Dir.tmpdir
    { statusCode: 200, body: JSON.generate('Hello from Lambda!') }
end

エラーが再現した

system temporary path is world-writable: /tmp
/tmp is world-writable: /tmp
. is not writable: /var/task
Error raised from handler method
{
  "errorMessage": "could not find a temporary directory",
  "errorType": "Function<ArgumentError>",
  "stackTrace": [
    "/var/lang/lib/ruby/3.3.0/tmpdir.rb:43:in `tmpdir'",
    "/var/task/lambda_function.rb:11:in `lambda_handler'",
    "/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric/lambda_handler.rb:28:in `call_handler'",
    "/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:88:in `run_user_code'",
    "/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:66:in `start_runtime_loop'",
    "/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:49:in `run'",
    "/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:221:in `bootstrap_handler'",
    "/var/runtime/gems/aws_lambda_ric-3.0.0/lib/aws_lambda_ric.rb:203:in `start'",
    "/var/runtime/index.rb:4:in `<main>'"
  ]
}
snakasnaka

Lambda 上で Dir.tmpdir 利用すると確実にエラーが発生する

snakasnaka

ランタイムのバージョンが関係している説

真偽不明だけど、過去のランタイムバージョンでは問題はなかった説がある。

今の Runtime version

INIT_START
Runtime Version: ruby:3.3.v12
Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:9cc8b6f58ab551d6089e31155c615e4f92eb9b4ad017cdc2e1788102a97e8625
snakasnaka

ログで前の Runtime バージョンが動いているのを確認したが...

INIT_START
Runtime Version: ruby:3.3.v11
Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:eec443cfe2ab3dce4f3c6abd60cc78ec60fdf7839d2b8b0744ae4654a0e6784b

結果は変わらず

このスクラップは3ヶ月前にクローズされました