Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend testing of JS runtime limits #5594

Merged
merged 12 commits into from
Sep 19, 2023
96 changes: 88 additions & 8 deletions tests/js-custom-authorization/custom_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import re
from http import HTTPStatus
import subprocess
from contextlib import contextmanager

from loguru import logger as LOG

Expand All @@ -42,16 +43,61 @@ def run(args):
network = test_custom_auth(network, args)


# Context manager to temporarily set JS execution limits.
# NB: Limits are currently applied to governance runtimes as well, so limits
# must be high enough that a proposal to restore the defaults can pass.
@contextmanager
def temporary_js_limits(network, primary, **kwargs):
with primary.client() as c:
# fetch defaults from js_metrics endpoint
r = c.get("/node/js_metrics")
assert r.status_code == http.HTTPStatus.OK, r.status_code
body = r.body.json()
default_max_heap_size = body["max_heap_size"]
default_max_stack_size = body["max_stack_size"]
default_max_execution_time = body["max_execution_time"]

default_kwargs = {
"max_heap_bytes": default_max_heap_size,
"max_stack_bytes": default_max_stack_size,
"max_execution_time_ms": default_max_execution_time,
}

temp_kwargs = default_kwargs.copy()
temp_kwargs.update(**kwargs)
network.consortium.set_js_runtime_options(
primary,
**temp_kwargs,
)

yield

# Restore defaults
network.consortium.set_js_runtime_options(primary, **default_kwargs)


@reqs.description("Test stack size limit")
def test_stack_size_limit(network, args):
primary, _ = network.find_nodes()

with primary.client("user0") as c:
r = c.post("/app/recursive", body={"depth": 50})
safe_depth = 50
unsafe_depth = 2000

with primary.client() as c:
r = c.post("/app/recursive", body={"depth": safe_depth})
assert r.status_code == http.HTTPStatus.OK, r.status_code

with primary.client("user0") as c:
r = c.post("/app/recursive", body={"depth": 2000})
# Stacks are significantly larger in SGX (and larger still in debug).
# So we need a platform-specific value to fail _this_ test, but still permit governance to pass
msb = 400 * 1024 if args.enclave_platform == "sgx" else 40 * 1024
with temporary_js_limits(network, primary, max_stack_bytes=msb):
r = c.post("/app/recursive", body={"depth": safe_depth})
assert r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR, r.status_code

r = c.post("/app/recursive", body={"depth": safe_depth})
assert r.status_code == http.HTTPStatus.OK, r.status_code

r = c.post("/app/recursive", body={"depth": unsafe_depth})
assert r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR, r.status_code

return network
Expand All @@ -61,12 +107,45 @@ def test_stack_size_limit(network, args):
def test_heap_size_limit(network, args):
primary, _ = network.find_nodes()

with primary.client("user0") as c:
r = c.post("/app/alloc", body={"size": 5 * 1024 * 1024})
safe_size = 5 * 1024 * 1024
unsafe_size = 500 * 1024 * 1024

with primary.client() as c:
r = c.post("/app/alloc", body={"size": safe_size})
assert r.status_code == http.HTTPStatus.OK, r.status_code

with primary.client("user0") as c:
r = c.post("/app/alloc", body={"size": 500 * 1024 * 1024})
with temporary_js_limits(network, primary, max_heap_bytes=3 * 1024 * 1024):
r = c.post("/app/alloc", body={"size": safe_size})
assert r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR, r.status_code

r = c.post("/app/alloc", body={"size": safe_size})
assert r.status_code == http.HTTPStatus.OK, r.status_code

r = c.post("/app/alloc", body={"size": unsafe_size})
assert r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR, r.status_code

return network


@reqs.description("Test execution time limit")
def test_execution_time_limit(network, args):
primary, _ = network.find_nodes()

safe_time = 50
unsafe_time = 5000

with primary.client() as c:
r = c.post("/app/sleep", body={"time": safe_time})
assert r.status_code == http.HTTPStatus.OK, r.status_code

with temporary_js_limits(network, primary, max_execution_time_ms=30):
r = c.post("/app/sleep", body={"time": safe_time})
assert r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR, r.status_code

r = c.post("/app/sleep", body={"time": safe_time})
assert r.status_code == http.HTTPStatus.OK, r.status_code

r = c.post("/app/sleep", body={"time": unsafe_time})
assert r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR, r.status_code

