Currently, the only supported version is whatever is on main. This is a research artefact
rather than a released product, so there are no release lines and no maintained release
branches.
Phobos is a sandbox, so parts of it exist to do things a normal project would avoid, and the rest exists to take privileges away. None of the following is a vulnerability.
protecter/src/applies the sandbox.phobos.shand the scripts beside it, and thephobos-landlock-filesystem-and-networksystemprogram they run, build a Landlock policy from an allow-list and grant back only what the allow-list names, then restrict the process so it and everything it starts can only lose access. Code that assembles access rules from a configuration file looks like path injection, and is the mechanism. It needs no privilege: a task may always restrict itself further.protecter/src/phobos-seccomp-networksystem/phobos-seccomp-networksystem.cis the connect guard. When the network layer is on it supervises everyconnect()with a seccomp user-notification and makes an allowed connection itself from outside the sandboxed process, so forconnectit is a boundary a raw system call cannot step around, enforcing the[connect]allow-list by host and port. It reads the destination address of the connect, so it holds a rule that names an IP literal to that exact address, the namelocalhostto the loopback range (every127.x.x.xaddress and::1; an IPv4-mapped IPv6 destination such as::ffff:127.0.0.1counts as the IPv4 address it maps), a rule that names an IP range to that network, and a rule that names a DNS hostname it cannot tie to an address there to its port alone. Such a hostname rule's host is enforced by the egress broker, an HAProxy the network layer starts automatically for such a rule and that checks the TLS host name the guard cannot see; the network layer refuses an exact-name rule when no resolver is given, so an instructor who needs a rotating, CDN-backed host either gives a resolver or names its address range rather than its name. Since seccomp stops the call before the kernel path where Landlock would check the port, the guard connects outside Landlock and so is the whole connect boundary where it runs; Landlock's--connect-tcpports remain a second, kernel-enforced expression of the same ports. That second expression is absent for a transport whose rules include a loopback rule that names no port, which the shipped policies carry: Landlock cannot keep loopback open on every port and close the rest, so a rule that names a port beside it is enforced by the guard alone, and the network layer logs the ports that applies to. For udp this also means that such a rule does not need Landlock version 10. Aconnectof a family the guard does not carry (a UNIX-domain socket) is refused rather than made outside the Landlock view the command is held to. A[connect]/[bind]rule may carry audptransport marker: the guard enforces it for the UDP transport apart from TCP, and Landlock's--connect-udp/--bind-udp(the version-10 UDP rights) are its second, kernel-enforced expression. Those rights need Landlock version 10, and below it a udp rule that names a concrete port runs best effort: the enforcer leaves the kernel's UDP rules out and says so on every run, the guard alone holds where a datagram goes, and UDP bind stays open as it does for every run on such a kernel.--minimum-landlock-version 10in the tail flags refuses such a kernel instead. A[connect]rule beside a udp loopback wildcard, which Landlock cannot express with any version, is left to the guard alone on every kernel. A udp rule may name an exact host: a datagram carries no TLS host name for the egress broker to read, so the network layer resolves the name once, before the command starts, through the resolver the operator gave, hands the guard a rule for each address (at most sixteen) and shows the command the same addresses in/etc/hosts. The rule is held to what the name led to at the start of the run, which is a snapshot: an address the name gains later is not reachable, and one it loses stays reachable until the run ends. The lookup is the guard's own, in a mode that runs outside the sandbox before the command, reads the one answer a resolver gives with every length checked, takes an address only from a record that belongs to the name asked or to an alias chain that leads from it, and refuses a truncated or malformed answer, a name that does not resolve and an answer to another question; it asks only the resolver given, never/etc/resolv.conf. The name's owner decides which addresses it leads to, as for a TCP name, so a name that leads to a loopback or private address allows datagrams there. A name is mapped in/etc/hostsonly where the file holds none of it yet or exactly the addresses this run resolved, whoever's lines they are, because otherwise the command, or the command of another run that pinned the name, could be handed an address its own guard denies; the run is refused too where the addresses add up to more rules than the guard keeps. A name written in two cases is resolved once. External egress in general is still a container started with--network none, and resolving needs a networked one, so a name in a udp rule assumes the same posture as one in a tcp rule.docker/pruner/runs the discovery phase, which deliberately runs a reference under a policy that refuses too much, over and over: the layer pruner (docker/pruner/layers/,pruner/exercise_pruner/) records the grading layers' refusals withstraceand grants what each refusal proves, and its orchestrator merges the result into the allow-list the sandbox later trusts. strace and the containment probe are installed in the prune image only, never in the run-phase image a submission is graded in, and the prune container needs no privilege. The recording pruner (pruner/runtime_pruner/) is the exception to "the reference runs under a policy": while it records, the program runs with no sandbox at all, and it is for the instructor's own reference program, never for an untrusted submission. The run-phase image does not hold it, it is started only in the prune image, which the helpers are mounted into, and nothing underprotecter/src/names it. It refuses the grading options and a command whose words name a file of the layers (a wrapper script that starts them is not caught); the policy it generates covers only what the recorded sessions did, and says so.phobos-cli.sh submissionsruns the layer pruner on past student submissions, each staged over the exercise's template, and merges their policies. The pruner runs each baseline without Phobos, so a submission can shape its own pruned policy and so the merged one: this rests on the instructor's assumption that the submissions are vetted and harmless, and is no defence against a malicious one. What it does hold is that each submission runs in a fresh container without network, with only its own staged exercise (read-only) and its own output directory mounted, so it cannot change the template, the helpers, the extracted grading bases, another submission's input or result, or any host file outside its own output, and that every input is sealed by its digest and checked again before it is merged or verified. Nothing in either phase uses Bubblewrap any more.- The Dockerfiles under
docker/extend the Artemis Java test image and, for Python, the official Python image, and compile the C products. The run-phase image needs no user namespaces, no added capabilities and no security options: Landlock, the connect guard and the timeout are all self-imposed by the unprivileged process. The container the grader starts should add--network noneand cgroup limits, which are the outer boundary Phobos cannot set from inside itself.
The four C products, phobos-landlock-filesystem-and-networksystem, the connect guard, the
timeout's group lock and the report-only supervisor phobos-seccomp-filesystem, are not
committed. The connect guard carries the denial reporter's sources, so that it can word what it
and the filesystem layer block when it is the run's one supervisor. They are compiled inside the run-phase image from the source under protecter/src/, and CI
checks the copies the image ships are position-independent with full RELRO. Where the connect guard binary is missing
the network layer refuses to start rather than run the command without connect supervision, so a
bare checkout with nothing built does not run. The delivery vehicle is the run-phase image,
published multi-arch, so a grader pulls the build for its own architecture.
Phobos defends the grading machine against untrusted student code executed during a test run. It does not defend the student's own submission from anything, and it is not a general containment boundary against a determined attacker with local privilege. The allow-list is derived empirically by the pruning phase, so it is only as tight as the reference exercises that produced it: a resource no reference exercise touched is hidden, and a resource one of them touched is permitted for every submission thereafter.
The policy is additive: everything is denied first, and the platform, language and exercise configurations each only widen the allow-list. An exercise configuration may therefore name a path or grant a right the base did not, so the configuration files are trusted input, on the same footing as the reference exercises the pruning phase runs. They must be supplied by the instructor and must never be writable by the code being graded; a submission that could edit its own exercise configuration could grant itself any access, and that is an integration requirement Phobos relies on rather than a boundary it enforces.
An Ares 2 security policy given to --config grants access exactly as an exercise
configuration does, so the same holds for it: it must come from the instructor's test
repository, never from the student's assignment tree. In an Artemis build a
security-policy.yaml usually sits in the test repository's src/test/resources, and a
grading script that searched the merged working tree for such a file could pick up a copy the
submission placed there. The values an import depends on are operator input in the same way:
the project root (--project-root, or the tail flags' last --chdir), which must be the
directory the build tool starts the test JVM in, and the placeholder values a programming
language configuration determines from the environment, the PATH and the password database
of the process that runs phobos.sh. All of them are fixed before the command is started; a
grading setup that hands phobos.sh an environment the submission influenced breaks this.
A path a policy names is resolved through its symbolic links when the filesystem layer
applies it, just before the command starts, and by then the submission is in place. A symbolic
link the submission places at any component of a path that is only read or executed, or at any
component but the last of a path that may be changed (the enforcer refuses a changeable path
that is itself a link), makes the grant land where the link points. For an Ares 2 policy this is
a rule where the import can see the risk: a path the import names that reaches the project root,
which usually holds the submission's checkout, through a symbolic link, or through a name for the
root that leads to some other place than the root itself, and so resolves to somewhere other than
where the path says, is refused with the file and the line. A link before the path enters the root
that leads away from it, such as /bin to /usr/bin, stays allowed. The project root itself must
be the real path of a project directory: /, a root that reaches its directory through a symbolic
link and a root with a .. segment are refused with PHB-EPOLICY, from --project-root always
and from the last --chdir where an imported path needs the root, because what lies below such a root cannot be told from
a link. The check is made when the specification is built, so a link the submission places between that moment and the start of the command is not
seen; the grader builds the specification after the submission is in place and starts the command
right after. A path in an exercise configuration that lies in the submission's tree carries the
risk as before: grant such a path only where no component of it is the submission's to choose.
A grader may start Phobos with the submission's tree as the current directory, so anything
Phobos looks up relative to that directory before the sandbox exists is the submission's to
choose. Every entry point, phobos.sh and each layer started on its own alike, therefore
begins with clean_startup_environment (protecter/src/phobos-tools-common/phobos-environment.sh),
before it runs any program or cd:
- PATH keeps only its absolute entries.
., every other relative directory, an empty entry (a leading or trailing colon, or two in a row) and an entry beginning with~, which bash expands butexecvptakes as a directory in the current one, are removed. Absolute entries are kept as written, whether or not they exist, since they name the same place from every directory, and none is ever added: a PATH with no absolute entry is refused withPHB-ERUNTIME, because an empty PATH is searched as the current directory. The command is given the cleaned PATH too, so a command that relied on.must be named by its path, as in./gradlew test. Inside the sandbox a relative entry could reach nothing Landlock denies, but one PATH for the whole run means no helper of Phobos can meet the original by mistake. Where the environment has no PATH at all, bash invents one for its own lookups, and some builds end it in.: that one is cleaned too, but stays unexported, so the command is given no PATH, as before. - CDPATH is unset, so the
cdan entry point makes to find its own directory goes where its argument says, and the command does not inherit it. - TMPDIR and the C library's own path variables keep only what is absolute.
TMPDIRis unset when it is not absolute, so no program Phobos or the command starts makes its temporary files beneath the current directory. The relative entries ofGCONV_PATH(where the C library loads character-set modules from, as code),LOCPATHandNLSPATHare removed, andHOSTALIASESandTZDIRare unset when they are not absolute, since every program Phobos starts would resolve them against the current directory. Absolute values are kept.
Each entry point says on stderr what it removed from these, CDPATH and an invented PATH aside.
The interpreter is not looked up either. Every script under protecter/src/ begins with #!/bin/bash, so
an entry point started as a program runs the system's bash whatever the caller's PATH holds, and
so does every layer it starts afterwards by its absolute path. With #!/usr/bin/env bash, env
would find bash through the caller's PATH before the script ran a line, and a relative entry
ahead of bash's own directory would run a bash from the current directory instead of Phobos.
The run-phase image, and every image built on it, has bash at /bin/bash.
Some of the same class happens before a script's first line, so no script can undo it, and these are integration requirements on the grader:
-
Start Phobos without
BASH_ENV. Bash sources the file it names before the first line of every non-interactive script, Phobos's own among them, and resolves a relative name against the current directory.ENVis read only by interactive shells and does not reach Phobos. -
Start Phobos without
LD_LIBRARY_PATH,LD_PRELOADandLD_AUDIT. The dynamic loader reads them when every program starts, the bash runningphobos.shincluded, and searches a relative or emptyLD_LIBRARY_PATHentry in the current directory. Removing them inside the script would come after that bash had already loaded its libraries, so it is not done. For the same reason the bash runningphobos.shmay already have read locale data or loaded character-set modules through a relativeLOCPATHorGCONV_PATHbefore they are cleaned for everything after it. -
Start Phobos with no network socket the command could reach. The connect guard pins and filters the sockets it creates; one the command inherits, on any descriptor, standard input, output and error included, or receives later over an inherited UNIX socket, was never in its hands. Phobos does not close the descriptors it is started with, so the grader must not hand it one.
The simplest way to meet the first two is a minimal environment of absolute values:
env -i PATH=/usr/local/bin:/usr/bin:/bin /var/tmp/opt/core/phobos.sh --config exercise.cfg -- ./gradlew test,
adding back only what the command needs.
Every temporary file Phobos makes for a run, the port lists the network layer builds its Landlock
rules from among them, lives in the run's specification directory, most of them in its scratch
subdirectory. The policy program refuses to place a specification directory it builds beneath a
write path; one handed straight to a layer is the caller's to place. Each layer that makes such
files sets the scratch directory itself, rather than taking PHOBOS_SCRATCH or TMPDIR from its
environment, and a helper called without one refuses with PHB-ERUNTIME rather than fall back to
TMPDIR, or to /tmp, which the shipped policies make writable, so the command of a concurrent
run in the same container could see such a file, and as the same user replace it, while another
run still reads its rules from it.
Every blocked action Phobos can attribute with certainty is worded on stderr, once per distinct
action and run, and one Phobos Security Summary line closes a run that blocked anything. This is
a report and never a decision: a supervisor answers the calls it watches for observation only with
"continue", so Landlock alone decides them, and the calls a Phobos filter refuses outright
(io_uring, a foreign ABI, and setsid and setpgid where the supervisor cannot continue a call)
are answered with EACCES by a handler that has no way to let a call through. Where it can continue a
call, setsid and setpgid are not refused: the supervisor answers them from a ledger of virtual
sessions and groups without running them, so every process stays in the group the timeout kills. While a supervisor serves, the errno a program sees for those calls
depends on it answering correctly where it once depended on the kernel alone; if the supervisor
dies, the kernel refuses them with ENOSYS, and every other watched call fails with ENOSYS too.
Nothing is granted in any state.
What a line proves is limited, and is meant for the person reading a log:
- The command writes to the same standard error, so it can print a line that looks the same. The counts in the summary are Phobos's own decisions; the lines are text.
- The egress broker reports the TLS host names it refuses through an anonymous pipe that only
HAProxy, the connect guard and the network layer's own shell hold, so no path names it and no
policy can grant the command access. A command that could open the guard's descriptor through
/proc/<pid>/fdcould write a forged broker line, which changes what is printed and what the network count says, and nothing the broker allows or refuses. It could also read lines from the pipe before the guard does, and so keep them from being worded or make the end of the run wait out its grace; it cannot end that wait early, because each handoff's token is drawn at random for that handoff alone and a token read from the pipe tells nothing about another. Against the bare guard, with no Landlock domain at all, it can. Underphobos.shit could not in the measured runs, with every layer on and with-nfr, but what stops it is the kernel's own access rule for another process's/procentries, not something Phobos enforces, so nothing here relies on it. - A connect to a UNIX socket is worded only for the command and what it started. Every layer's shell asks the name service cache for the user at start-up and is refused by the guard too; those are Phobos's own helpers, not the program, and are not worded. With the filesystem layer off there is no domain to tell them apart by, so no UNIX connect is worded.
- Some blocked actions are never worded: a call another filter refuses first (the container's own
profile), an
openat2with resolve flags, anO_TMPFILEopen, a call relative to a deleted directory, the interpreter the kernel opens for anexecve, aconnectto a UNIX socket under-nnr, process, open file and memory limits (they surface as errors the command handles), a refused inbound connection (the actor is an outside client), a broker line lost to a full pipe, one the broker writes more than two seconds after the command has ended, one owed when the run was being ended by a signal (the guard then waits for nothing), and one for a handoff the broker never turned into a connection. A doubt always ends in silence, so a run without a line may still have been refused something. - The broker logs every connection the guard hands it, allowed or refused, each with a token the guard counts back, and the guard reads on after the command has ended until every token has come back, for at most two seconds. So a run with a [connect] rule that names a host can end up to two seconds later than its command, when a connection the command made was still being resolved or connected onward as it ended. Only a refusal is worded.
An [accept] rule fronts a student's TCP listener with an inbound HAProxy that admits only the
source addresses the rule names, forwarding an admitted connection to the student's backend port.
It is a defence-in-depth layer for deployments that expose a student's server, not a boundary of
the same class as the filesystem or egress layers, and enabling it changes the run's posture:
-
It removes
--network none. An external client cannot reach a--network nonecontainer, so an[accept]rule only works where the container has a real network. Turning it on therefore gives up the hard no-network backstop the rest of the sandbox leans on; egress is then contained only by the connect guard, the Landlock TCP-port rules, the egress broker when it is on and whatever the integrator firewalls, not by the absence of a network. -
Phobos locks the listening port, and keeps the listener on loopback. With
[bind]naming only the backend port, Landlock refuses the student a listener on any other port, the public port included, in the kernel and against raw system calls. Landlock's bind right is per port, not per address, so the student may bind the backend port on all interfaces, but the connect guard pins every listener it lets listen to the loopback device (below), so the backend is reached from this host only, the inbound filter's own forwarding to127.0.0.1included. The container still decides what is published: only the public port, and[accept]covers TCP only. -
The connect guard closes the listen that never binds. Landlock judges
bind(), but the kernel also gives a socket that was never bound a port of its own choosing when it callslisten(), and Landlock has no hook for that. The connect guard therefore trapslisten()and runs it itself, only on a socket it created for the command, and refuses a socket that is unbound unless it is started with--allow-ephemeral-listen, which the network layer sets when a[bind]row names port 0, because that row already grants the port the kernel would choose. A swap of another socket under the descriptor cannot create a listener, because the guard never lets the kernel run the command's ownlisten(). With--no-networksystem-restrictionthe guard is absent and so is this. -
The connect guard keeps a submission's sockets local, from inside. Phobos must hold in a container it did not start, so it does not leave a submission's inbound reach to an outer
--network none:- A listener is pinned to the loopback device. Within the
listen()it runs itself, the guard first binds a socket that has no port to the address it holds and a port the kernel picks (the grant--allow-ephemeral-listenalready gives), withIP_BIND_ADDRESS_NO_PORTcleared, and refuses the listen when the socket still has no port; then it setsSO_BINDTODEVICE "lo", reads it back and only then listens. A socket bound tolotakes a connection only from this host, over IPv4, IPv6 and IPv4-mapped alike, whatever address it bound, and every socket it accepts inherits the device. The bind comes first because the IPv6 bind of a scoped address sets a socket's device with no privilege and keeps it when the bind fails, and the kernel refuses every bind of a socket that already has a port before it reads the scope. A listen the guard cannot pin, because an unprivileged guard cannot change a device the command set or the device reads back as another, is refused. - A datagram socket receives only from the host and from the peers a UDP rule names. Before
the command holds a datagram socket, the guard attaches a classic BPF socket filter to it and
locks it (
SO_LOCK_FILTER), so it cannot be detached, replaced or unlocked, by root either, and it stays with the socket however the descriptor is duplicated, closed or evicted. A source in 127.0.0.0/8 or ::1 is taken if and only if it arrived on the loopback interface, whatever a rule says, so aroute_localnetconfiguration that admits such a source from outside does not readmit it; any other source is taken only when a UDP[connect]rule names it by address or range. A UDP rule whose host the guard cannot tie to an address (a wildcard, or a name it was not given resolved) allows an answer from anywhere, so then no filter is attached. A kernel or security module that refuses socket filters on datagram sockets (EPERM, EACCES or ENOPROTOOPT at start-up) gets a warning and an unfiltered run, the best effort a kernel without a Landlock right gets too; any other failure ends the run, so a defect never becomes an unfiltered run. Classic socket filters on TCP needCAP_NET_ADMINsince kernel commit 5d39580f68e6, which is why a listener is pinned instead. - The command cannot undo either. The guard's filter refuses
setsockoptofSO_BINDTODEVICEandSO_BINDTOIFINDEX, which would move a socket offlo, and ofIP_RECVERRandIPV6_RECVERR, because a socket's error queue takes the quoted payload of an ICMP error past its receive filter. Each refusal is reported.
What this leaves: a run with the network layer off has none of it; a datagram that spoofs the address of a peer a UDP rule names is received, since UDP has no handshake; ICMP errors and path-MTU feedback still reach a socket as kernel-generated information (an
errnoon a connected datagram socket,IPV6_PATHMTU), never as quoted payload; an answer to a multicast or broadcast send comes from a unicast address the rule does not name and is dropped, so a policy that needs such answers names their sources too; a client connecting to the container's own non-loopback address, asInetAddress.getLocalHost()gives it, is refused for TCP and not answered for UDP; and a network socket the guard did not create is neither pinned nor filtered, which the integration requirement on network descriptors below rules out. The suites read the device, the lock and the refusals back from the kernel, with a control without the guard for each; reaching a listener from a second host is shown by a measurement with two containers, not yet by a suite. - A listener is pinned to the loopback device. Within the
-
The connect guard makes every datagram connect and send itself. A destination behind a pointer cannot be checked and then left to the kernel: re-running the call reads the pointer again, and a second thread of the command can show the check one address and the kernel another. Measured against a destination that thread rewrites, that sent about one datagram in six to an address the allow-list did not name. So the guard copies the address, the data and the lengths out of the command once, checks the copies, and sends from them on its own descriptor for the very socket the command holds, never letting the call continue. The datagram
connect()is made the same way, which is why the command's own address-lesssend()then reaches a peer the guard vetted, and asendto()without an address is the one send the filter lets through. The guard enforces the destination port and the source port itself, because it runs outside Landlock: a connect or send on a socket that was never bound is refused unless the policy grants an ephemeral UDP bind (--allow-ephemeral-udp-bind, set by the network layer for a udp[bind]row of port 0 or any udp[connect]rule), since the kernel would give it a source port nobody judged. This costs, and the costs are the price of closing the race rather than defects: a datagram socket stays held by the guard after the command closes it until the table of 512 held sockets evicts it, so a port it was bound to stays taken and datagrams queued for it stay in memory; a send on a socket the guard does not hold (inherited, received over a descriptor, or evicted) is refused;sendmsgandsendmmsgare refused on every socket the guard does not hold, which includes TCP and UNIX sockets, because a second thread could swap a datagram socket under such a descriptor and name a destination; a send with ancillary data, a flag the guard cannot pass on (MSG_ZEROCOPY,MSG_OOB), a datagram beyond 64 KiB and asendtoorsendmsgwith a UNIX address are refused; and a blocking socket that cannot take a datagram is waited on for a second at most before the answer isEAGAIN. The guard serves one notification at a time, so a slow socket delays every other call of the command by that second at most.protecter/test/integration/seccomp_networksystem.shmeasures all four calls against the rewritten destination, with a control without the guard that reaches it. -
Bind is closed unless a
[bind]row opens it, as far as the kernel can close it. The network layer always applies a network-only Landlock ruleset that handles TCP and UDP bind with nothing granted, so a run with no[bind]rule, or given no--config, binds and listens on nothing. Port 0 grants only a port the kernel chooses. The shipped Java policy carriesallow 0andallow 0 udpfor Gradle, so a submission under it can still take a kernel-chosen port, which the guard pins to loopback when it listens, but cannot name one; because policies are additive, an exercise that names onlyallow 8080gets port 0 from that base as well. A kernel below Landlock version 4 (TCP) or 10 (UDP) cannot close the direction: the enforcer says so on every run and leaves the port open, while a listener on it is still pinned and a datagram socket still filtered, so what stays open is the port, not the reach from outside.--minimum-landlock-versionin the tail flags turns that into a refusal. At the time of writing none of the kernels the project's CI and local runs use offers version 10, so the UDP half has been exercised against a recording of the kernel's calls only. -
A source address is weak authentication. It resists spoofing only for an established handshake, and NAT and shared egress addresses make it coarse. Treat it as a filter, not an identity.
Enabling [accept] is deliberately not silent: a run with one present says loudly that it assumes
this networked posture.
A report is in scope when Phobos fails at what it claims to do, or when it causes harm nobody asked for. Concretely:
- a submission reaching a file, or a TCP or (on Landlock version 10) UDP port Landlock was given, that the active allow-list does not permit
- a listener the connect guard let listen, or a datagram socket it created, taking from outside the host what the section on inbound filtering says it cannot
- the sandbox failing open, that is, running the tests unconfined while reporting success
- the pruning phase writing an allow-list that grants more than the runs it observed required
- privilege escalation out of the sandbox onto the host
- any defect in this repository's own scripts that damages the machine running them beyond the working tree they were pointed at
Out of scope: the deliberately dangerous code listed above, anything the container's own
settings are responsible for rather than Phobos (the residuals of the network layer that the
section on inbound filtering names, which --network none closes, and the hard resource caps,
which cgroups set), JVM-internal
attacks that never touch the operating system, which are Ares's responsibility, and anything
that follows from running the discovery phase against a project you were not willing to have
repeatedly broken.
If the problem relates to a bug that is associated with unexpected behaviour or inconvenience or something non-critical is broken, simply report it as a bug and use the issues for that.
If the problem relates to a vulnerability that could be used maliciously or is in another way a security issue, please do not make the issue public. Instead, collect the following information first:
- as with a bug report, describe how the vulnerability can be reproduced
- state the commit, the allow-list in use and the language environment the run used
- state the kernel and whether the run was inside a container, since the enforcement mechanism depends on both
- provide any additional information and context, if possible
Then report it through GitHub's private vulnerability reporting, which is enabled on this repository. The report stays private while it is assessed and remediated.