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
975b1a5e
Commit
975b1a5e
authored
May 18, 2022
by
topjohnwu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Prune unused UIDs from su policies
parent
e11508f8
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
165 additions
and
80 deletions
+165
-80
Android.mk
native/jni/Android.mk
+1
-0
bootstages.cpp
native/jni/core/bootstages.cpp
+2
-2
db.cpp
native/jni/core/db.cpp
+0
-42
package.cpp
native/jni/core/package.cpp
+114
-0
daemon.hpp
native/jni/include/daemon.hpp
+9
-0
db.hpp
native/jni/include/db.hpp
+0
-3
connect.cpp
native/jni/su/connect.cpp
+1
-1
su.hpp
native/jni/su/su.hpp
+1
-1
su_daemon.cpp
native/jni/su/su_daemon.cpp
+34
-4
deny.hpp
native/jni/zygisk/deny/deny.hpp
+0
-2
utils.cpp
native/jni/zygisk/deny/utils.cpp
+2
-12
entry.cpp
native/jni/zygisk/entry.cpp
+1
-13
No files found.
native/jni/Android.mk
View file @
975b1a5e
...
@@ -23,6 +23,7 @@ LOCAL_SRC_FILES := \
...
@@ -23,6 +23,7 @@ LOCAL_SRC_FILES := \
core/bootstages.cpp \
core/bootstages.cpp \
core/socket.cpp \
core/socket.cpp \
core/db.cpp \
core/db.cpp \
core/package.cpp \
core/cert.cpp \
core/cert.cpp \
core/scripting.cpp \
core/scripting.cpp \
core/restorecon.cpp \
core/restorecon.cpp \
...
...
native/jni/core/bootstages.cpp
View file @
975b1a5e
...
@@ -128,7 +128,7 @@ static bool magisk_env() {
...
@@ -128,7 +128,7 @@ static bool magisk_env() {
unlink
(
stub_path
.
data
());
unlink
(
stub_path
.
data
());
string
pkg
;
string
pkg
;
get_manager
(
&
pkg
);
get_manager
(
0
,
&
pkg
);
sprintf
(
buf
,
"%s/0/%s/install"
,
APP_DATA_DIR
,
sprintf
(
buf
,
"%s/0/%s/install"
,
APP_DATA_DIR
,
pkg
.
empty
()
?
"xxx"
/* Ensure non-exist path */
:
pkg
.
data
());
pkg
.
empty
()
?
"xxx"
/* Ensure non-exist path */
:
pkg
.
data
());
...
@@ -376,7 +376,7 @@ void boot_complete(int client) {
...
@@ -376,7 +376,7 @@ void boot_complete(int client) {
xmkdir
(
SECURE_DIR
,
0700
);
xmkdir
(
SECURE_DIR
,
0700
);
if
(
stub_fd
>
0
)
{
if
(
stub_fd
>
0
)
{
if
(
!
get_manager
()
)
{
if
(
get_manager
()
<
0
)
{
// Install stub
// Install stub
struct
stat
st
{};
struct
stat
st
{};
fstat
(
stub_fd
,
&
st
);
fstat
(
stub_fd
,
&
st
);
...
...
native/jni/core/db.cpp
View file @
975b1a5e
...
@@ -359,48 +359,6 @@ int get_db_strings(db_strings &str, int key) {
...
@@ -359,48 +359,6 @@ int get_db_strings(db_strings &str, int key) {
return
0
;
return
0
;
}
}
bool
get_manager
(
int
user_id
,
std
::
string
*
pkg
,
struct
stat
*
st
)
{
db_strings
str
;
get_db_strings
(
str
,
SU_MANAGER
);
char
app_path
[
128
];
if
(
!
str
[
SU_MANAGER
].
empty
())
{
// App is repackaged
sprintf
(
app_path
,
"%s/%d/%s"
,
APP_DATA_DIR
,
user_id
,
str
[
SU_MANAGER
].
data
());
if
(
stat
(
app_path
,
st
)
==
0
)
{
if
(
pkg
)
pkg
->
swap
(
str
[
SU_MANAGER
]);
return
true
;
}
}
// Check the official package name
sprintf
(
app_path
,
"%s/%d/"
JAVA_PACKAGE_NAME
,
APP_DATA_DIR
,
user_id
);
if
(
stat
(
app_path
,
st
)
==
0
)
{
if
(
pkg
)
*
pkg
=
JAVA_PACKAGE_NAME
;
return
true
;
}
else
{
LOGE
(
"su: cannot find manager
\n
"
);
memset
(
st
,
0
,
sizeof
(
*
st
));
if
(
pkg
)
pkg
->
clear
();
return
false
;
}
}
bool
get_manager
(
string
*
pkg
)
{
struct
stat
st
;
return
get_manager
(
0
,
pkg
,
&
st
);
}
int
get_manager_app_id
()
{
struct
stat
st
;
if
(
get_manager
(
0
,
nullptr
,
&
st
))
return
to_app_id
(
st
.
st_uid
);
return
-
1
;
}
void
exec_sql
(
int
client
)
{
void
exec_sql
(
int
client
)
{
run_finally
f
([
=
]{
close
(
client
);
});
run_finally
f
([
=
]{
close
(
client
);
});
string
sql
=
read_string
(
client
);
string
sql
=
read_string
(
client
);
...
...
native/jni/core/package.cpp
0 → 100644
View file @
975b1a5e
#include <base.hpp>
#include <magisk.hpp>
#include <daemon.hpp>
#include <db.hpp>
using
namespace
std
;
// These functions will be called on every single zygote process specialization and su request,
// so performance is absolutely critical. Most operations should either have its result cached
// or simply skipped unless necessary.
static
atomic
<
ino_t
>
pkg_xml_ino
=
0
;
// pkg_lock protects mgr_app_id and mgr_pkg
static
pthread_mutex_t
pkg_lock
=
PTHREAD_MUTEX_INITIALIZER
;
static
int
mgr_app_id
=
-
1
;
static
string
*
mgr_pkg
;
bool
need_pkg_refresh
()
{
struct
stat
st
{};
stat
(
"/data/system/packages.xml"
,
&
st
);
ino_t
ino
=
st
.
st_ino
;
if
(
pkg_xml_ino
.
compare_exchange_strong
(
ino
,
st
.
st_ino
))
{
// Packages have not changed
return
false
;
}
else
{
mutex_guard
g
(
pkg_lock
);
mgr_app_id
=
-
1
;
return
true
;
}
}
// app_id = app_no + AID_APP_START
// app_no range: [0, 9999]
vector
<
bool
>
get_app_no_list
()
{
vector
<
bool
>
list
;
auto
data_dir
=
xopen_dir
(
APP_DATA_DIR
);
if
(
!
data_dir
)
return
list
;
dirent
*
entry
;
while
((
entry
=
xreaddir
(
data_dir
.
get
())))
{
// For each user
int
dfd
=
xopenat
(
dirfd
(
data_dir
.
get
()),
entry
->
d_name
,
O_RDONLY
);
if
(
auto
dir
=
xopen_dir
(
dfd
))
{
while
((
entry
=
xreaddir
(
dir
.
get
())))
{
// For each package
struct
stat
st
{};
xfstatat
(
dfd
,
entry
->
d_name
,
&
st
,
0
);
int
app_id
=
to_app_id
(
st
.
st_uid
);
if
(
app_id
>=
AID_APP_START
&&
app_id
<=
AID_APP_END
)
{
int
app_no
=
app_id
-
AID_APP_START
;
if
(
list
.
size
()
<=
app_no
)
{
list
.
resize
(
app_no
+
1
);
}
list
[
app_no
]
=
true
;
}
}
}
else
{
close
(
dfd
);
}
}
return
list
;
}
int
get_manager
(
int
user_id
,
std
::
string
*
pkg
)
{
mutex_guard
g
(
pkg_lock
);
char
app_path
[
128
];
struct
stat
st
{};
if
(
mgr_pkg
==
nullptr
)
default_new
(
mgr_pkg
);
int
app_id
=
mgr_app_id
;
if
(
app_id
>
0
)
{
// Just need to check whether the app is installed in the user
snprintf
(
app_path
,
sizeof
(
app_path
),
"%s/%d/%s"
,
APP_DATA_DIR
,
user_id
,
mgr_pkg
->
data
());
if
(
access
(
app_path
,
F_OK
)
==
0
)
{
if
(
pkg
)
*
pkg
=
*
mgr_pkg
;
return
user_id
*
AID_USER_OFFSET
+
app_id
;
}
else
{
goto
not_found
;
}
}
else
{
db_strings
str
;
get_db_strings
(
str
,
SU_MANAGER
);
if
(
!
str
[
SU_MANAGER
].
empty
())
{
// App is repackaged
snprintf
(
app_path
,
sizeof
(
app_path
),
"%s/%d/%s"
,
APP_DATA_DIR
,
user_id
,
str
[
SU_MANAGER
].
data
());
if
(
stat
(
app_path
,
&
st
)
==
0
)
{
mgr_pkg
->
swap
(
str
[
SU_MANAGER
]);
}
}
else
{
// Check the original package name
snprintf
(
app_path
,
sizeof
(
app_path
),
"%s/%d/"
JAVA_PACKAGE_NAME
,
APP_DATA_DIR
,
user_id
);
if
(
stat
(
app_path
,
&
st
)
==
0
)
{
*
mgr_pkg
=
JAVA_PACKAGE_NAME
;
}
else
{
goto
not_found
;
}
}
}
mgr_app_id
=
to_app_id
(
st
.
st_uid
);
if
(
pkg
)
*
pkg
=
*
mgr_pkg
;
return
st
.
st_uid
;
not_found
:
LOGE
(
"su: cannot find manager
\n
"
);
if
(
pkg
)
pkg
->
clear
();
return
-
1
;
}
native/jni/include/daemon.hpp
View file @
975b1a5e
...
@@ -9,6 +9,10 @@
...
@@ -9,6 +9,10 @@
#include <socket.hpp>
#include <socket.hpp>
#define AID_APP_START 10000
#define AID_APP_END 19999
#define AID_USER_OFFSET 100000
// Daemon command codes
// Daemon command codes
namespace
MainRequest
{
namespace
MainRequest
{
enum
:
int
{
enum
:
int
{
...
@@ -81,6 +85,11 @@ void denylist_handler(int client, const sock_cred *cred);
...
@@ -81,6 +85,11 @@ void denylist_handler(int client, const sock_cred *cred);
void
su_daemon_handler
(
int
client
,
const
sock_cred
*
cred
);
void
su_daemon_handler
(
int
client
,
const
sock_cred
*
cred
);
void
zygisk_handler
(
int
client
,
const
sock_cred
*
cred
);
void
zygisk_handler
(
int
client
,
const
sock_cred
*
cred
);
// Package
bool
need_pkg_refresh
();
std
::
vector
<
bool
>
get_app_no_list
();
int
get_manager
(
int
user_id
=
0
,
std
::
string
*
pkg
=
nullptr
);
// Denylist
// Denylist
void
initialize_denylist
();
void
initialize_denylist
();
int
denylist_cli
(
int
argc
,
char
**
argv
);
int
denylist_cli
(
int
argc
,
char
**
argv
);
native/jni/include/db.hpp
View file @
975b1a5e
...
@@ -125,9 +125,6 @@ using db_row_cb = std::function<bool(db_row&)>;
...
@@ -125,9 +125,6 @@ using db_row_cb = std::function<bool(db_row&)>;
int
get_db_settings
(
db_settings
&
cfg
,
int
key
=
-
1
);
int
get_db_settings
(
db_settings
&
cfg
,
int
key
=
-
1
);
int
get_db_strings
(
db_strings
&
str
,
int
key
=
-
1
);
int
get_db_strings
(
db_strings
&
str
,
int
key
=
-
1
);
bool
get_manager
(
int
user_id
,
std
::
string
*
pkg
,
struct
stat
*
st
);
bool
get_manager
(
std
::
string
*
pkg
=
nullptr
);
int
get_manager_app_id
();
void
exec_sql
(
int
client
);
void
exec_sql
(
int
client
);
char
*
db_exec
(
const
char
*
sql
);
char
*
db_exec
(
const
char
*
sql
);
char
*
db_exec
(
const
char
*
sql
,
const
db_row_cb
&
fn
);
char
*
db_exec
(
const
char
*
sql
,
const
db_row_cb
&
fn
);
...
...
native/jni/su/connect.cpp
View file @
975b1a5e
...
@@ -205,7 +205,7 @@ int app_request(const su_context &ctx) {
...
@@ -205,7 +205,7 @@ int app_request(const su_context &ctx) {
strcpy
(
fifo
,
"/dev/socket/"
);
strcpy
(
fifo
,
"/dev/socket/"
);
gen_rand_str
(
fifo
+
12
,
32
,
true
);
gen_rand_str
(
fifo
+
12
,
32
,
true
);
mkfifo
(
fifo
,
0600
);
mkfifo
(
fifo
,
0600
);
chown
(
fifo
,
ctx
.
info
->
mgr_
st
.
st_uid
,
ctx
.
info
->
mgr_st
.
st_g
id
);
chown
(
fifo
,
ctx
.
info
->
mgr_
uid
,
ctx
.
info
->
mgr_u
id
);
setfilecon
(
fifo
,
"u:object_r:"
SEPOL_FILE_TYPE
":s0"
);
setfilecon
(
fifo
,
"u:object_r:"
SEPOL_FILE_TYPE
":s0"
);
// Send request
// Send request
...
...
native/jni/su/su.hpp
View file @
975b1a5e
...
@@ -23,7 +23,7 @@ public:
...
@@ -23,7 +23,7 @@ public:
db_settings
cfg
;
db_settings
cfg
;
su_access
access
;
su_access
access
;
std
::
string
mgr_pkg
;
std
::
string
mgr_pkg
;
struct
stat
mgr_st
;
int
mgr_uid
;
void
check_db
();
void
check_db
();
// These should be guarded with global cache lock
// These should be guarded with global cache lock
...
...
native/jni/su/su_daemon.cpp
View file @
975b1a5e
...
@@ -20,7 +20,7 @@ static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER;
...
@@ -20,7 +20,7 @@ static pthread_mutex_t cache_lock = PTHREAD_MUTEX_INITIALIZER;
static
shared_ptr
<
su_info
>
cached
;
static
shared_ptr
<
su_info
>
cached
;
su_info
::
su_info
(
int
uid
)
:
su_info
::
su_info
(
int
uid
)
:
uid
(
uid
),
eval_uid
(
-
1
),
access
(
DEFAULT_SU_ACCESS
),
mgr_
st
{}
,
uid
(
uid
),
eval_uid
(
-
1
),
access
(
DEFAULT_SU_ACCESS
),
mgr_
uid
(
-
1
)
,
timestamp
(
0
),
_lock
(
PTHREAD_MUTEX_INITIALIZER
)
{}
timestamp
(
0
),
_lock
(
PTHREAD_MUTEX_INITIALIZER
)
{}
su_info
::~
su_info
()
{
su_info
::~
su_info
()
{
...
@@ -82,7 +82,7 @@ void su_info::check_db() {
...
@@ -82,7 +82,7 @@ void su_info::check_db() {
// We need to check our manager
// We need to check our manager
if
(
access
.
log
||
access
.
notify
)
if
(
access
.
log
||
access
.
notify
)
get_manager
(
to_user_id
(
eval_uid
),
&
mgr_pkg
,
&
mgr_st
);
mgr_uid
=
get_manager
(
to_user_id
(
eval_uid
),
&
mgr_pkg
);
}
}
bool
uid_granted_root
(
int
uid
)
{
bool
uid_granted_root
(
int
uid
)
{
...
@@ -137,6 +137,32 @@ bool uid_granted_root(int uid) {
...
@@ -137,6 +137,32 @@ bool uid_granted_root(int uid) {
return
granted
;
return
granted
;
}
}
static
void
prune_su_access
()
{
vector
<
bool
>
app_no_list
=
get_app_no_list
();
vector
<
int
>
rm_uids
;
char
query
[
256
],
*
err
;
strlcpy
(
query
,
"SELECT uid FROM policies"
,
sizeof
(
query
));
err
=
db_exec
(
query
,
[
&
](
db_row
&
row
)
->
bool
{
int
uid
=
parse_int
(
row
[
"uid"
]);
int
app_id
=
to_app_id
(
uid
);
if
(
app_id
>=
AID_APP_START
&&
app_id
<=
AID_APP_END
)
{
int
app_no
=
app_id
-
AID_APP_START
;
if
(
app_no
>=
app_no_list
.
size
()
||
!
app_no_list
[
app_no
])
{
// The app_id is no longer installed
rm_uids
.
push_back
(
uid
);
}
}
return
true
;
});
db_err_cmd
(
err
,
return
);
for
(
int
uid
:
rm_uids
)
{
snprintf
(
query
,
sizeof
(
query
),
"DELETE FROM policies WHERE uid == %d"
,
uid
);
// Don't care about errors
db_exec
(
query
);
}
}
static
shared_ptr
<
su_info
>
get_su_info
(
unsigned
uid
)
{
static
shared_ptr
<
su_info
>
get_su_info
(
unsigned
uid
)
{
LOGD
(
"su: request from uid=[%d]
\n
"
,
uid
);
LOGD
(
"su: request from uid=[%d]
\n
"
,
uid
);
...
@@ -144,6 +170,10 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
...
@@ -144,6 +170,10 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
{
{
mutex_guard
lock
(
cache_lock
);
mutex_guard
lock
(
cache_lock
);
if
(
need_pkg_refresh
())
{
cached
.
reset
();
prune_su_access
();
}
if
(
!
cached
||
cached
->
uid
!=
uid
||
!
cached
->
is_fresh
())
if
(
!
cached
||
cached
->
uid
!=
uid
||
!
cached
->
is_fresh
())
cached
=
make_shared
<
su_info
>
(
uid
);
cached
=
make_shared
<
su_info
>
(
uid
);
cached
->
refresh
();
cached
->
refresh
();
...
@@ -157,7 +187,7 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
...
@@ -157,7 +187,7 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
info
->
check_db
();
info
->
check_db
();
// If it's root or the manager, allow it silently
// If it's root or the manager, allow it silently
if
(
info
->
uid
==
UID_ROOT
||
to_app_id
(
info
->
uid
)
==
to_app_id
(
info
->
mgr_
st
.
st_
uid
))
{
if
(
info
->
uid
==
UID_ROOT
||
to_app_id
(
info
->
uid
)
==
to_app_id
(
info
->
mgr_uid
))
{
info
->
access
=
SILENT_SU_ACCESS
;
info
->
access
=
SILENT_SU_ACCESS
;
return
info
;
return
info
;
}
}
...
@@ -189,7 +219,7 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
...
@@ -189,7 +219,7 @@ static shared_ptr<su_info> get_su_info(unsigned uid) {
return
info
;
return
info
;
// If still not determined, check if manager exists
// If still not determined, check if manager exists
if
(
info
->
mgr_
pkg
.
empty
()
)
{
if
(
info
->
mgr_
uid
<
0
)
{
info
->
access
=
NO_SU_ACCESS
;
info
->
access
=
NO_SU_ACCESS
;
return
info
;
return
info
;
}
}
...
...
native/jni/zygisk/deny/deny.hpp
View file @
975b1a5e
...
@@ -47,8 +47,6 @@ void ls_list(int client);
...
@@ -47,8 +47,6 @@ void ls_list(int client);
// Utility functions
// Utility functions
bool
is_deny_target
(
int
uid
,
std
::
string_view
process
);
bool
is_deny_target
(
int
uid
,
std
::
string_view
process
);
void
revert_unmount
();
void
revert_unmount
();
extern
std
::
atomic
<
bool
>
denylist_enforced
;
extern
std
::
atomic
<
bool
>
denylist_enforced
;
extern
std
::
atomic
<
int
>
cached_manager_app_id
;
native/jni/zygisk/deny/utils.cpp
View file @
975b1a5e
...
@@ -32,23 +32,13 @@ atomic<bool> denylist_enforced = false;
...
@@ -32,23 +32,13 @@ atomic<bool> denylist_enforced = false;
#define do_kill (zygisk_enabled && denylist_enforced)
#define do_kill (zygisk_enabled && denylist_enforced)
static
unsigned
long
long
pkg_xml_ino
=
0
;
static
void
rescan_apps
()
{
static
void
rescan_apps
()
{
{
if
(
!
need_pkg_refresh
())
struct
stat
st
{};
return
;
stat
(
"/data/system/packages.xml"
,
&
st
);
if
(
pkg_xml_ino
==
st
.
st_ino
)
{
// Packages has not changed, do not rescan
return
;
}
pkg_xml_ino
=
st
.
st_ino
;
}
LOGD
(
"denylist: rescanning apps
\n
"
);
LOGD
(
"denylist: rescanning apps
\n
"
);
app_id_to_pkgs
.
clear
();
app_id_to_pkgs
.
clear
();
cached_manager_app_id
=
-
1
;
auto
data_dir
=
xopen_dir
(
APP_DATA_DIR
);
auto
data_dir
=
xopen_dir
(
APP_DATA_DIR
);
if
(
!
data_dir
)
if
(
!
data_dir
)
...
...
native/jni/zygisk/entry.cpp
View file @
975b1a5e
...
@@ -7,7 +7,6 @@
...
@@ -7,7 +7,6 @@
#include <base.hpp>
#include <base.hpp>
#include <daemon.hpp>
#include <daemon.hpp>
#include <magisk.hpp>
#include <magisk.hpp>
#include <db.hpp>
#include "zygisk.hpp"
#include "zygisk.hpp"
#include "module.hpp"
#include "module.hpp"
...
@@ -313,8 +312,6 @@ static void magiskd_passthrough(int client) {
...
@@ -313,8 +312,6 @@ static void magiskd_passthrough(int client) {
send_fd
(
client
,
is_64_bit
?
app_process_64
:
app_process_32
);
send_fd
(
client
,
is_64_bit
?
app_process_64
:
app_process_32
);
}
}
atomic
<
int
>
cached_manager_app_id
=
-
1
;
extern
bool
uid_granted_root
(
int
uid
);
extern
bool
uid_granted_root
(
int
uid
);
static
void
get_process_info
(
int
client
,
const
sock_cred
*
cred
)
{
static
void
get_process_info
(
int
client
,
const
sock_cred
*
cred
)
{
int
uid
=
read_int
(
client
);
int
uid
=
read_int
(
client
);
...
@@ -322,19 +319,10 @@ static void get_process_info(int client, const sock_cred *cred) {
...
@@ -322,19 +319,10 @@ static void get_process_info(int client, const sock_cred *cred) {
uint32_t
flags
=
0
;
uint32_t
flags
=
0
;
// This function is called on every single zygote process specialization,
// so performance is critical. get_manager_app_id() is expensive as it goes
// through a SQLite query and potentially multiple filesystem stats, so we
// really want to cache its app ID value.
if
(
is_deny_target
(
uid
,
process
))
{
if
(
is_deny_target
(
uid
,
process
))
{
flags
|=
PROCESS_ON_DENYLIST
;
flags
|=
PROCESS_ON_DENYLIST
;
}
}
int
manager_app_id
=
cached_manager_app_id
;
int
manager_app_id
=
get_manager
();
if
(
manager_app_id
<
0
)
{
manager_app_id
=
get_manager_app_id
();
cached_manager_app_id
=
manager_app_id
;
}
if
(
to_app_id
(
uid
)
==
manager_app_id
)
{
if
(
to_app_id
(
uid
)
==
manager_app_id
)
{
flags
|=
PROCESS_IS_MAGISK_APP
;
flags
|=
PROCESS_IS_MAGISK_APP
;
}
}
...
...
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