return network
Expand All @@ -79,6 +158,7 @@ def run_limits(args):
network.start_and_open(args)
network = test_stack_size_limit(network, args)
network = test_heap_size_limit(network, args)
network = test_execution_time_limit(network, args)


@reqs.description("Cert authentication")
Expand Down
14 changes: 12 additions & 2 deletions tests/js-limits/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"js_module": "limits.js",
"js_function": "recursive",
"forwarding_required": "sometimes",
"authn_policies": ["user_cert"],
"authn_policies": ["no_auth"],
"mode": "readonly",
"openapi": {}
}
Expand All @@ -15,7 +15,17 @@
"js_module": "limits.js",
"js_function": "alloc",
"forwarding_required": "sometimes",
"authn_policies": ["user_cert"],
"authn_policies": ["no_auth"],
"mode": "readonly",
"openapi": {}
}
},
"/sleep": {
"post": {
"js_module": "limits.js",
"js_function": "sleep",
"forwarding_required": "sometimes",
"authn_policies": ["no_auth"],
"mode": "readonly",
"openapi": {}
}
Expand Down
14 changes: 14 additions & 0 deletions tests/js-limits/src/limits.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ export function alloc(request) {
new Uint8Array(size);
return {};
}

export function sleep(request) {
const time = request.body.json()["time"];
ccf.enableUntrustedDateTime(true);
const start = new Date();
while (true) {
const now = new Date();
const diff = now - start;
if (diff > time) {
break;
}
}
return {};
}
68 changes: 0 additions & 68 deletions tests/js-modules/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,73 +991,6 @@ def test_npm_app(network, args):
return network


@reqs.description("Test JS execution time out with npm app endpoint")
def test_js_execution_time(network, args):
primary, _ = network.find_nodes()

LOG.info("Deploying npm app")
app_dir = os.path.join(PARENT_DIR, "npm-app")
bundle_path = os.path.join(
app_dir, "dist", "bundle.json"
) # Produced by build step of test npm-app in the previous test_npm_app
network.consortium.set_js_app_from_json(primary, bundle_path)

LOG.info("Store JWT signing keys")
jwt_key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
jwt_cert_pem = infra.crypto.generate_cert(jwt_key_priv_pem)
jwt_kid = "my_other_key_id"
issuer = "https://example.issuer"
with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
jwt_cert_der = infra.crypto.cert_pem_to_der(jwt_cert_pem)
der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
data = {
"issuer": issuer,
"jwks": {"keys": [{"kty": "RSA", "kid": jwt_kid, "x5c": [der_b64]}]},
}
json.dump(data, metadata_fp)
metadata_fp.flush()
network.consortium.set_jwt_issuer(primary, metadata_fp.name)

LOG.info("Calling jwt endpoint after storing keys")
with primary.client("user0") as c:
# fetch defaults from js_metrics endpoint
r = c.get("/node/js_metrics")
body = r.body.json()
default_max_heap_size = body["max_heap_size"]
default_max_stack_size = body["max_stack_size"]
default_max_execution_time = body["max_execution_time"]

# set JS execution time to a lower value which will timeout this
# endpoint execution
network.consortium.set_js_runtime_options(
primary,
max_heap_bytes=50 * 1024 * 1024,
max_stack_bytes=1024 * 512,
max_execution_time_ms=1,
)
user_id = "user0"
jwt = infra.crypto.create_jwt({"sub": user_id}, jwt_key_priv_pem, jwt_kid)

r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
assert r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR, r.status_code
body = r.body.json()
assert body["error"]["message"] == "Operation took too long to complete."

# reset the execution time
network.consortium.set_js_runtime_options(
primary,
max_heap_bytes=default_max_heap_size,
max_stack_bytes=default_max_stack_size,
max_execution_time_ms=default_max_execution_time,
)
r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
assert r.status_code == http.HTTPStatus.OK, r.status_code
body = r.body.json()
assert body["userId"] == user_id, r.body

return network


@reqs.description("Test JS exception output")
def test_js_exception_output(network, args):
primary, _ = network.find_nodes()
Expand Down Expand Up @@ -1165,7 +1098,6 @@ def run(args):
network = test_dynamic_endpoints(network, args)
network = test_set_js_runtime(network, args)
network = test_npm_app(network, args)
network = test_js_execution_time(network, args)
network = test_js_exception_output(network, args)
network = test_user_cose_authentication(network, args)

Expand Down