---
title: "ngx_mruby v2.7.2"
language: "en"
documentation_version: "3.x"
channel: "development"
release_context: null
source_revision: "10297d1e28653321d6996609fc53718d3b7c8278"
working_tree_clean: true
source_path: "docs/releases/v2.7.2.md"
source_sha256: "680b9f1e779d1c6634f013eb8215a4fb0b5bcea9c1eb5e8767d9217c27139d93"
source_at_revision: true
source_url: "https://github.com/matsumotory/ngx_mruby/blob/10297d1e28653321d6996609fc53718d3b7c8278/docs/releases/v2.7.2.md"
html_url: "https://ngx.mruby.org/releases/v2.7.2/"
markdown_url: "https://ngx.mruby.org/markdown/releases/v2.7.2.md"
last_updated: "2026-10-29"
---

# ngx_mruby v2.7.2

ngx_mruby 2.7.2 is a maintenance release of the 2.x line.

It fixes three security issues, each with its own advisory: a use-after-free
in `Nginx::Upstream#server=` and the stream
`Nginx::Stream::Connection#upstream_server=`
([GHSA-22jc-w698-vfqw](https://github.com/matsumotory/ngx_mruby/security/advisories/GHSA-22jc-w698-vfqw),
High); uninitialised worker memory disclosed through an `Nginx::Var` value
that contains a NUL byte
([GHSA-gxqc-3p4h-5p36](https://github.com/matsumotory/ngx_mruby/security/advisories/GHSA-gxqc-3p4h-5p36),
Medium); and a HEAD request receiving a response body from a handler that
writes output
([GHSA-8mxf-gvvp-38fq](https://github.com/matsumotory/ngx_mruby/security/advisories/GHSA-8mxf-gvvp-38fq),
Medium). Entries 6, 7 and 8 describe them.

Some of its fixes change what an existing configuration, Ruby script or
build can observe. Read the first section before you upgrade. Each entry says what you
could observe in 2.7.1 ("Before"), what you observe in 2.7.2 ("Now"), why the
behavior had to change ("Why it changed": "The defect" or "The reason", "Why
the old behavior could not be kept" and "What the documentation promised"),
which configurations are affected, what to do, and, where the test suite
covers the change, the port of its tests. "Not changed by this release" names
a related behavior that stays as it was.

## Behavior changes: read before upgrading

### 1. Nginx::Var raises for a method name that names no variable and for a setter without its value (#589)

- Before: `Nginx::Var` treats a call of a method that it does not define as
  access to the nginx variable of that name (`v.http_host` reads
  `$http_host`), and a method name that ends with `=` as a setter of the
  variable named before the `=` (`v.http_host = "a"`). Code that builds the
  name at run time passes it to `send`. For three kinds of names the accessor
  read memory outside the name or outside the arguments, so the result
  depended on that memory:
  - an empty name (`v.send("")`) read the byte before the name, and usually
    returned `nil`;
  - a name made only of `=` (`v.send("=", "x")`) read past the end of the
    name, and returned `nil` or ended the worker process;
  - a setter called without its value (`v.send("http_host=")`) read a value
    from outside its arguments. It could end the worker process, or set the
    variable to a value that the code did not pass.
- Now: an empty name and a name made only of `=` raise `NoMethodError`
  (`undefined method '' for Nginx::Var`, `undefined method '=' for
  Nginx::Var`). A setter called without its value raises `ArgumentError`
  (`wrong number of arguments (given 0, expected 1) for
  Nginx::Var#http_host=`) and leaves the variable as it was. The accessor
  checks the name and the arguments before it reads them.
- Why it changed:
  - The defect: the accessor read the byte before an empty name, ran `strtok`
    past the end of its copy of a name made only of `=`, and read the first
    element of an empty argument list for a setter without its value, as
    "Before" says ([#589](https://github.com/matsumotory/ngx_mruby/pull/589)).
  - Why the old behavior could not be kept: the results of these calls came
    from memory outside the name and the arguments, so the same call could
    return `nil` once and end the worker process another time. Such a name
    names no variable, and a setter has no value to set without its
    argument, so there is no result to keep. The calls raise what Ruby raises
    for a method that the receiver does not have (`NoMethodError`) and for a
    call with fewer arguments than the method takes (`ArgumentError`).
  - What the documentation promised: the 2.7.1 documentation says that the
    accessor is called for `Nginx::Var#VARNAME` and `Nginx::Var#VARNAME=` and
    sets "the 2nd arg" to the variable when the name ends with `=`
    ([Nginx::Var#method_missing of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md#nginxvarmethod_missingsymbol--args---result)).
    It says nothing about a name without a variable name in it, or about a
    setter without its value.
- Affected: code that calls `Nginx::Var` with a name that it builds at run
  time (`send`, `public_send`, `method_missing`) when the name can be empty
  or made only of `=`, and code that calls a setter without a value.
- What to do: check the name before the call, or rescue `NoMethodError`
  where the name can be empty. Pass a value to each setter.
- Not changed by this release: the accessor reads and sets the same
  variables as before, also for names with uppercase letters, which name the
  variable in lowercase. A setter called with more than one value still uses
  the first one, and a name like `=foo=` still sets `$foo`.
- Tests: port 18153 (`test/t/cases/var_input.rb`).

### 2. Nginx::Var#set leaves the String or Symbol of the name unchanged (#589)

- Before: `Nginx::Var#set(name, value)` looked the variable up by the
  lowercased name, and lowercased the bytes of `name` in place to get it.
  After the call, the String passed as `name` was in lowercase, also when it
  was frozen, and so was a String that shared its bytes, such as a copy that
  `dup` made of a long String. For a Symbol with a long name, `set`
  lowercased the name that the worker keeps for the Symbol, so `to_s` of that
  Symbol returned the name in lowercase in every later request of the
  worker. Long means more than 27 bytes on a 64-bit build: mruby keeps a
  shorter String inside the String object, and a copy does not share it.
- Now: `set` looks the variable up by a lowercased copy of the name. The
  String or Symbol passed as `name` stays as it was. `set` sets the same
  variable and returns the same value as before.
- Why it changed:
  - The defect: `set` wrote into the String passed as `name`, and through it
    into frozen Strings, into the Strings that shared its bytes and into the
    names of Symbols, as "Before" says ([#589](https://github.com/matsumotory/ngx_mruby/pull/589)).
  - Why the old behavior could not be kept: a frozen String must not change,
    a String that shares its bytes is another object of the code, and the
    name of a Symbol belongs to every request of the worker. The lookup needs
    the name in lowercase, so it takes a copy.
  - What the documentation promised: the 2.7.1 documentation says that `set`
    sets the value to the nginx variable
    ([Nginx::Var#set of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md#nginxvarsetname-value)).
    It does not say that `set` changes its arguments.
- Affected: code that uses the String or Symbol that it passed to `set`
  after the call, and expects it in lowercase.
- What to do: nothing, unless the code relied on `set` lowercasing the name.
  Such code calls `downcase` on the name itself.
- Tests: port 18153 (`test/t/cases/var_input.rb`).

### 3. Redis is in the default build again, and mruby-redis builds hiredis v1.5.0 on every platform without stopping on hiredis warnings (#604, matsumotory/mruby-redis#108)

- Before: the default `build_config.rb` of 2.7.1 leaves
  `matsumotory/mruby-redis` out (entry 1 of the
  [2.7.1 notes](/releases/v2.7.1/#1-redis-is-no-longer-in-the-default-build)), so
  a binary built with it has no `Redis` class, and code that refers to
  `Redis` raises `NameError` (uninitialized constant Redis). A build that
  enables the gem, by uncommenting
  `conf.gem github: 'matsumotory/mruby-redis'` or with its own
  `build_config.rb`, gets hiredis from the build script of the gem. When the
  build directory of the gem had no `hiredis` directory yet, the script
  cloned hiredis from the head of its default branch, and on macOS checked
  out v0.13.3. The script compiled hiredis with the C flags of the mruby
  build, and the Makefile of hiredis 1.2.0 and later adds `-Werror`, so a
  new warning in hiredis stopped the build. With hiredis 1.4.0 and later and
  the gcc toolchain of mruby, the build stopped at
  `ffc.h:330:5: error: "FFC_DEBUG" is not defined, evaluates to 0 [-Werror=undef]`.
- Now: the default `build_config.rb` includes `matsumotory/mruby-redis`
  again ([#604](https://github.com/matsumotory/ngx_mruby/pull/604)), as
  the default `build_config.rb` of 2.7.0 and earlier did. A binary built
  with it has the `Redis` class, and its build clones the gem and hiredis
  from GitHub. The build script of the gem clones the hiredis tag `v1.5.0` into
  `hiredis-v1.5.0` on every platform, macOS included. It stops, with a
  message that names both commits, when the clone is not at commit
  `5bb7f030f6631b243438cf0fdd3be84352b905bb`, the commit of that tag. It
  builds hiredis with `USE_WERROR=0`, so the build prints the warnings of
  hiredis and goes on. It builds hiredis with `make install` only, so
  `hiredis-test` is no longer built, and musl systems such as Alpine no
  longer need `linux-headers`. A `hiredis` directory left by an earlier
  build is not reused. The change of hiredis is in the gem: ngx_mruby 2.x commits no
  gem lock, so the build clones the gem at the head of its `master` branch.
  Every build that clones the gem after the merge of
  [matsumotory/mruby-redis#108](https://github.com/matsumotory/mruby-redis/pull/108)
  gets the change, a build of 2.7.1 included. mruby does not update a gem
  that it cloned before, and it checks a gem out at the commit that
  `build_config.rb.lock` records, a file that mruby writes at the end of
  each build and that git ignores on 2.x: a source tree that built
  mruby-redis before the merge keeps that commit of the gem, and its
  hiredis, until both its mruby build directory `mruby/build` and
  `build_config.rb.lock` are removed.
- Why it changed:
  - The defect: the unpinned clone let every new hiredis commit reach every
    new build that enables the gem, and hiredis 1.4.0 stopped such builds
    with the `FFC_DEBUG` error above
    ([#528](https://github.com/matsumotory/ngx_mruby/pull/528),
    [matsumotory/mruby-redis#108](https://github.com/matsumotory/mruby-redis/pull/108)).
    2.7.1 left the gem out of the default build because of this defect
    (entry 1 of the 2.7.1 notes).
  - Why the old behavior could not be kept: a build that follows the default
    branch of hiredis can break with any hiredis commit, which is why 2.7.1
    left the gem out of the default build. A pinned release with a checked
    commit gives every build the same hiredis. `-Werror` had to go as well:
    with ngx_mruby's sanitizer flags
    (`-fsanitize=address,undefined -fno-omit-frame-pointer`), gcc 13 to 15
    stop the build of hiredis v1.5.0 at `sds.c:525` with
    `null format string [-Werror=format-truncation=]`, so a newer compiler
    would break the pinned build too. `USE_WERROR=0` is the switch that
    hiredis 1.3.0 and later provide for builds that do not want `-Werror`
    ([redis/hiredis#1210](https://github.com/redis/hiredis/pull/1210)).
    v1.5.0 builds and passes the tests of the gem on Linux and macOS, so
    macOS no longer needs v0.13.3. With hiredis pinned, the reason for
    leaving the gem out of the default build no longer holds: entry 1 of
    the 2.7.1 notes kept the gem out of the default list "until
    mruby-redis pins hiredis". Leaving it out longer would keep `Redis`
    from the scripts that used it with the default build of 2.7.0 and
    earlier, the examples in the README and in `docs/use_case` and the
    client of the bundled `auto-ssl` mrbgem among them.
  - What the documentation promised: entry 1 of the 2.7.1 notes says that
    the build of the gem "can fail while hiredis has the problem that #528
    describes". The 2.7.1 documentation says to uncomment the line of the
    gem in `build_config.rb`
    ([built-in mrbgems of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md#built-in-mrbgems)).
    It says that the build of the gem "clones the hiredis library from an
    unpinned branch, which broke the build of ngx_mruby"
    ([docs/install of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/install/README.md#2-configuring-mrbgems)),
    and does not name a version of hiredis. It also says that the `Redis`
    class "is not in the default build since 2.7.1". The 2.7.0
    documentation listed mruby-redis among "the default mrbgems"
    ([docs/install of 2.7.0](https://github.com/matsumotory/ngx_mruby/blob/v2.7.0/docs/install/README.md#2-configuring-mrbgems))
    and `Redis` among the classes that can be used "with default build
    configuration"
    ([built-in mrbgems of 2.7.0](https://github.com/matsumotory/ngx_mruby/blob/v2.7.0/docs/class_and_method/README.md#built-in-mrbgems)).
- Affected: the default builds of 2.7.2. Their binaries have the `Redis`
  class again, so code that refers to `Redis` no longer raises
  `NameError`. Their build clones three more repositories from GitHub:
  matsumotory/mruby-redis, and matsumotory/mruby-pointer, which
  mruby-redis depends on and which adds no class, at the heads of their
  `master` branches, and the hiredis tag `v1.5.0`. It links hiredis
  statically into nginx or into
  the dynamic module, which grows by about 190 KB: a stripped static
  nginx built with the release options of the performance comparison of
  `next` (`test/build_release.sh` there), on Linux aarch64 with gcc 11.4,
  grows from 3.88 MB to 4.08 MB. Only a script that calls
  `Redis` needs a Redis server. The change of hiredis affects the default
  builds of 2.7.2, builds of 2.7.1 that enable mruby-redis, and the
  default builds of 2.7.0 and earlier, whose default `build_config.rb`
  includes the gem. When they clone the gem after the merge, they get
  hiredis v1.5.0 instead of the head of the default branch of hiredis, and
  on macOS instead of v0.13.3. mruby-redis#108 tested the gem with mruby
  3.3.0, which 2.6.0 to 2.7.2 bundle, and with mruby 4.0.0; 2.5.0 and
  earlier bundle older mruby releases. The gem links hiredis statically, so a binary built before
  keeps its hiredis until it is built again after `mruby/build` and
  `build_config.rb.lock` are removed. A build of 2.7.1 with the default `build_config.rb` is not
  affected, because it does not include the gem.
- What to do: nothing for the default build of 2.7.2, unless this source
  tree built mruby-redis before, in a build that enabled it or in the
  default build of 2.7.0 or earlier: then remove `mruby/build` and
  `build_config.rb.lock` first. Removing `mruby/build` alone is not
  enough: the next build clones the gem again, but checks it out at the
  commit that the lock recorded, with the unpinned hiredis clone.
  Then build as before: the warning of entry 1 of the 2.7.1 notes ("The
  build of that gem can fail ...") no longer applies. To build without the
  gem, as the default build of 2.7.1 did, comment out
  `conf.gem github: 'matsumotory/mruby-redis'` in `build_config.rb`. To
  build with another hiredis release, set the environment variable
  `MRUBY_REDIS_HIREDIS_VERSION` to its tag when you build, or set
  `ENV['MRUBY_REDIS_HIREDIS_VERSION']` in `build_config.rb` (see the
  [README of mruby-redis](https://github.com/matsumotory/mruby-redis#readme)).
  The build does not check the commit of another tag. mruby-redis#108 also
  built and tested `v1.4.1`, on its earlier head 2cc63c1 with gcc and mruby
  3.3.0.
- Not changed by this release: as in 2.7.0 and in builds of 2.7.1 that
  enable the gem, each call of `Redis` waits in the worker until the Redis
  server answers, because the gem uses the blocking API of hiredis, so the
  worker serves no other request meanwhile. `Redis#ttl`, whose declaration
  mruby-redis#108 changed for mruby 4.0.0, behaves as before with the
  bundled mruby 3.3.0.
- Tests: the test suite of ngx_mruby has no test that calls Redis, and it
  starts no Redis server. Its default build builds the gem since #604,
  so every CI cell builds the gem and links hiredis, the cell with the
  sanitizers included, and the test nginx has the `Redis` class. #604
  quotes the suite on `master` with the static and the dynamic module and
  with the sanitizers, the symbols of the gem and of hiredis in the nginx
  binary, and a request that calls Redis without and with a running Redis
  server. mruby-redis#108 quotes the suite of
  ngx_mruby `master` (03aad7031) and `next` (75bfa6ff4), each built with the
  gem enabled at the head of that pull request, and both pass. The CI of
  the gem runs its tests on Ubuntu 22.04 and on Ubuntu 24.04, the latter
  with the sanitizer settings of ngx_mruby's CI.

### 4. Redis.connect_set_udptr and Redis.new without arguments keep the connection in the mrb_state, not in mrb->ud, and Redis.new with one argument raises ArgumentError (matsumotory/mruby-redis#109)

- Before: in a build that includes mruby-redis,
  `Redis.connect_set_udptr host, port[, timeout]` (also named
  `Redis.connect_set_raw`) connected to the server and stored the
  connection through matsumotory/mruby-pointer, which keeps its own struct
  in `mrb->ud`, the user-data pointer of the `mrb_state`. `Redis.new`
  without arguments read the connection from there. ngx_mruby keeps its
  own data in `mrb->ud`: the stream module its context, in all the code of
  a `stream {}` block, and the HTTP module its server configuration while
  `mruby_server_context_handler_code`, `mruby_ssl_handshake_handler` and
  `mruby_ssl_handshake_handler_code` run. In that code the two methods
  wrote the connection into ngx_mruby's data and read it from there. In
  the other code of an `http {}` block, `mrb->ud` is NULL:
  `Redis.connect_set_udptr` raised `RuntimeError` ("can not set, mrb ud
  pointer was not initialized") after it had connected, and left that
  connection open, and `Redis.new` without arguments raised `RuntimeError`
  ("can not get. mrb ud pointer was not initialized"). Where a connection
  was stored, every `Redis.new` without arguments returned an object with
  that same connection, and each of these objects closed it when it was
  collected. `Redis.new` with one argument used a value that the call had
  not set: for a new object, a NULL pointer, which ended the worker.
- Now: the gem keeps the connection in the `mrb_state`, in an instance
  variable of the `Redis` class that Ruby code cannot reach, and no
  longer reads or writes `mrb->ud`. The two methods work the same way in
  all the code of an `http {}` or a `stream {}` block.
  `Redis.connect_set_udptr` keeps the connection. When it connects, it
  closes a connection that it kept before and that no `Redis.new` has
  taken. A failed `Redis.connect_set_udptr` raises
  `Redis::ConnectionError`, as before; it now also closes the connection
  that failed, and a connection kept before stays kept. `Redis.new`
  without arguments takes the kept connection, so that one object owns
  it: a second `Redis.new` without arguments raises an error until
  `Redis.connect_set_udptr` is called again, and so does a call when no
  connection is kept. The error is `Redis::ConnectionError`, a subclass
  of `RuntimeError`, with the message "no connection kept: call
  Redis.connect_set_udptr first". `Redis.new` with one argument raises
  `ArgumentError` with the message "wrong number of arguments (given 1,
  expected 0, 2 or 3)". The change is in the gem: ngx_mruby 2.x commits no gem
  lock, so every build that clones the gem after the merge of
  [matsumotory/mruby-redis#109](https://github.com/matsumotory/mruby-redis/pull/109)
  gets it, a build of 2.7.1 included. A source tree that built the gem
  before keeps its checkout of the gem until `mruby/build` and
  `build_config.rb.lock` are removed (see entry 3).
- Why it changed:
  - The defect: mruby leaves `mrb->ud` to the application that embeds it,
    and ngx_mruby uses it. The two methods used ngx_mruby's data as the
    struct of mruby-pointer in the code named above, and raised in the
    rest of an `http {}` block. `Redis.new` with one argument used a value
    that the call had not set
    ([matsumotory/mruby-redis#109](https://github.com/matsumotory/mruby-redis/pull/109)).
  - Why the old behavior could not be kept: `mrb->ud` has no place for
    the data of a gem. While the gem kept its connection there, the two
    methods either raised, where ngx_mruby leaves `mrb->ud` NULL, or used
    ngx_mruby's data, where it does not. Handing one connection to several
    objects made each of them close it, so the connection was closed more
    than once; with one owner, it is closed once. A call with one
    argument has no host and port to connect to, and its old result came
    from a pointer that the call had not set, so there is no result to
    keep. It raises what Ruby raises for a wrong number of arguments
    (`ArgumentError`).
  - What the documentation promised: before this change, neither the
    README of mruby-redis nor the documentation of ngx_mruby described the
    two methods. The README of
    [mruby-pointer](https://github.com/matsumotory/mruby-pointer#readme)
    shows them in a C program that calls `mrb_udptr_init` and
    `mrb_udptr_copy` to share a connection between two `mrb_state`s;
    ngx_mruby calls neither.
- Affected: scripts that call `Redis.connect_set_udptr`,
  `Redis.connect_set_raw`, or `Redis.new` without arguments or with one
  argument, in the default build of 2.7.2, which includes mruby-redis
  again (entry 3), and in every other build that includes the gem: builds
  of 2.7.1 that enable it, and the default builds of 2.7.0 and earlier.
  Scripts that create every `Redis` object with
  `Redis.new host, port[, timeout]` are not affected.
- What to do: call `Redis.connect_set_udptr` again before each further
  `Redis.new` without arguments, or keep the object that `Redis.new`
  returned and use it again, or call `Redis.new host, port` instead.
  `Redis::ConnectionError` is a subclass of `RuntimeError`, so code that
  rescues `RuntimeError` around `Redis.new` without arguments still
  catches the new error. Change only code that compares the class of the
  error with `RuntimeError` exactly, or its message. Call
  `Redis.connect_set_udptr` in code that runs in the worker
  (`mruby_init_worker_code`, `mruby_stream_init_worker_code` or a
  handler): a connection kept by `mruby_init_code`,
  `mruby_server_context_handler_code`, `mruby_stream_init_code` or
  `mruby_stream_server_context_code` is copied into every worker. If this
  source tree built mruby-redis before, remove `mruby/build` and
  `build_config.rb.lock` first.
- Not changed by this release: `Redis.new host, port[, timeout]` and the
  other methods of `Redis` behave as before. The build of mruby-redis
  still clones matsumotory/mruby-pointer, which the gem no longer calls.
- Tests: the test suite of ngx_mruby has no test that uses Redis.
  mruby-redis#109 adds tests of `Redis.connect_set_udptr`,
  `Redis.connect_set_raw`, `Redis.new` without arguments and `Redis.new`
  with one argument to the gem, among them tests with `mrb->ud` set the
  way a host application sets it. It quotes the suite of ngx_mruby
  `master` and `next`, each built with the gem enabled at the head of
  that pull request, and both pass.

### 5. A content handler that writes output and then sets a status below 300 answers with that status (#594)

- Before: an `mruby_content_handler*` on the main request that wrote output
  with `Nginx.rputs` or `Nginx.echo` and then called `Nginx.return`,
  `Nginx.send_header` or `Nginx.status_code=` with a status below 300 other
  than 200, 201 and 204, for example `Nginx::HTTP_ACCEPTED`, or with 0, sent
  no response. nginx closed an HTTP/1.0 connection without sending a byte
  and kept an HTTP/1.1 keep-alive connection open without a response until
  `keepalive_timeout` (75 s by default), so the client waited for its own
  timeout. 2.7.1 lists this under "Known limitations".
- Now: the request gets the output with the status that the handler set,
  with `Content-Length` of the output, like nginx's own `return 202 "text";`
  without its `Content-Type` (ngx_mruby sends none, as for 200 after
  output). This holds also when the handler ends so after
  `Nginx::Async::HTTP.sub_request` resumed it. `Nginx.rputs "queued"` followed by
  `Nginx.return Nginx::HTTP_ACCEPTED` answers `202 Accepted` with the body
  `queued`. A status below 200 goes out the same way, as the number without
  a reason phrase (`HTTP/1.1 100 `), as nginx sends it for its own
  `return 100 "text";`, although HTTP does not allow a final response with
  such a status. `Nginx.return 0` sets no status, and the output gets 200,
  as output without a status does. A keep-alive connection stays open for
  the next request, as after a 200 with output.
- Why it changed:
  - The defect: `Nginx.return` after the output marked the end of the
    output, and ngx_mruby then handed the status to nginx without sending
    the output. nginx sends a response of its own for 201, 204 and the
    statuses from 300 up, but for any other status below 300 it ends the
    request without sending anything, so the request had no response.
    [#566](https://github.com/matsumotory/ngx_mruby/pull/566) fixed this for
    handlers without output and left this case open, as its
    [review](https://github.com/matsumotory/ngx_mruby/pull/566#issuecomment-5977670159) records; this release fixes it
    ([#594](https://github.com/matsumotory/ngx_mruby/pull/594)).
  - Why the old behavior could not be kept: keeping it would leave these
    requests without a response. A content handler that writes output and
    then sets 200 already answers with its output; the fix answers the other
    statuses that nginx does not answer by itself in the same way, with the
    status the handler set. No request that got a response in 2.7.1 gets a
    different one.
  - What the documentation promised: the 2.7.1 documentation says that
    `Nginx.rputs` "Puts response text" and that `Nginx.return` "Returns nginx
    status code to send back HTTP status to the client"
    ([Nginx.return of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md#nginxreturnnginx_status_code)),
    and the 2.7.1 release notes list this case under "Known limitations"
    ([v2.7.1.md](/releases/v2.7.1/#known-limitations)). The documentation of the
    methods did not say what a content handler answers after output with
    such a status.
- Affected: `mruby_content_handler*` handlers that write output and then set
  a status below 300 other than 200, 201 and 204, or 0.
- What to do: nothing for a handler that means to answer its output with
  that status. A handler that should answer 200 sets `Nginx::HTTP_OK`.
- Not changed by this release: after output, `Nginx::HTTP_OK` answers 200
  with the output, and `Nginx::HTTP_CREATED`, `Nginx::HTTP_NO_CONTENT` and the
  statuses from 300 up get nginx's own response for that status without the
  output, as in 2.7.1. A status set before the first `Nginx.rputs` or
  `Nginx.echo` still does not apply to the output, which gets 200. Handlers
  of other phases are not changed: a rewrite handler that writes output and
  then sets `Nginx::HTTP_ACCEPTED` still sends no response. Subrequests are
  not changed either: a subrequest ends without a response body, and its
  parent answers.
- Tests: port 18154 (`test/conf/conf.d/68-output-then-status.conf`,
  `test/t/cases/output_then_status.rb`).

### 6. Nginx::Var copies the whole value, so a value with a NUL byte no longer exposes uninitialized worker memory

- Before: the `Nginx::Var` accessor (`v.name = value`) and
  `Nginx::Var#set(name, value)` copied the value into a request-pool buffer
  with `ngx_cpystrn`. `ngx_cpystrn` stops at the first NUL byte in the
  source, but the length recorded for the variable (`vv->len`) was the full
  length of the mruby string. mruby strings are byte strings and can hold a
  NUL, for example a value taken from client input such as the request body
  or a decoded argument. The bytes after the NUL were therefore never
  written, while the variable's length still spanned the whole buffer.
  Reading the variable back (`Nginx::Var#name`, or nginx emitting `$name`)
  returned `vv->len` bytes, so the untouched bytes after the NUL came from
  uninitialized request-pool memory. Sending that variable to the client
  (`Nginx.rputs`/`Nginx.echo` of the variable, `add_header $name`, a value
  placed in a proxied request, or a logged value) then sent those
  uninitialized bytes out of the worker. On a worker that had already served
  requests, that memory could hold fragments of earlier data and heap
  pointers.
- Now: ngx_mruby copies the whole value with `ngx_memcpy` for its full
  length, so a value that holds a NUL is stored byte for byte and the memory
  after the NUL is not exposed. Both the accessor (`ngx_mrb_var_set_vector`)
  and `Nginx::Var#set` (`ngx_mrb_var_set_func`) are changed.
- Why it changed:
  - The defect: `ngx_cpystrn` stopped the copy at the first NUL while the
    variable kept the full length, so a variable set from a value with a NUL
    read uninitialized request-pool memory past the NUL, as "Before" says.
  - Why the old behavior could not be kept: the bytes past the NUL were
    never written, so the value read back was not the value that was set,
    and on a reused worker it could carry fragments of earlier data and heap
    pointers out to the client. A value that holds a NUL has to be stored as
    it is, and reading memory past it gives no result worth keeping.
  - What the documentation promised: the 2.7.1 documentation says that the
    accessor and `set` set the value to the nginx variable
    ([Nginx::Var#method_missing of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md#nginxvarmethod_missingsymbol--args---result),
    [Nginx::Var#set of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md#nginxvarsetname-value)).
    It does not say that a value is cut off at a NUL byte, or that reading
    the variable back can return other memory.
- Affected: a configuration that stores a value that can hold a NUL byte,
  derived from client input, into an nginx variable with `Nginx::Var#set` or
  the `v.name =` accessor, and then sends that variable back toward a client.
- What to do: nothing; the value is stored in full. On an earlier release
  that you cannot upgrade, remove the NUL bytes before you store the value,
  for example `value.delete("\x00")`.
- Not changed by this release: a value that holds no NUL byte is stored and
  read back as before.
- Tests: port 18153 (`test/t/cases/var_input.rb`, `/set_nul_value` and
  `/set_nul_value_via_set`).

### 7. A HEAD request to a content handler that writes output receives only the headers

- Before: for a HEAD request, nginx sets `r->header_only`, and the response
  should carry the headers without a body. But when the content handler of
  the main request wrote output with `Nginx.rputs` or `Nginx.echo`,
  `ngx_mrb_finalize_rputs` (`src/http/ngx_http_mruby_core.c`) called
  `ngx_http_send_header` and then passed the output chain to
  `ngx_http_output_filter` without checking `r->header_only`. A HEAD request
  to such a location therefore received the body after the headers. On a
  keep-alive connection, the extra body bytes shifted where the client read
  the next response, so the responses on the connection no longer lined up
  with the requests. When a reverse proxy in front reused one upstream
  connection for more than one client, it could pair a response with the
  wrong request (a response-queue desynchronization, the HTTP
  request/response smuggling class). A client triggered it by sending a HEAD
  request to a location whose mruby content handler writes output.
- Now: a HEAD request receives no body. `ngx_mrb_finalize_rputs` checks
  `r->header_only` before `ngx_http_output_filter`, as nginx's own content
  handlers do. The headers, `Content-Length` among them, are unchanged, so
  the HEAD response still reports the length the body would have had. This
  holds for the "output and then a status" path of
  [#594](https://github.com/matsumotory/ngx_mruby/pull/594) as well: a HEAD
  request to a handler that sets 202 after output receives the headers of
  202 with no body.
- Why it changed:
  - The defect: `ngx_mrb_finalize_rputs` passed the output chain to
    `ngx_http_output_filter` without checking `r->header_only`, so a content
    handler that wrote output answered a HEAD request with a body, as
    "Before" says.
  - Why the old behavior could not be kept: a HEAD response must carry no
    body, and the body on a keep-alive connection shifted the framing of the
    responses that followed, which a front proxy that reuses an upstream
    connection across clients could pair with the wrong request. nginx's own
    content handlers send no body for HEAD, and the mruby content handler
    now does the same.
  - What the documentation promised: the 2.7.1 documentation says that
    `Nginx.rputs` "Puts response text"
    ([the methods of 2.7.1](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md)).
    It does not say that a HEAD request receives that output, and HTTP does
    not allow a body in the response to a HEAD request.
- Affected: a location whose main-request handler writes output with
  `Nginx.rputs` or `Nginx.echo` and that receives a HEAD request. The fix is
  in the shared output path (`ngx_mrb_finalize_rputs`), so a content, rewrite
  or access handler that writes output sends the headers only for a HEAD
  request.
- What to do: nothing.
- Not changed by this release: a GET request behaves as before.
- Tests: port 18137 (`test/t/cases/head_output.rb`) and the HEAD cases added
  to port 18154 (`test/t/cases/output_then_status.rb`).

### 8. Nginx::Upstream#server= and Nginx::Stream::Connection#upstream_server= keep the peer valid for later requests and connections (a use-after-free fix)

- Before: `Nginx::Upstream#server=` (`src/http/ngx_http_mruby_upstream.c`)
  copied the new server name into memory from the request pool
  (`ngx_palloc(r->pool, ...)`), resolved it with
  `ngx_parse_url(r->pool, ...)`, and stored the result into the upstream's
  round-robin peer. The peer lives for the worker's lifetime (the
  configuration pool), and every request that uses the upstream shares it.
  `Nginx::Stream::Connection#upstream_server=`
  (`src/stream/ngx_stream_mruby_connection.c`) did the same on the stream
  side, storing into the peer a Ruby string (which mruby's garbage collector
  can free or move) or connection-pool memory. When the request or
  connection that set the server ended, its pool was freed, but the shared
  peer still pointed into it. A later request or connection that used the
  same upstream without calling the setter (a `proxy_pass` to the upstream
  from another location, a read of `Nginx::Upstream#server`, or a read on a
  later connection) then read freed memory: the address nginx connected to,
  and the name it logged, came from a freed allocation. This is a
  use-after-free: it could crash the worker, send the upstream connection to
  the wrong address, or disclose freed memory in a log.
- Now: ngx_mruby copies the name and the resolved sockaddr into
  worker-lifetime storage that belongs to the peer (one per peer, allocated
  once in the cycle pool and reused), so the peer no longer points at freed
  memory. A repeated setter call reuses the one store instead of growing it;
  the store holds 1 to 255 bytes, and the fix also checks that the
  destination resolved. Both `Nginx::Upstream#server=` (HTTP) and
  `Nginx::Stream::Connection#upstream_server=` (stream) are changed.
- Why it changed:
  - The defect: the peer, which lives for the worker's lifetime, pointed at
    request-pool or connection-pool memory, and on the stream side at an
    mruby string that the garbage collector can move, so a later use of the
    upstream read memory that had been freed, as "Before" says.
  - Why the old behavior could not be kept: the peer outlives the request or
    connection that set it, so it cannot hold a pointer into their pools.
    The name and the address have to live as long as the peer, so the fix
    copies them into storage that lives for the worker's lifetime.
  - What the documentation promised: the documented usage sets the server in
    `mruby_rewrite_handler_code` and proxy_passes in the same request.
    Nothing said that a later request that does not set the server reads a
    valid peer, or that reading `Nginx::Upstream#server` after the setting
    request ended returns valid memory.
- Affected: a configuration that uses `Nginx::Upstream#server=` (HTTP) or
  `Nginx::Stream::Connection#upstream_server=` (stream).
- What to do: nothing.
- Not changed by this release: setting the server on each request or
  connection that uses the upstream still works, and the value read back is
  the value that was set.
- Not changed by this release: an upstream with a shared-memory `zone` keeps
  its peer in shared memory that every worker shares, so pointing the peer
  at any one worker's memory (the per-worker store of this fix, or the
  request pool of the old code) is invalid across workers. This release
  corrects the case without a `zone`. An upstream with a `zone` needs the
  name and the sockaddr in the zone's slab, which is outside the scope of
  this release; the old code was worse here, an existing problem.
- Tests: HTTP on port 18139 (`test/t/cases/upstream_server.rb`), stream on
  ports 12364 and 12365 (`test/t/cases/stream_upstream_server.rb`).

## New features

None.

## Fixes

- When nginx has no value for a variable that `Nginx::Var` reads, for
  example `$realpath_root` in a location whose root does not exist, the line
  that `Nginx::Var` writes to `error.log` (`<name> is NULL`) ends the name of
  the variable at its last character. Before, the line could carry the bytes
  that followed the name in memory ([#589](https://github.com/matsumotory/ngx_mruby/pull/589)).

## Build and test changes

- `test.sh`: the HTTP test servers also listen on 18153, for
  `test/t/cases/var_input.rb` ([#589](https://github.com/matsumotory/ngx_mruby/pull/589)).
- `test.sh`: instead of sleeping for `NGINX_HEATTIME` seconds before the
  first request, `test.sh` waits until the test nginx accepts connections on
  every port of the `listen` directives in `test/conf/nginx.conf` and
  `test/conf/conf.d/*.conf`, and prints how long that took. A CI cell under
  valgrind had failed with `Errno::ECONNREFUSED`, because nginx was still
  reading its configuration when the 10 seconds of the sleep had passed
  ([#564](https://github.com/matsumotory/ngx_mruby/pull/564#issuecomment-5977252444)). `NGINX_HEATTIME` is now the upper bound of the wait, 60
  seconds by default; it was the length of the sleep, 2 seconds by default
  and 10 in CI, and CI no longer sets it. The run fails, and names the ports
  that still refuse connections, when nginx does not listen within the bound
  or ends before it listens. Before it starts nginx, `test.sh` also waits
  until no process accepts connections on those ports. "Waiting for nginx to
  listen" in `docs/test/README.md` describes the wait ([#593](https://github.com/matsumotory/ngx_mruby/pull/593)).
- CI: three workflows check the supply chain ("Supply chain checks" in
  `docs/test/README.md`). `zizmor.yml` runs the static analyzer zizmor on
  pull requests and pushes that change `.github/workflows/`,
  `.github/dependabot.yml` or `.github/zizmor.yml`, and fails on any finding.
  `scorecard.yml` runs OpenSSF Scorecard on pushes to `master` and weekly, and
  publishes the results to the Scorecard API and to code scanning.
  `gem-heads.yml` reports every Monday, for each `conf.gem github:` line of
  `build_config.rb`, the head of the branch that a build clones, and whether
  it moved since the previous report. `mirror-v2x.yml` asks for
  `contents: write` in its job instead of the whole workflow, and Dependabot
  proposes a new release of an action only when it is 7 days old ([#599](https://github.com/matsumotory/ngx_mruby/pull/599)).
- CI: a pull request that changes an input of the measured binary (`src/`,
  `mrbgems/`, `mruby/`, `dependence/`, the top-level build files,
  `nginx_version`), the comparison or the workflow runs the new `perf` job.
  It builds the base and the pull request as release builds and compares the
  instructions that nginx executes per request with callgrind
  (`test/perf/`, `test/build_release.sh`, and the configurations, scenarios
  and mock LLM upstream in `test/soak/`, taken from the `next` branch).
  `ci-ok` does not need the job yet. Locally, `sh test/perf/compare.sh
  BASE_DIR` (Linux with valgrind) builds in `build_perf/` and listens on
  12370-12372. "Performance comparison with callgrind" in
  `docs/test/README.md` describes it ([#601](https://github.com/matsumotory/ngx_mruby/pull/601)).
- CI: the "Check for sanitizer reports" step of the sanitizer cell also
  reads `build/nginx/logs/stderr.log`, the other `*.log` files of
  `build/nginx/logs/` and the logs of the second nginx under
  `build/nginx/second_instance/`, which hold the reports of the processes
  that the tests start. Before, it read only `build/nginx/logs/error.log`
  ([#601](https://github.com/matsumotory/ngx_mruby/pull/601)).
- Tests for directives and Ruby methods that had none. They check that
  `nginx -t` rejects `mruby_output_filter` and `mruby_output_filter_code`
  with their message, and that `mruby_exit_worker_code` and
  `mruby_stream_exit_worker_code` run when nginx stops. They check what
  these methods return or write to `error.log`: `Nginx.module_version`,
  `Nginx.nginx_version`, `Nginx.server_version`,
  `Nginx::Request#content_type`, `#content_type=`, `#method=`, `#scheme`
  and `#document_root`, `Nginx::SSL.log`, the methods of the Rack logger,
  `Nginx::Utils.escape` and `.encode_parameters`, the methods of
  `Nginx::Stream::Connection` that read the addresses of the session,
  `#upstream_server`, `Nginx::Stream.errlogger` and
  `Nginx::Stream.module_name`.
  The HTTP test servers also listen on 18155 to 18157 and the stream test
  servers on 12363. `test/t/cases/config_hooks.rb` starts an nginx of its own
  on 18158 and stops it with `nginx -s quit` ([#603](https://github.com/matsumotory/ngx_mruby/pull/603)).
- `test.sh`: the HTTP test servers also listen on 18154, for
  `test/t/cases/output_then_status.rb` ([#594](https://github.com/matsumotory/ngx_mruby/pull/594)).

## Performance

Measured on 2026-10-29 with the callgrind comparison of `master`
(`test/perf/compare.sh` at `6b27f08da`, see "Performance comparison with
callgrind" in `docs/test/README.md`): release builds of both trees with
nginx 1.31.6, Ubuntu 22.04 in a container on aarch64 (gcc 11.4, valgrind
3.18.1), 20000 requests per scenario after 2000 warmup requests. Base: the
tag `v2.7.1` (tree `0e6185b02799`, `src/` tree `8b6857bd850e`); head:
`master` at `6b27f08da` (tree `db916ab4a26f`, `src/` tree `9f6fd46d1a5c`).
Both builds have the same 21 third-party mrbgems at the same commits; the
head also has matsumotory/mruby-redis and matsumotory/mruby-pointer, which
the default build includes again (entry 3). Instructions that nginx
executes per request, without mruby's garbage collector:

| Scenario | 2.7.1 | 2.7.2 | Change |
|---|---|---|---|
| `hello` | 11172 | 11214 | +0.38% |
| `headers` | 34511 | 34574 | +0.18% |
| `var` | 19952 | 19998 | +0.23% |
| `filter` | 13350 | 13426 | +0.57% |
| `sleep` | 14892 | 15012 | +0.80% |
| `sub_request` | 47551 | 47782 | +0.49% |
| `file` | 13215 | 13319 | +0.79% |
| `proxy_plain_2k` | 22099 | 22099 | +0.00% |
| `proxy_plain_64k` | 22414 | 22415 | +0.00% |
| `proxy_stream_plain_50` | 53736 | 53968 | +0.43% |
| `auth` | 46142 | 47166 | +2.22% |
| `route_json_2k` | 106227 | 106944 | +0.67% |
| `ruby_call_1` | 10677 | 10685 | +0.08% |
| `ruby_call_10` | 35597 | 35712 | +0.32% |

Every scenario stays below the 3% at which the comparison warns. The
largest change, relative and absolute, is in `auth`: +2.22%, or 1024
instructions per request. In the comparison of #604 with its base, which
the `perf` job of CI made on its x86_64 runner (`ubuntu-22.04`, base
`68032bb6a`), `auth` was +3.71% and `sleep` +3.04%. Those numbers come from
another machine and another base than the table above, so the two are not
compared with each other. #604 traced almost all of that increase to
`mrb_vm_const_get`: with mruby-redis, `Object` has 119 constants instead
of 118 in a table of 128 slots on the bundled mruby 3.3.0, so constant
lookups probe longer. The owner accepted that difference on #604. Of the
changes since 2.7.1, only #589 changes code on the request path: it checks
the name and the arguments of `Nginx::Var` before it reads them, and
`Nginx::Var#set` takes a lowercased copy of the name from the request pool
(`ngx_pnalloc`) instead of lowercasing the name in place. Neither change
adds a blocking wait.

## Upgrading

Supported nginx versions: the nginx mainline and stable lines (see
[README.md](https://github.com/matsumotory/ngx_mruby/blob/next/README.md)); CI builds nginx 1.31.6, 1.30.5, 1.28.3 and
1.26.3.

- No directive was added, renamed or removed. Rebuild the module with this
  release the way you built 2.7.1, and replace the binary or the dynamic
  module as usual.

Check these before upgrading. Each line is the "What to do" of its entry
above.

- `Nginx::Var` with a name built at run time: check the name before the
  call, or rescue `NoMethodError` where the name can be empty, and pass a
  value to each setter (entry 1).
- `Nginx::Var#set`: nothing, unless the code relied on `set` lowercasing the
  name; such code calls `downcase` on the name itself (entry 2).
- mruby-redis, which the default build includes again: nothing, unless
  this source tree built it before; then remove `mruby/build` and
  `build_config.rb.lock` first. The
  build gets hiredis v1.5.0, and the warning of entry 1 of the 2.7.1 notes
  no longer applies. To build without the gem, comment out its line in
  `build_config.rb`. Set the environment variable
  `MRUBY_REDIS_HIREDIS_VERSION` to build another hiredis tag (entry 3).
- `Redis.connect_set_udptr` and `Redis.new` without arguments of
  mruby-redis: call `Redis.connect_set_udptr` again before each further
  `Redis.new` without arguments, keep the object that `Redis.new`
  returned, or call `Redis.new host, port` instead. `rescue RuntimeError`
  still catches the new `Redis::ConnectionError`, a subclass of it. Call
  `Redis.connect_set_udptr` in code that runs in the worker. If this
  source tree built mruby-redis before, remove `mruby/build` and
  `build_config.rb.lock` first (entry 4).
- A content handler that writes output and then sets a status below 300
  other than 200, 201 and 204: nothing for a handler that means to answer
  its output with that status; a handler that should answer 200 sets
  `Nginx::HTTP_OK` (entry 5).
- A value that can hold a NUL byte, stored into an nginx variable with
  `Nginx::Var#set` or the `v.name =` accessor: nothing; the value is stored
  in full (entry 6).
- A HEAD request to a content handler that writes output with `Nginx.rputs`
  or `Nginx.echo`: nothing (entry 7).
- `Nginx::Upstream#server=` or `Nginx::Stream::Connection#upstream_server=`:
  nothing (entry 8).
