Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
M
Magisk
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Magisk
Commits
d119dd9a
Commit
d119dd9a
authored
Oct 04, 2018
by
topjohnwu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Rewrite su daemon and client
parent
09ef19f7
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
345 additions
and
409 deletions
+345
-409
daemon.c
native/jni/daemon/daemon.c
+1
-1
socket.c
native/jni/daemon/socket.c
+9
-7
daemon.h
native/jni/include/daemon.h
+2
-2
connect.c
native/jni/su/connect.c
+24
-24
su.c
native/jni/su/su.c
+138
-165
su.h
native/jni/su/su.h
+11
-16
su_daemon.c
native/jni/su/su_daemon.c
+160
-194
No files found.
native/jni/daemon/daemon.c
View file @
d119dd9a
...
...
@@ -67,7 +67,7 @@ static void *request_handler(void *args) {
ls_hide_list
(
client
);
break
;
case
SUPERUSER
:
su_daemon_
receiv
er
(
client
,
&
credential
);
su_daemon_
handl
er
(
client
,
&
credential
);
break
;
case
CHECK_VERSION
:
write_string
(
client
,
xstr
(
MAGISK_VERSION
)
":MAGISK"
);
...
...
native/jni/daemon/socket.c
View file @
d119dd9a
...
...
@@ -39,7 +39,7 @@ int create_rand_socket(struct sockaddr_un *sun) {
return
fd
;
}
int
socket_accept
(
int
s
erv_
fd
,
int
timeout
)
{
int
socket_accept
(
int
s
ock
fd
,
int
timeout
)
{
struct
timeval
tv
;
fd_set
fds
;
int
rc
;
...
...
@@ -47,16 +47,16 @@ int socket_accept(int serv_fd, int timeout) {
tv
.
tv_sec
=
timeout
;
tv
.
tv_usec
=
0
;
FD_ZERO
(
&
fds
);
FD_SET
(
s
erv_
fd
,
&
fds
);
FD_SET
(
s
ock
fd
,
&
fds
);
do
{
rc
=
select
(
s
erv_
fd
+
1
,
&
fds
,
NULL
,
NULL
,
&
tv
);
rc
=
select
(
s
ock
fd
+
1
,
&
fds
,
NULL
,
NULL
,
&
tv
);
}
while
(
rc
<
0
&&
errno
==
EINTR
);
if
(
rc
<
1
)
{
PLOGE
(
"select"
);
exit
(
-
1
)
;
return
-
1
;
}
return
xaccept4
(
s
erv_
fd
,
NULL
,
NULL
,
SOCK_CLOEXEC
);
return
xaccept4
(
s
ock
fd
,
NULL
,
NULL
,
SOCK_CLOEXEC
);
}
/*
...
...
@@ -164,13 +164,15 @@ void send_fd(int sockfd, int fd) {
int
read_int
(
int
fd
)
{
int
val
;
xxread
(
fd
,
&
val
,
sizeof
(
int
));
if
(
xxread
(
fd
,
&
val
,
sizeof
(
int
))
!=
sizeof
(
int
))
return
-
1
;
return
val
;
}
int
read_int_be
(
int
fd
)
{
uint32_t
val
;
xxread
(
fd
,
&
val
,
sizeof
(
val
));
if
(
xxread
(
fd
,
&
val
,
sizeof
(
val
))
!=
sizeof
(
int
))
return
-
1
;
return
ntohl
(
val
);
}
...
...
native/jni/include/daemon.h
View file @
d119dd9a
...
...
@@ -61,7 +61,7 @@ int check_and_start_logger();
socklen_t
setup_sockaddr
(
struct
sockaddr_un
*
sun
,
daemon_t
d
);
int
create_rand_socket
(
struct
sockaddr_un
*
sun
);
int
socket_accept
(
int
s
erv_
fd
,
int
timeout
);
int
socket_accept
(
int
s
ock
fd
,
int
timeout
);
int
recv_fd
(
int
sockfd
);
void
send_fd
(
int
sockfd
,
int
fd
);
int
read_int
(
int
fd
);
...
...
@@ -98,6 +98,6 @@ void ls_hide_list(int client);
* Superuser *
*************/
void
su_daemon_
receiv
er
(
int
client
,
struct
ucred
*
credential
);
void
su_daemon_
handl
er
(
int
client
,
struct
ucred
*
credential
);
#endif
native/jni/su/connect.c
View file @
d119dd9a
...
...
@@ -14,86 +14,86 @@
#include "magisk.h"
#include "daemon.h"
#include "utils.h"
#include "su.h"
#define AM_PATH "/system/bin/app_process", "/system/bin", "com.android.commands.am.Am"
static
char
*
get_command
(
const
struct
su_request
*
to
)
{
if
(
to
->
command
)
if
(
to
->
command
[
0
]
)
return
to
->
command
;
if
(
to
->
shell
)
if
(
to
->
shell
[
0
]
)
return
to
->
shell
;
return
DEFAULT_SHELL
;
}
static
void
silent_run
(
char
*
const
args
[])
{
set_identity
(
0
);
if
(
fork
())
if
(
fork_dont_care
())
return
;
int
zero
=
open
(
"/dev/zero"
,
O_RDONLY
|
O_CLOEXEC
);
dup2
(
zero
,
0
);
x
dup2
(
zero
,
0
);
int
null
=
open
(
"/dev/null"
,
O_WRONLY
|
O_CLOEXEC
);
dup2
(
null
,
1
);
dup2
(
null
,
2
);
x
dup2
(
null
,
1
);
x
dup2
(
null
,
2
);
setenv
(
"CLASSPATH"
,
"/system/framework/am.jar"
,
1
);
execv
(
args
[
0
],
args
);
PLOGE
(
"exec am"
);
_exit
(
EXIT_FAILURE
);
}
static
void
setup_user
(
char
*
user
)
{
switch
(
DB_SET
(
su_ctx
->
info
,
SU_MULTIUSER_MODE
))
{
static
void
setup_user
(
char
*
user
,
struct
su_info
*
info
)
{
switch
(
DB_SET
(
info
,
SU_MULTIUSER_MODE
))
{
case
MULTIUSER_MODE_OWNER_ONLY
:
case
MULTIUSER_MODE_OWNER_MANAGED
:
sprintf
(
user
,
"%d"
,
0
);
break
;
case
MULTIUSER_MODE_USER
:
sprintf
(
user
,
"%d"
,
su_ctx
->
info
->
uid
/
100000
);
sprintf
(
user
,
"%d"
,
info
->
uid
/
100000
);
break
;
}
}
void
app_log
()
{
void
app_log
(
struct
su_context
*
ctx
)
{
char
user
[
8
];
setup_user
(
user
);
setup_user
(
user
,
ctx
->
info
);
char
fromUid
[
8
];
sprintf
(
fromUid
,
"%d"
,
DB_SET
(
su_
ctx
->
info
,
SU_MULTIUSER_MODE
)
==
MULTIUSER_MODE_OWNER_MANAGED
?
su_ctx
->
info
->
uid
%
100000
:
su_
ctx
->
info
->
uid
);
DB_SET
(
ctx
->
info
,
SU_MULTIUSER_MODE
)
==
MULTIUSER_MODE_OWNER_MANAGED
?
ctx
->
info
->
uid
%
100000
:
ctx
->
info
->
uid
);
char
toUid
[
8
];
sprintf
(
toUid
,
"%d"
,
su_ctx
->
to
.
uid
);
sprintf
(
toUid
,
"%d"
,
ctx
->
req
.
uid
);
char
pid
[
8
];
sprintf
(
pid
,
"%d"
,
su_
ctx
->
pid
);
sprintf
(
pid
,
"%d"
,
ctx
->
pid
);
char
policy
[
2
];
sprintf
(
policy
,
"%d"
,
su_
ctx
->
info
->
access
.
policy
);
sprintf
(
policy
,
"%d"
,
ctx
->
info
->
access
.
policy
);
char
*
cmd
[]
=
{
AM_PATH
,
"broadcast"
,
"-a"
,
"android.intent.action.BOOT_COMPLETED"
,
"-p"
,
DB_STR
(
su_
ctx
->
info
,
SU_MANAGER
),
"-p"
,
DB_STR
(
ctx
->
info
,
SU_MANAGER
),
"--user"
,
user
,
"--es"
,
"action"
,
"log"
,
"--ei"
,
"from.uid"
,
fromUid
,
"--ei"
,
"to.uid"
,
toUid
,
"--ei"
,
"pid"
,
pid
,
"--ei"
,
"policy"
,
policy
,
"--es"
,
"command"
,
get_command
(
&
su_ctx
->
to
),
"--es"
,
"command"
,
get_command
(
&
ctx
->
req
),
NULL
};
silent_run
(
cmd
);
}
void
app_connect
(
const
char
*
socket
)
{
void
app_connect
(
const
char
*
socket
,
struct
su_info
*
info
)
{
char
user
[
8
];
setup_user
(
user
);
setup_user
(
user
,
info
);
char
*
cmd
[]
=
{
AM_PATH
,
"broadcast"
,
"-a"
,
"android.intent.action.BOOT_COMPLETED"
,
"-p"
,
DB_STR
(
su_ctx
->
info
,
SU_MANAGER
),
"-p"
,
DB_STR
(
info
,
SU_MANAGER
),
"--user"
,
user
,
"--es"
,
"action"
,
"request"
,
"--es"
,
"socket"
,
(
char
*
)
socket
,
...
...
@@ -102,8 +102,8 @@ void app_connect(const char *socket) {
silent_run
(
cmd
);
}
void
socket_send_request
(
int
fd
)
{
write_key_token
(
fd
,
"uid"
,
su_ctx
->
info
->
uid
);
void
socket_send_request
(
int
fd
,
struct
su_info
*
info
)
{
write_key_token
(
fd
,
"uid"
,
info
->
uid
);
write_string_be
(
fd
,
"eof"
);
}
native/jni/su/su.c
View file @
d119dd9a
...
...
@@ -19,7 +19,6 @@
#include <errno.h>
#include <signal.h>
#include <sched.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
...
...
@@ -27,11 +26,9 @@
#include "daemon.h"
#include "utils.h"
#include "su.h"
#include "
selinux
.h"
#include "
pts
.h"
#include "flags.h"
struct
su_context
*
su_ctx
;
static
void
usage
(
int
status
)
{
FILE
*
stream
=
(
status
==
EXIT_SUCCESS
)
?
stdout
:
stderr
;
...
...
@@ -49,7 +46,7 @@ static void usage(int status) {
" -V display version code and exit
\n
"
" -mm, -M,
\n
"
" --mount-master force run in the global mount namespace
\n
"
);
exit
2
(
status
);
exit
(
status
);
}
static
char
*
concat_commands
(
int
argc
,
char
*
argv
[])
{
...
...
@@ -64,89 +61,33 @@ static char *concat_commands(int argc, char *argv[]) {
return
strdup
(
command
);
}
static
void
populate_environment
()
{
struct
passwd
*
pw
;
if
(
su_ctx
->
to
.
keepenv
)
return
;
pw
=
getpwuid
(
su_ctx
->
to
.
uid
);
if
(
pw
)
{
setenv
(
"HOME"
,
pw
->
pw_dir
,
1
);
if
(
su_ctx
->
to
.
shell
)
setenv
(
"SHELL"
,
su_ctx
->
to
.
shell
,
1
);
else
setenv
(
"SHELL"
,
DEFAULT_SHELL
,
1
);
if
(
su_ctx
->
to
.
login
||
su_ctx
->
to
.
uid
)
{
setenv
(
"USER"
,
pw
->
pw_name
,
1
);
setenv
(
"LOGNAME"
,
pw
->
pw_name
,
1
);
}
static
void
sighandler
(
int
sig
)
{
restore_stdin
();
// Assume we'll only be called before death
// See note before sigaction() in set_stdin_raw()
//
// Now, close all standard I/O to cause the pumps
// to exit so we can continue and retrieve the exit
// code
close
(
STDIN_FILENO
);
close
(
STDOUT_FILENO
);
close
(
STDERR_FILENO
);
// Put back all the default handlers
struct
sigaction
act
;
memset
(
&
act
,
0
,
sizeof
(
act
));
act
.
sa_handler
=
SIG_DFL
;
for
(
int
i
=
0
;
quit_signals
[
i
];
++
i
)
{
sigaction
(
quit_signals
[
i
],
&
act
,
NULL
);
}
}
void
set_identity
(
unsigned
uid
)
{
/*
* Set effective uid back to root, otherwise setres[ug]id will fail
* if uid isn't root.
/*
* Connect daemon, send argc, argv, cwd, pts slave
*/
if
(
seteuid
(
0
))
{
PLOGE
(
"seteuid (root)"
);
}
if
(
setresgid
(
uid
,
uid
,
uid
))
{
PLOGE
(
"setresgid (%u)"
,
uid
);
}
if
(
setresuid
(
uid
,
uid
,
uid
))
{
PLOGE
(
"setresuid (%u)"
,
uid
);
}
}
static
__attribute__
((
noreturn
))
void
allow
()
{
char
*
argv
[]
=
{
NULL
,
NULL
,
NULL
,
NULL
};
if
(
su_ctx
->
to
.
login
)
argv
[
0
]
=
"-"
;
else
argv
[
0
]
=
basename
(
su_ctx
->
to
.
shell
);
if
(
su_ctx
->
to
.
command
)
{
argv
[
1
]
=
"-c"
;
argv
[
2
]
=
su_ctx
->
to
.
command
;
}
// Setup shell
umask
(
022
);
populate_environment
();
set_identity
(
su_ctx
->
to
.
uid
);
if
(
su_ctx
->
info
->
access
.
notify
||
su_ctx
->
info
->
access
.
log
)
app_log
();
execvp
(
su_ctx
->
to
.
shell
,
argv
);
fprintf
(
stderr
,
"Cannot execute %s: %s
\n
"
,
su_ctx
->
to
.
shell
,
strerror
(
errno
));
PLOGE
(
"exec"
);
exit
(
EXIT_FAILURE
);
}
static
__attribute__
((
noreturn
))
void
deny
()
{
if
(
su_ctx
->
info
->
access
.
notify
||
su_ctx
->
info
->
access
.
log
)
app_log
();
LOGW
(
"su: request rejected (%u->%u)"
,
su_ctx
->
info
->
uid
,
su_ctx
->
to
.
uid
);
fprintf
(
stderr
,
"%s
\n
"
,
strerror
(
EACCES
));
exit
(
EXIT_FAILURE
);
}
__attribute__
((
noreturn
))
void
exit2
(
int
status
)
{
// Handle the pipe, or the daemon will get stuck
if
(
su_ctx
->
pipefd
[
0
]
>=
0
)
{
xwrite
(
su_ctx
->
pipefd
[
1
],
&
su_ctx
->
info
->
access
.
policy
,
sizeof
(
policy_t
));
close
(
su_ctx
->
pipefd
[
0
]);
close
(
su_ctx
->
pipefd
[
1
]);
}
exit
(
status
);
}
int
su_daemon_main
(
int
argc
,
char
**
argv
)
{
int
su_client_main
(
int
argc
,
char
*
argv
[])
{
int
c
;
struct
option
long_opts
[]
=
{
{
"command"
,
required_argument
,
NULL
,
'c'
},
...
...
@@ -160,36 +101,53 @@ int su_daemon_main(int argc, char **argv) {
{
NULL
,
0
,
NULL
,
0
},
};
struct
su_request
su_req
=
{
.
uid
=
UID_ROOT
,
.
login
=
0
,
.
keepenv
=
0
,
.
shell
=
DEFAULT_SHELL
,
.
command
=
""
,
};
for
(
int
i
=
0
;
i
<
argc
;
i
++
)
{
// Replace -cn with -z, -mm with -M for supporting getopt_long
if
(
strcmp
(
argv
[
i
],
"-cn"
)
==
0
)
strcpy
(
argv
[
i
],
"-z"
);
else
if
(
strcmp
(
argv
[
i
],
"-mm"
)
==
0
)
strcpy
(
argv
[
i
],
"-M"
);
}
while
((
c
=
getopt_long
(
argc
,
argv
,
"c:hlmps:Vvuz:M"
,
long_opts
,
NULL
))
!=
-
1
)
{
switch
(
c
)
{
case
'c'
:
su_ctx
->
to
.
command
=
concat_commands
(
argc
,
argv
);
su_req
.
command
=
concat_commands
(
argc
,
argv
);
optind
=
argc
;
break
;
case
'h'
:
usage
(
EXIT_SUCCESS
);
break
;
case
'l'
:
su_ctx
->
to
.
login
=
1
;
su_req
.
login
=
1
;
break
;
case
'm'
:
case
'p'
:
su_ctx
->
to
.
keepenv
=
1
;
su_req
.
keepenv
=
1
;
break
;
case
's'
:
su_ctx
->
to
.
shell
=
optarg
;
su_req
.
shell
=
optarg
;
break
;
case
'V'
:
printf
(
"%d
\n
"
,
MAGISK_VER_CODE
);
exit2
(
EXIT_SUCCESS
);
exit
(
EXIT_SUCCESS
);
case
'v'
:
printf
(
"%s
\n
"
,
xstr
(
MAGISK_VERSION
)
":MAGISKSU (topjohnwu)"
);
exit2
(
EXIT_SUCCESS
);
exit
(
EXIT_SUCCESS
);
case
'z'
:
// Do nothing, placed here for legacy support :)
break
;
case
'M'
:
DB_SET
(
su_ctx
->
info
,
SU_MNT_NS
)
=
NAMESPACE_MODE_GLOBAL
;
/* TODO */
break
;
default:
/* Bionic getopt_long doesn't terminate its error output by newline */
...
...
@@ -199,7 +157,7 @@ int su_daemon_main(int argc, char **argv) {
}
if
(
optind
<
argc
&&
strcmp
(
argv
[
optind
],
"-"
)
==
0
)
{
su_
ctx
->
to
.
login
=
1
;
su_
req
.
login
=
1
;
optind
++
;
}
/* username or uid */
...
...
@@ -207,54 +165,69 @@ int su_daemon_main(int argc, char **argv) {
struct
passwd
*
pw
;
pw
=
getpwnam
(
argv
[
optind
]);
if
(
pw
)
su_
ctx
->
to
.
uid
=
pw
->
pw_uid
;
su_
req
.
uid
=
pw
->
pw_uid
;
else
su_
ctx
->
to
.
uid
=
atoi
(
argv
[
optind
]);
su_
req
.
uid
=
atoi
(
argv
[
optind
]);
optind
++
;
}
// Handle namespaces
switch
(
DB_SET
(
su_ctx
->
info
,
SU_MNT_NS
))
{
case
NAMESPACE_MODE_GLOBAL
:
LOGD
(
"su: use global namespace
\n
"
);
break
;
case
NAMESPACE_MODE_REQUESTER
:
LOGD
(
"su: use namespace of pid=[%d]
\n
"
,
su_ctx
->
pid
);
if
(
switch_mnt_ns
(
su_ctx
->
pid
))
{
LOGD
(
"su: setns failed, fallback to isolated
\n
"
);
xunshare
(
CLONE_NEWNS
);
}
break
;
case
NAMESPACE_MODE_ISOLATE
:
LOGD
(
"su: use new isolated namespace
\n
"
);
xunshare
(
CLONE_NEWNS
);
break
;
}
char
pts_slave
[
PATH_MAX
];
int
ptmx
,
fd
;
// C
hange directory to cwd
chdir
(
su_ctx
->
cwd
);
// C
onnect to client
fd
=
connect_daemon
(
);
if
(
su_ctx
->
pipefd
[
0
]
>=
0
)
{
// Create random socket
struct
sockaddr_un
addr
;
int
sockfd
=
create_rand_socket
(
&
addr
);
// Tell the daemon we are su
write_int
(
fd
,
SUPERUSER
);
// Connect Magisk Manager
app_connect
(
addr
.
sun_path
+
1
);
int
fd
=
socket_accept
(
sockfd
,
60
);
// Send su_request
xwrite
(
fd
,
&
su_req
,
3
*
sizeof
(
unsigned
));
write_string
(
fd
,
su_req
.
shell
);
write_string
(
fd
,
su_req
.
command
);
socket_send_request
(
fd
);
su_ctx
->
info
->
access
.
policy
=
read_int_be
(
fd
);
// Determine which one of our streams are attached to a TTY
int
atty
=
0
;
if
(
isatty
(
STDIN_FILENO
))
atty
|=
ATTY_IN
;
if
(
isatty
(
STDOUT_FILENO
))
atty
|=
ATTY_OUT
;
if
(
isatty
(
STDERR_FILENO
))
atty
|=
ATTY_ERR
;
close
(
fd
);
close
(
sockfd
);
if
(
atty
)
{
// We need a PTY. Get one.
ptmx
=
pts_open
(
pts_slave
,
sizeof
(
pts_slave
));
}
else
{
pts_slave
[
0
]
=
'\0'
;
}
// Send pts_slave
write_string
(
fd
,
pts_slave
);
// Send stdin
send_fd
(
fd
,
(
atty
&
ATTY_IN
)
?
-
1
:
STDIN_FILENO
);
// Send stdout
send_fd
(
fd
,
(
atty
&
ATTY_OUT
)
?
-
1
:
STDOUT_FILENO
);
// Send stderr
send_fd
(
fd
,
(
atty
&
ATTY_ERR
)
?
-
1
:
STDERR_FILENO
);
// Wait for ack from daemon
if
(
read_int
(
fd
))
{
// Fast fail
fprintf
(
stderr
,
"%s
\n
"
,
strerror
(
EACCES
));
return
DENY
;
}
// Report the policy to main daemon
xwrite
(
su_ctx
->
pipefd
[
1
],
&
su_ctx
->
info
->
access
.
policy
,
sizeof
(
policy_t
));
close
(
su_ctx
->
pipefd
[
0
]);
close
(
su_ctx
->
pipefd
[
1
]);
if
(
atty
&
ATTY_IN
)
{
setup_sighandlers
(
sighandler
);
pump_stdin_async
(
ptmx
);
}
if
(
atty
&
ATTY_OUT
)
{
// Forward SIGWINCH
watch_sigwinch_async
(
STDOUT_FILENO
,
ptmx
);
pump_stdout_blocking
(
ptmx
);
}
su_ctx
->
info
->
access
.
policy
==
ALLOW
?
allow
()
:
deny
();
}
// Get the exit code
int
code
=
read_int
(
fd
);
close
(
fd
);
return
code
;
}
\ No newline at end of file
native/jni/su/su.h
View file @
d119dd9a
...
...
@@ -12,6 +12,11 @@
#define DEFAULT_SHELL "/system/bin/sh"
// Constants for atty
#define ATTY_IN 1
#define ATTY_OUT 2
#define ATTY_ERR 4
struct
su_info
{
unsigned
uid
;
/* Unique key to find su_info */
pthread_mutex_t
lock
;
/* Internal lock */
...
...
@@ -33,32 +38,22 @@ struct su_info {
struct
su_request
{
unsigned
uid
;
int
login
;
int
keepenv
;
unsigned
login
;
unsigned
keepenv
;
char
*
shell
;
char
*
command
;
};
struct
su_context
{
struct
su_info
*
info
;
struct
su_request
to
;
struct
su_request
req
;
pid_t
pid
;
char
cwd
[
PATH_MAX
];
int
pipefd
[
2
];
};
extern
struct
su_context
*
su_ctx
;
// su.c
int
su_daemon_main
(
int
argc
,
char
**
argv
);
__attribute__
((
noreturn
))
void
exit2
(
int
status
);
void
set_identity
(
unsigned
uid
);
// connect.c
void
app_log
();
void
app_connect
(
const
char
*
socket
);
void
socket_send_request
(
int
fd
);
void
app_log
(
struct
su_context
*
ctx
);
void
app_connect
(
const
char
*
socket
,
struct
su_info
*
info
);
void
socket_send_request
(
int
fd
,
struct
su_info
*
info
);
#endif
native/jni/su/su_daemon.c
View file @
d119dd9a
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment