Skip to content

Commit

Permalink
nginx: Fix ETag patch to ignore realpath(3) error
Browse files Browse the repository at this point in the history
While our ETag patch works pretty fine if it comes to serving data off
store paths, it unfortunately broke something that might be a bit more
common, namely when using regexes to extract path components of
location directives for example.

Recently, @devhell has reported a bug with a nginx location directive
like this:

  location ~^/\~([a-z0-9_]+)(/.*)?$" {
    alias /home/$1/public_html$2;
  }

While this might look harmless at first glance, it does however cause
issues with our ETag patch. The alias directive gets broken up by nginx
like this:

  *2 http script copy: "/home/"
  *2 http script capture: "foo"
  *2 http script copy: "/public_html/"
  *2 http script capture: "bar.txt"

In our patch however, we use realpath(3) to get the canonicalised path
from ngx_http_core_loc_conf_s.root, which returns the *configured* value
from the root or alias directive. So in the example above, realpath(3)
boils down to the following syscalls:

  lstat("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
  lstat("/home/$1", 0x7ffd08da6f60) = -1 ENOENT (No such file or directory)

During my review[1] of the initial patch, I didn't actually notice that
what we're doing here is returning NGX_ERROR if the realpath(3) call
fails, which in turn causes an HTTP 500 error.

Since our patch actually made the canonicalisation (and thus additional
syscalls) necessary, we really shouldn't introduce an additional error
so let's - at least for now - silently skip return value if realpath(3)
has failed.

However since we're using the unaltered root from the config we have
another issue, consider this root:

  /nix/store/...-abcde/$1

Calling realpath(3) on this path will fail (except if there's a file
called "$1" of course), so even this fix is not enough because it
results in the ETag not being set to the store path hash.

While this is very ugly and we should fix this very soon, it's not as
serious as getting HTTP 500 errors for serving static files.

I added a small NixOS VM test, which uses the example above as a
regression test.

It seems that my memory is failing these days, since apparently I *knew*
about this issue since digging for existing issues in nixpkgs, I found
this similar pull request which I even reviewed:

#66532

However, since the comments weren't addressed and the author hasn't
responded to the pull request, I decided to keep this very commit and do
a follow-up pull request.

[1]: #48337

Signed-off-by: aszlig <aszlig@nix.build>
Reported-by: @devhell
Acked-by: @7c6f434c
Acked-by: @yorickvP
Merges: #80671
Fixes: #66532
(cherry picked from commit e1d63ad)
  • Loading branch information
aszlig committed Mar 28, 2020
1 parent 64a3ccb commit 598a9cb
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 14 deletions.
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Expand Up @@ -197,6 +197,7 @@ in
nghttpx = handleTest ./nghttpx.nix {};
nginx = handleTest ./nginx.nix {};
nginx-etag = handleTest ./nginx-etag.nix {};
nginx-pubhtml = handleTest ./nginx-pubhtml.nix {};
nginx-sso = handleTest ./nginx-sso.nix {};
nix-ssh-serve = handleTest ./nix-ssh-serve.nix {};
nixos-generate-config = handleTest ./nixos-generate-config.nix {};
Expand Down
22 changes: 22 additions & 0 deletions nixos/tests/nginx-pubhtml.nix
@@ -0,0 +1,22 @@
import ./make-test.nix {
name = "nginx-pubhtml";

machine = { pkgs, ... }: {
services.nginx.enable = true;
services.nginx.virtualHosts.localhost = {
locations."~ ^/\\~([a-z0-9_]+)(/.*)?$".alias = "/home/$1/public_html$2";
};
users.users.foo.isNormalUser = true;
};

testScript = ''
$machine->waitForUnit("nginx");
$machine->waitForOpenPort(80);
$machine->succeed(
'chmod 0711 /home/foo',
"su -c 'mkdir -p /home/foo/public_html' foo",
"su -c 'echo bar > /home/foo/public_html/bar.txt' foo",
'test "$(curl -fvvv http://localhost/~foo/bar.txt)" = bar',
);
'';
}
29 changes: 15 additions & 14 deletions pkgs/servers/http/nginx/nix-etag-1.15.4.patch
Expand Up @@ -2,38 +2,35 @@ This patch makes it possible to serve static content from Nix store paths, by
using the hash of the store path for the ETag header.

diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c
index cb49ef74..f88dc77c 100644
index cb49ef74..7b456993 100644
--- a/src/http/ngx_http_core_module.c
+++ b/src/http/ngx_http_core_module.c
@@ -1583,6 +1583,7 @@ ngx_http_set_etag(ngx_http_request_t *r)
@@ -1583,6 +1583,8 @@ ngx_http_set_etag(ngx_http_request_t *r)
{
ngx_table_elt_t *etag;
ngx_http_core_loc_conf_t *clcf;
+ u_char *real, *ptr1, *ptr2;
+ ngx_err_t err;

clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);

@@ -1598,16 +1599,62 @@ ngx_http_set_etag(ngx_http_request_t *r)
@@ -1598,16 +1600,60 @@ ngx_http_set_etag(ngx_http_request_t *r)
etag->hash = 1;
ngx_str_set(&etag->key, "ETag");

- etag->value.data = ngx_pnalloc(r->pool, NGX_OFF_T_LEN + NGX_TIME_T_LEN + 3);
- if (etag->value.data == NULL) {
- etag->hash = 0;
- return NGX_ERROR;
+ err = ngx_errno;
+ real = ngx_realpath(clcf->root.data, NULL);
+ ngx_set_errno(err);
+
+ if (real == NULL) {
etag->hash = 0;
return NGX_ERROR;
}

- etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"",
- r->headers_out.last_modified_time,
- r->headers_out.content_length_n)
- - etag->value.data;
+ #define NIX_STORE_DIR "@nixStoreDir@"
+ #define NIX_STORE_LEN @nixStoreDirLen@
+
+ if (r->headers_out.last_modified_time == 1
+ && real != NULL
+ && !ngx_strncmp(real, NIX_STORE_DIR, NIX_STORE_LEN)
+ && real[NIX_STORE_LEN] == '/'
+ && real[NIX_STORE_LEN + 1] != '\0')
Expand Down Expand Up @@ -76,8 +73,12 @@ index cb49ef74..f88dc77c 100644
+ r->headers_out.last_modified_time,
+ r->headers_out.content_length_n)
+ - etag->value.data;
+ }
+
}

- etag->value.len = ngx_sprintf(etag->value.data, "\"%xT-%xO\"",
- r->headers_out.last_modified_time,
- r->headers_out.content_length_n)
- - etag->value.data;
+ ngx_free(real);

r->headers_out.etag = etag;
Expand Down

0 comments on commit 598a9cb

Please sign in to comment.